diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 028fe26c4..2fd0abfee 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -25,6 +25,7 @@ "artifactcheck.sh": 5.4, "atcheck.sh": 12.1, "atomscheck.sh": 4.2, +"attribmultidefcheck.sh": 75.0, "attrvocabcheck.sh": 66.7, "baselinecheck.sh": 1.8, "baselinedirtycheck.sh": 9.8, @@ -111,6 +112,7 @@ "deadprecisioncheck.sh": 0.8, "deckcheck.sh": 54.8, "deckclaimcheck.sh": 0.1, +"declinedsitecheck.sh": 35.0, "deeptailcheck.sh": 1.0, "defaultceilingcheck.sh": 20.1, "defectshapecheck.sh": 14.2, @@ -187,6 +189,7 @@ "fnptrcheck.sh": 11.3, "forautobodycheck.sh": 21.6, "forblowupcheck.sh": 1.0, +"forbodyslotcheck.sh": 150.0, "forbudgetmonotoncheck.sh": 2.5, "forcalibfactscheck.sh": 8.0, "forcompresscheck.sh": 6.4, @@ -200,6 +203,7 @@ "forrootlegendcheck.sh": 8.6, "forsectioncollapsecheck.sh": 5.2, "forsigspancheck.sh": 6.0, +"forsoftbudgetcheck.sh": 120.0, "freshclonecheck.sh": 170.8, "freshnesscheck.sh": 4.4, "g1configcheck.sh": 0.8, @@ -428,6 +432,7 @@ "qbaselineproducercheck.sh": 5.0, "qchurncheck.sh": 3.7, "qchurnmemocheck.sh": 4.3, +"qddegradecheck.sh": 20.0, "qdrefpaircheck.sh": 92.2, "qextractionkeycheck.sh": 2.8, "qoriginoraclecheck.sh": 8.7, diff --git a/CHANGELOG.md b/CHANGELOG.md index c6534ed84..66d38b560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,156 @@ Ruby-only `captureRubyMixinBases` and its `captureIncludes` call site are gone; the C++ reader keeps the two guards a tags pattern cannot state (the directive's text, and its class-body position). Output is byte-identical: the map, `--deps --limit=100000`, `--skipped`, `--callers`, `--callees`, `--impact` and `--uses` all match the base build over a real Rails corpus and the Ruby fixtures, and `--lego` lists the same -implementors. `kParserVer` moves 171 -> 173 (the extraction path changed; 172 is 171's rich file tag). +implementors. `kParserVer` moves (the extraction path changed; 176 in this release, see the versions note). + +### Fixed — a function stored through `??`, `||`, `&&`, `or` or `and` is a value use + +`this.getPath = (strict ?? true) ? (options.getPath ?? getPath) : getPathNoStrict` recorded only `getPathNoStrict`: +value-reference capture walked parentheses and ternary branches but stopped at a logical operator, so `--callers`, +`--impact`, `--safe-delete` and `--dead-code` never saw `getPath` stored there. The walk now climbs the +value-preserving operators — JS/TS `??`, `||`, `&&` and the logical assignments `??=`, `||=`, `&&=`; Python `or`, +`and` — in every position it already read (assignment, return, argument, parameter default). The left operand of +`&&`/`and` only decides and stays no row; C/C++ and Go `||`/`&&` yield a truth value and stay no row. Parser version +176 in this release (caches re-parse once; see the versions note). Gate: `test/recallshapecheck.sh` arms OP1–OP17. + +### Added — `--grep` names what a hit's variable carries and who reads it + +When a hit sits inside a variable, constant or initialiser (its enclosing definition's kind is a variable), the `` +row now says where that value goes instead of only a call count nobody can satisfy: `reads=N reads_at=` are the +uses verb's read sites for the name (first three, in tier, path, line order — not the order the uses verb prints; only where that verb indexes reads — +Python, C++ and ObjC outside headers — and absent, never zero, elsewhere), and `vals=` names up to three function +values the carrier stores, as `slot=fn@line` of the one definition each resolves to (`vals_total=` beyond three). +When the top hit's carrier has reads, the root `next=` is `--uses=`. The MCP `grep` verb's `enclosing` +rows carry the same `reads`/`reads_at`/`vals` keys (`src/grepcarrier.h`, shared). Reads are rich-family facts, so a +lean `--grep` run counts them only when the TOP hit sits in such a carrier (the row that decides `next=`): it then +ingests the same root(s) once more through the rich warm cache — per root on a workspace run, so the CLI and the MCP +workspace print the same reads — or uncached when `--cache=F` names the cache (F keeps the lean blob; no other cache file +is written). Any other page costs nothing extra; a lower carrier keeps `floor_next="--uses="` as its clue. The +second ingest runs under the memory guard (`--max-memory`, then the hard line): a stop keeps the lean answer and the +root says so with `reads_skipped=memory_stop|hard_limit` and `reads_next=--uses=`. Gate: +`test/countfloorcheck.sh` arm P (P/top, P/mem, P/cache, P/ws). + +### Added — an element access is a call of the receiver class's subscript operator, where the receiver is proven (Ruby, Python) + +Ruby `recv[ k ]` runs the receiver class's `[]` (`[]=` as an assignment's target), and Python `obj[ k ]` its +`__getitem__` (`__setitem__`). Neither was a call before, so rake's `--callees=invoke_task` missed `t = self[ name ]`, +which runs TaskManager#[] through Application's `include TaskManager`. The site is now a call **only where a receiver +rule proves the class**: Ruby's lookup on `self` in a def (through include/extend ancestry), a receiver the code builds +(`r = Registry.new`, `Registry.new[ k ]`), a class constant's own `def self.[]` (`Rake::Task[ name ]`); Python's receiver +evidence (`self` in a class or its bases, a typed parameter, a constructed local, a field the class assigns). Every +other element access — a Hash/Array/dict/list local, a literal, an untyped parameter, a Python class object — makes no +edge, no `via="name"` hedge and no count: a `[]` or `__getitem__` definition elsewhere in the tree never decides it by +name. It does NOT mean every subscript of an in-tree class is found: a Ruby instance variable receiver is not typed, a +compound assignment (`h[ k ] ||= v`) is recorded as the read form, and `del o[ k ]`, a multiple-assignment target and an +annotation record nothing. On rake 93 callers of `[]` appear; on textual 42 of `__getitem__`. C++ `operator[]` could +share the path and is not armed. `--uses` lists no subscript site. Parser version 176 in this release (see the versions note). Gate: +`test/subscriptopcheck.sh`. + +### Added — `--lego=M` lists Ruby's main object when a file's top level mixes M in + +A top-level `include M`, `extend M` or `self.extend M` makes Ruby's main object answer M's methods (rake's +dsl_definition.rb ends with `self.extend Rake::DSL`). `--lego=M` now lists it as ``, one row per directive, counted once in `implementors=` (every directive mixes into the same object), +with a present-only legend clause. Ruby-only: no other indexed language has +an object that a file's top level mixes a module into. Not read: a directive inside a top-level `if`/`begin` or a block, +`prepend`, a mixin onto another object, and a module the tree never opens. The ranked `--for` lego section is unchanged. +Gate: `test/subscriptopcheck.sh` (L arms). + +### Changed — ripwire neutralises repository-configured filter and diff programs for its own git children + +Every git command ripwire runs now pins the filter drivers the repository's own config defines to an empty command, carries `--no-ext-diff --no-textconv` on `diff`/`log`/`show`/`blame` and `--ignore-submodules=dirty` on `status`/`diff`, and keeps signature display off; stderr discloses the pinning as `git_harden=filter-driver`, and `--doctor`'s `git-config-trust` row reports it (`filter_drivers=`, `filter_scope=`, `filter_pinned=`). Visible effects: files behind a repository-local filter (git-crypt, a per-repository LFS driver) are seen unfiltered, as stored; edits inside a submodule no longer mark the stamp `+dirty`; a user's own textconv no longer shapes `--numstat` (such binary rows read `-`); and a pinned driver name applies to every root in the same run. Gate: `test/gitchildoptscheck.sh`. + +### Fixed — an Elixir call inside `unquote(...)` under `quote` is a call again; quoted call sites are counted (#357, PR #380 by @LaimaWu) + +Everything under a `quote` used to be dropped, so a helper called only from `unquote(...)` or `unquote_splicing(...)` +inside a macro's quote had no caller edge from that macro. The quoted body is now read through one shared walk +(`src/ingest_inert.h`, `inInertRegion`), with Elixir deciding per ancestor whether a region is inert or a live hole +(`src/ingest_elixir.h`): calls inside `unquote(...)`/`unquote_splicing(...)` are live, as are `bind_quoted:` option +expressions; `bind_quoted` turns unquoting off unless `unquote: true` turns it back on, and `unquote: false` keeps the +argument inert; a nested quote inside a live hole is inert again. Ordinary call, operator and named-capture candidates +that stay inert mint no edge and are counted: the map's `declined=` includes them (its full legend says so where any were +counted) and `--callees` counts them in `declined_calls=` on their enclosing function. They are not possible callers: +`--callers`, `--uses` and `--impact` of a function called only from inert quoted code still read zero. Not read yet: a +bracketed options list (`quote [unquote: false] do`), under which a hole is taken as live. Gate: `test/elixircheck.sh`. + +### Added — the call sites the resolver declined are named, as hedged `` rows + +A call the resolver declines to bind (two or more same-language candidates and no evidence to choose one, the builtin-method +and C++ standard-member name gates, Ruby's class-versus-instance side cut and `new`) gets no edge, and `declined_calls=` counted +it without naming it: "what does doPackage call" missed a call through a Go interface value, and "who calls +TaskManager#define_task" missed the tests that call it on an untyped receiver. `--callees` now lists each such call the +definitions make as ``, and `--callers` lists each declined call whose candidates include the +selector's definition as `` (by the stored candidate list, never by the called name). +`gate="builtin"|"std"|"side"` names the rule that declined it; candidates (`cand=`) print only with `--legend=full`. A row is a +call SITE, never an edge: it is not in `count=`, the `` rows, `hop_tested=`/`hop_untested=` or any reach, and the legend says +the call may reach none of its `cands=` definitions. 16 rows by default (`--limit` raises it; the callers form is cut +file-first), with `dc_shown=`/`dc_next=` past the cap. The same rows ride `--json` (`"dc"`), `--format=columnar` and the MCP +twins (`"declined_sites"`). `--safe-delete` now reads `risk="possible-callers"` (not `none-found`) for a definition only a +declined call could have meant, and lists those calls; `--impact` prints no rows and its legend names `--callers` as where the +first-hop sites are listed. One record per declined call (13 B); the graph is rebuilt every run, so no cache version moves. + +### Changed — a multi-definition answer says which definition each row belongs to + +A selector that names several same-named definitions (`--callers=empty` over two classes' `empty`) used to answer with the +union of their callers, callees or dependants, and only `defs=` said there was more than one. `--callers`, `--callees`, +`--safe-delete`, `--impact` (and its file rollup) and their MCP twins now list the definition GROUPS as +`` rows (JSON `def_table`) and tag every row with `of_def="K..."`: `K` a resolved edge +ties the row to group K, `K~s` its edge carries the resolver's `split` label, `K~n` by name alone, `K~t` a resolved edge into a +primary template while a specialisation of it is another group. A group is a definition with the declarations tied to it — the +C/C++/ObjC declaration identity `--edit-check` already uses, and a TypeScript implementation with its overload signatures; a +declaration that identity rule cannot tie stays silent unless a row names it. Above 16 groups the table lists the ones the +page's rows name (`defs_listed=`). Labels come from the call graph's own edge provenance; they are the resolver's binding, not +a type check, and a hedged token does not mean the edge is false. +Two by-design byte changes ride along: a row (or a `--path` hop) whose edge to the selector is one arm of a resolver split +carries `prov="split"` on every answer, single-definition ones included; and `--path` leaves out `to_p=` (`from_p=`) when that +endpoint has several definitions and no proven hop bound one. Measured base vs head on 217 CLI argv over 7 repositories and +16 MCP requests (single-definition selectors, prototype + definition pairs, every dialect): 212 and 16 byte-identical; the +other five are the split-arm break above (2) and one C++ prototype whose parameter types are spelled differently from its +definition (`std::string` vs `string`), which the declaration identity rule reads as another function (3). +On `--for`, a body's or a hop's `` row whose edge is a resolver split arm carries the same `prov="split"`, defined in the +bundle's first-screen legend when any listed callee edge can carry it (the `via="name"` rule's over-approximation). ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. +### Changed — `--quality-delta=A..B` answers the kinds it can under memory pressure, and exits 6 when that answer has no gating row (#387 follow-up) + +A tripped memory guard in the range form used to give exit 5 and nothing, so on a large repository a four-file change +could not get even the findings those four files decide. Now, when the guard stops either commit's full tree (a crawl +or parse stop, or the hard limit), or when a projection after the base commit says the target phase will not fit the +limit, the range form releases what it loaded and ingests only both commits' **changed files** (never through a cache). +There it runs the kinds those files decide — api-surface, complexity, error-masking, nesting, params, placeholder, +verbosity, and defect-shape when the changed files define the same C-family macro names on both sides (and either the +base's whole macro set was read or no changed file is C++) — with the same code and bars as a full answer, so their +rows are the full answer's rows of those kinds. dead-code, duplication and new-clone-of-reused-helper need every file +and are skipped. Every cut is named in the header: `kinds_skipped=`, `skipped_why="memory"`, +`ingest_scope="changed-files"`, `memory_limit=`, `memory_need=` (the projected need, or `"unmeasured"` after a stop, +with `memory_stop=` naming it), `also_skipped=` (config-warnings, stale-acks, and ack-healing when the ledger is not +empty: there acks match by their recorded key only), and `kinds_next=`, the call that runs exactly the skipped kinds on +the same two commits. One stderr line says the same. + +- **Exit codes.** A partial answer exits **2** when a row of a kind that ran gates (the skipped kinds can only add + rows, so that verdict is final — except, with ack-healing skipped, for a row only an identity-healed ack hides in the + full answer) and **6** (new) otherwise — a partial answer is never a clean exit 0. Exit **5** now + means, for this form, that even the changed files did not fit, or that they cannot stand in for the trees (a symlink + anywhere, an export-subst attribute, or a changed `.gitattributes` with an export attribute), on one line naming why. + Exit 6 also when every selected kind ran: the `also_skipped=` facts are still missing. `--quality-ack` needs every + kind's rows, so the projection never pre-empts it: it runs in full, and only a real memory stop refuses it (exit 5, + ledger untouched). A refusal whose process still held more than the limit after releasing the full tree says the + changed files were not tried. +- **`--quality-kinds=K[,K…]`** (new, range form only) runs only the named kinds — the replay `kinds_next=` names. A + named kind's rows are its rows in a full answer; under pressure the kinds it left out stay the caller's + (`skipped_why="selector,memory"`, with `memory_skipped=` naming memory's share, which `kinds_next=` replays); stale acks are judged for the named kinds only; passes no named kind + needs (the clone passes, the dead-code evidence) do not run. Refused beside `--quality-ack`/`--quality-baseline` + and on the working-tree form; the MCP `quality_delta` tool (working-tree form) has no selector yet. +- **`memory_peak=`** (new, present-only) on any range answer whose peak footprint went over the limit after the guard's + last reading, with one stderr line; the exit code is unchanged. +- **Behaviour change, by design:** `test/memguardcheck.sh` B23 pinned "a cut base ingest → exit 5 and nothing". That is + the refusal this change replaces: the cut tree is still never scored (one cut ingest, then two changed-file ingests, + and no row names a file outside the diff), and a twin arm keeps the exit-5 refusal when even the changed files are cut. + When memory suffices the answer is byte-identical to before. Gate: `test/qddegradecheck.sh`. + ### Fixed — `--quality-delta=A..B` indexed the working tree it never reads (#387) The range form materializes and indexes both commits itself. `main()` still ingested and graphed the working tree @@ -99,6 +243,18 @@ memory puts the leaf over `--max-memory=256M` (exit 5; the 0.6.7 rule answers, e does not (exit 0). `--help` says what is measured, and the MCP refusal at the hard line names the cgroup, not this server, when the cgroup's charge is the larger reading: restarting the server does not help there. +### Changed — the versions this release moves, stated once + +`kParserVer` 167 → 176: the Clojure grammar (#349) took 171 on `main` and slice 4a (#400) 173 (its full-use file tag 174); the value-reference walk through +`??`/`||`/`&&`/`or`/`and` carried 173 on its branch, the Ruby/Python subscript references and Ruby's main-object mixins +174 (file tag 175), and the Elixir quote live holes (#380) 145 on theirs. They land together at 176, above every number a +branch build has used, so no cache such a build wrote is read as this release's; its full-use file tag is 177. +`kCacheVersion` 31 → 32: a reference record's role byte gained two values, the subscript role (9) and the inert role (10, +numbered 9 on #380's branch, which predates the subscript role), and the binding kind byte one (Ruby's main-object mixin); no record changes shape. `kQSnapCacheScheme` 20 → 21: +the calls that keep a definition alive moved (proven subscript calls, live `unquote(...)` calls), so the dead kind can +differ for an unchanged commit. 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=f83935bb22c0b728 entries=885`. + ## [0.6.7] — 2026-10-11 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d4cb5e60..203482172 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -502,6 +502,29 @@ rules it enforces: (Elixir today; a Lisp-style `defn` if one is added) keep a small keyword table in C++. That table belongs in `tags.scm` once the mechanism lands (#355, slice 5a); it is not a pattern to extend. +#### Where language-specific code lives + +Languages differ in fewer places than their code suggests. Language-specific code therefore has three homes, and shared code reads each one at a named point: + +1. **Facts** are decided once per language: a boolean, an enum or a short list (does `x.f()` ever reach a free function, which line-comment markers the language has). + They are columns of one `constexpr` table with one row per `Lang`. A missing row or a missing required column is a compile error. + This table is being introduced by slice 6c. Until it lands, extend the nearest existing per-`Lang` table and never add a new `l == Lang::X || …` predicate. +2. **Hooks** are for an algorithm that genuinely differs (a different tree walk or binding rule). A hook is a named function pointer in a per-language hook table, and an empty slot means the shared path. + The body lives in that language's own file (`src/ingest_.h`), with a one-sentence reason above it. Slice 3a builds the first hook table, for `captureTagsFacts`. +3. **Family rows** belong to a mechanism that only some languages use, such as the qualified-call resolver of slice 6b. It keeps its own small `constexpr` rows beside its shared handler, and a language without a row does not use it. + +A language-only feature with no counterpart elsewhere, such as Rails schema files, goes in the language's file and still emits the shared fact kinds. + +To decide where a change goes: +- If a second language has the concept, even spelled differently, it belongs to the shared handler, and the difference is a `tags.scm` capture or a table entry. +- If the difference is a value, it is data. +- Only an algorithm gets a hook. + +Reviewers look for a new `Lang::` comparison in a shared file, a `Foo` that `--clones` pairs with another language's `Foo`, and a hook without its reason. +A clone pair is not proof of one concept: two keyword lists can match token for token and mean different things. Fold them into data, not into one function. + +**Most wanted: Rust, C++ and Go.** Most contributions so far have improved Python, TypeScript and Ruby, and those answers improved release over release. Rust, C++ and Go answers have improved less. If you write Rust, C++ or Go, a slice below that touches your language is the most useful place to start. Three are written up as ready-to-claim issues: Go method scope (#403), `--lego` for Go interfaces (#404) and the standard-member gate for Rust (#405). Others are marked **wanted** in the table: 1a for Rust (`use` trees and `mod`), 2b for Rust renames, 4c for Go interfaces (#386), 7a for Go `MethodByName` (#375), and 11a for Rust's test-scope walker (#356). A question ripwire answers badly on a real Rust, C++ or Go repository makes a good issue even without a fix: include the command, what it printed, and what the right answer is. + #### Feature families: one shared handler each The same language feature is often implemented several times in per-language code (before #358 there were ~21 separate @@ -556,17 +579,17 @@ review will credit it, but it is never a condition for merging the language. | Family | Shared mechanism today | State | Claimable slices | |---|---|---|---| -| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS, Go and Python read the capture (Python since #382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. | -| 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: Python's `@import.names` / `@import.alias` captures, read in `captureTagsFacts` (`ingest_sidecap.h`) and turned into binds by the Python-only `emitCapturedPythonImportBind` (`ingest_importcap.h`); `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). | +| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS, Go and Python read the capture (Python since #382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`; **wanted**), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. | +| 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: Python's `@import.names` / `@import.alias` captures, read in `captureTagsFacts` (`ingest_sidecap.h`) and turned into binds by the Python-only `emitCapturedPythonImportBind` (`ingest_importcap.h`); `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, **wanted**; Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). | | 3. Types | `captureTagsFacts` (`ingest_sidecap.h`) reads `@definition.*` captures for every language. | shared, but language decisions are `Lang::` branches inside the loop: 36 non-comment `Lang::` mentions, 15 of them Elixir. | **3a** #374: one constexpr per-language hook table that `captureTagsFacts` calls once per capture, one language per commit, Elixir first. M. | -| 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins read the same path through `@reference.mixin` captures since slice 4a (#400). | **4a** done (#400): Ruby `include`/`extend`/`prepend` read the shared path through `queries/ruby/tags.scm` captures, `captureRubyMixinBases` retired. **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first. | +| 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins read the same path through `@reference.mixin` captures since slice 4a (#400). | **4a** done (#400): Ruby `include`/`extend`/`prepend` read the shared path through `queries/ruby/tags.scm` captures, `captureRubyMixinBases` retired. **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first (Go **wanted**). | | 5. Callables | Definitions come from `tags.scm`, read by `captureTagsFacts`. | partly. Keyword-headed definitions keep keyword tables in C++: `elixirFunctionKeyword` and `elixirMetadataAttribute` (`ingest_elixir.h`) repeat `macroBodyKeyword` (`ingest_relations.h`), 0.82 and 0.96 similar by `--clones`. | **5a** #355: the tags pass evaluates `#eq?`/`#any-of?`/`#not-any-of?`, then Elixir's table moves into its `tags.scm`. M; Elixir, then Clojure. | | 6. Dispatch | `buildGraph` (`graph.h`): a SCIP pin, then the canonical tier (`appendCanonicalCandidates`, `resolve.h`), bindings and receiver rules (`receiverevidence.h`), then name tiers. Per-language qualified-call rules: `ElixirResolver` (`elixir_resolve.h`), `keepRustQualifiedCandidates` and `keepStdQualifiedCandidates` (`graph.h`). | partly. Facts about each language are scattered `l == Lang::X \|\| …` predicates (`implicitReceiverLang`, `memberNeverReachesFreeFunction` and `classNameReceiverLang` in `receiverevidence.h`; `langUsesHashComment` in `quality.h`), and `--clones` reports them as one group. | **6a** the shared refusal for an unplaced module qualifier (the known gap above): S, *maintainers*, not yet scheduled. **6b** the **language-neutral qualified-call resolver**: generalise `ElixirResolver`'s module table (alias/import/require facts → `Module::name`) so that Clojure `ns/f`, Elixir and other module-qualified languages use one resolver driven by per-language data. M; Elixir byte-identical, Clojure as the first new user. **6c** those predicates become columns of one constexpr per-`Lang` table: S, all languages. | -| 7. Data-driven dispatch | One mechanism per shape: fn-pointer and callback bindings `buildFnPtrBindTables` (`graph.h`, C/C++); HTTP routes `routesVisitNode` (`ingest_sidecap.h`, a JS/TS and Python walk) into `RouteDef`/`RouteUse` (`model.h`); FFI aliases `BindingAlias` (`model.h`). | partly. Calls by a literal method name (`send(:m)`) resolve for Ruby only. | **7a** #375: one capture for a call whose method name is a literal, resolved through the ordinary receiver rules. M; Python `getattr`, JS/TS `obj["m"]`, Go `MethodByName`. **7b** route registration read from captures instead of the walk: M; JS/TS and Python. | -| 8. Preprocessing | `retagMacroCallReferences` (`model.h`) for macro calls. Inert regions (quote, discard, comment forms) are per language today, for example `elixirQuoted` (`ingest_elixir.h`). | duplicated. A shared inert-region mechanism is in review (#357, PR #380). | **8a** after #380 and #349 are both on `main`, Clojure's quote/discard filter moves onto the shared mechanism: S, Clojure. | +| 7. Data-driven dispatch | One mechanism per shape: fn-pointer and callback bindings `buildFnPtrBindTables` (`graph.h`, C/C++); HTTP routes `routesVisitNode` (`ingest_sidecap.h`, a JS/TS and Python walk) into `RouteDef`/`RouteUse` (`model.h`); FFI aliases `BindingAlias` (`model.h`). | partly. Calls by a literal method name (`send(:m)`) resolve for Ruby only. | **7a** #375: one capture for a call whose method name is a literal, resolved through the ordinary receiver rules. M; Python `getattr`, JS/TS `obj["m"]`, Go `MethodByName` (**wanted**). **7b** route registration read from captures instead of the walk: M; JS/TS and Python. | +| 8. Preprocessing | `retagMacroCallReferences` (`model.h`) for macro calls. Inert regions (quote, discard, comment forms): `inInertRegion` (`ingest_inert.h`) for Elixir's quote and live holes; Clojure's `clojureInert` (`ingest_clojure.h`) is still per language. | partly. The shared inert-region walk `inInertRegion` (`ingest_inert.h`) landed with #380 (#357); Elixir's quote and live holes use it, Clojure's filter does not yet. | **8a** Clojure's quote/discard filter (`clojureInert`, `ingest_clojure.h`) moves onto `inInertRegion`: S, Clojure. Ready now that #380 and #349 are both on `main`. | | 9. Scope and state | `enclosingScopeOf` (`ingest_names.h`); module and global state in `computeNonLocalState` (`nonlocalstate.h`). | duplicated: `rustEnclosingScopeOf` and `kotlinEnclosingScopeOf` are 0.82 similar to the generic walker by `--clones`. | **9a** #356 part 1: one table-driven enclosing-scope walker (scope-owner kinds, how the name is read). S–M; Rust and Kotlin. Read the notes above both functions first: each says why it was kept apart. If a table does not make the code smaller, a written finding that closes the slice is a valid result. | | 10. Entry points | `inventoryEntryStyle` (`mapinventory.h`): one per-language table of entry conventions (`main`, a static `Main`, `__main__.py`). | shared. Entries declared in a manifest or by an attribute are not detected, and the legend says so. | **10a** manifest-declared entries (`package.json` `main`/`bin`, `console_scripts`, Cargo `[[bin]]`): one reader per manifest format feeding the same table. M. **10b** attribute entries (Swift `@main`): S. | -| 11. Tests | `inFileTestScope` (`ingest_names.h`) dispatches to `pythonInFileTestScope`, `rustInFileTestScope`, `jsInFileTestScope` and `csharpInFileTestScope`. | duplicated: four functions. | **11a** #356 part 2: a table of test-scope shapes. S–M; Python, Rust, JS/TS, C#. The note above `inFileTestScope` says the four rules have different shapes, so measure before folding, as in 9a. | +| 11. Tests | `inFileTestScope` (`ingest_names.h`) dispatches to `pythonInFileTestScope`, `rustInFileTestScope`, `jsInFileTestScope` and `csharpInFileTestScope`. | duplicated: four functions. | **11a** #356 part 2: a table of test-scope shapes. S–M; Python, Rust (**wanted**), JS/TS, C#. The note above `inFileTestScope` says the four rules have different shapes, so measure before folding, as in 9a. | | 12. Async | None. | none. | **12a** step (c) only: open a design issue (what counts as the callee of a goroutine, callback or future) with a measured miss before writing code. | #### Definition of done for a common-code slice @@ -994,6 +1017,13 @@ round. Each takes minutes to check; each miss costs a review round. 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. +22. **A zero-hit word stays out of what the repository indexes.** A gate that queries a word to prove an absence + keeps that word out of every file the tree indexes: gate scripts, fixtures, and function bodies a test script + writes. Otherwise the gate's own text becomes the hit. Run the zero-hit query on the repository root and on the + gate's own fixture directory; both must stay at zero hits. +23. **A new rule that repeats a reviewed helper reuses it.** Before you write a new predicate or rule (a scope, an + owner, a tie-break, a gate), look for the one that already exists (`ripwire . --exemplar=…`, `--grep=…`, or the + `ripwire-reuse-first` skill). Name the helper you reuse, or say why none fits. **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 diff --git a/README.md b/README.md index bf380fff8..9556d97c0 100644 --- a/README.md +++ b/README.md @@ -982,9 +982,9 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers
-190 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 +192 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 191 long flags advertised in `--help`, across seven families — plus an MCP +Around the core sit 192 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. @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +692 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 **685 gate scripts** and is the authoritative list; +`test/regression.sh` names **692 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 @@ -2684,7 +2684,7 @@ identity of the index, and says which one is at fault. ### 5. Command families -The `--help` output groups 191 long flags advertised in `--help` into seven families. The `--help=` +The `--help` output groups 192 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. @@ -2878,7 +2878,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 253 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 259 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/SECURITY.md b/SECURITY.md index acd274412..2b1ec2623 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,7 +18,7 @@ ripwire is a command-line indexing tool with the following security model: - **Input:** Arbitrary source code repositories on the local filesystem - **Output:** XML summaries and analysis results streamed to stdout or written to cache - **Trust boundary:** The tool operates only on local files with the user's own permissions. It does not communicate over the network, with two named exceptions: a root given as a git URL is cloned into the cache (`git clone --depth=1`, with `protocol.ext.allow=never` and `protocol.file.allow=user` pinned on the command), and the tree-sitter registry is listed during initial setup -- **Subprocesses:** ripwire runs **read-only `git`** inside the analysed checkout (`status --porcelain`, `ls-files`, `log`, `diff --numstat`, `archive`, `rev-parse`), and — only for binary document formats, which are not collected by default — `markitdown`. Git honours the checkout's **own** `.git/config`. A *hook-form* `core.fsmonitor` there is a command git would run on every one of those calls, so ripwire neutralises that one key for its own git children at process start and says so (`--doctor`'s `git-config-trust` row; a stderr line carrying `git_harden=fsmonitor-hook`); boolean values, git's builtin daemon, are left alone. `git clone` never copies `.git/config`, so a repository you cloned yourself carries no such key — a tarball, a copied worktree or a shared checkout carries whatever its last owner wrote. Treat pointing ripwire at one as you would treat running `git status` there yourself +- **Subprocesses:** ripwire runs **read-only `git`** inside the analysed checkout (`status --porcelain`, `ls-files`, `log`, `diff --numstat`, `archive`, `rev-parse`), and — only for binary document formats, which are not collected by default — `markitdown`. Git honours the checkout's **own** `.git/config`. A *hook-form* `core.fsmonitor` there is a command git would run on every one of those calls, so ripwire neutralises that one key for its own git children at process start and says so (`--doctor`'s `git-config-trust` row; a stderr line carrying `git_harden=fsmonitor-hook`); boolean values, git's builtin daemon, are left alone. ripwire likewise neutralises repository-configured filter and diff programs for its own git children: the repository's own `filter..clean/smudge/process` drivers are pinned to an empty command on every git child ripwire starts (a stderr line carrying `git_harden=filter-driver`, and `--doctor`'s `git-config-trust` row: `filter_drivers=`, `filter_scope=`, `filter_pinned=`), diff programs are refused with `--no-ext-diff --no-textconv`, signature display is pinned off, and `status`/`diff` do not enter submodule repositories (`--ignore-submodules=dirty`). The MCP and LSP servers apply the same pinning to every root a request names. The user-visible cost: files behind a repository-local filter (git-crypt, a per-repository LFS driver) are read as stored, and edits inside a submodule no longer mark `+dirty`. `git clone` never copies `.git/config`, so a repository you cloned yourself carries no such key — a tarball, a copied worktree or a shared checkout carries whatever its last owner wrote. Treat pointing ripwire at one as you would treat running `git status` there yourself Security vulnerabilities relevant to this tool include: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a238ba71b..f421bda02 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -166,7 +166,16 @@ metadata do not become executable calls. Alias/import/require/use and behaviour module dependencies resolved through declared module identities, regardless of umbrella/file layout. `@behaviour` and `defimpl` supply contract/implementation relationships for `--uses` and `--lego`. -**Static limits:** quoted AST and macro-generated definitions are not expanded. `use` records the +**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call, +operator and named-function-capture candidates inside a `quote` body mint no edges and are counted +in the map's `declined=` gauge; `--callees` counts those sites on their enclosing function. +Bare variable-shaped AST identifiers are not counted merely for being identifiers. Calls inside `unquote(...)` and +`unquote_splicing(...)` are live when enabled, as are expressions in `bind_quoted:` options. +`bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and +`unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. +Nested quotes become inert again inside a live hole; an inner quote that is itself inert +cannot execute its own holes in the current stage. Quoted module declarations do not change the lexical module of evaluated holes. Quoted declaration heads and metadata +do not suppress evaluated hole arguments; bound variables in those arguments remain variables. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call @@ -197,9 +206,7 @@ that function — the underscore rule is for unused variables, and a bare `_seed (`src/ingest_elixir.h`); and a call that omits a defaulted argument reaches the bodyless head that evaluates the default beside the clauses, so `--path` and `--impact` see the default expression's calls from that caller, while a call that supplies the argument reaches the clauses alone -(`src/elixir_resolve.h`). One gap stays open: executable `unquote(...)` and `bind_quoted:` expressions -under `quote` are omitted with the rest of the quoted-AST filter, so a helper called only from inside -an `unquote` has no caller edge from its macro. +(`src/elixir_resolve.h`). `test/elixircheck.sh`, `test/eliximportcheck.sh` and `test/elixirsemanticcheck.sh` cover extraction, metrics, exact target selection against decoys, lexical boundaries, contracts, CLI/MCP use-site parity, diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a5ab7628a..ad1389803 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-11.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-16.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. @@ -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) · [`--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) · [`--show-stale`](#--show-stale) · [`--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-full-text`](#--whereis-full-text) · [`--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) · [`--show-stale`](#--show-stale) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--quality-kinds`](#--quality-kindskk) · [`--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-full-text`](#--whereis-full-text) · [`--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,20 +68,20 @@ _Same map, capped to the 5 highest-ranked symbols._ ``` $ ./build/ripwire . --top-k=5 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -104,20 +104,20 @@ _The same capped map without its inventory tier: --top-k=5 leaves files out, so ``` $ ./build/ripwire . --top-k=5 --no-inventory - + - - + + - + - + ... [7 more line(s); run it to see the whole thing] ``` @@ -138,10 +138,10 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ ``` $ ./build/ripwire . --max-tokens=1500 - + - - + + @@ -159,9 +159,9 @@ $ ./build/ripwire . --max-tokens=1500 ### `--token-budget=N[K|M|G]` -**Answers:** cap the whole document at N tokens — a hard gate on the map, a trim-to-fit on the task lenses two personalities depending on the verb: - default map / --query / --recall: a CI GATE — exit 3 if the emitted DOCUMENT's est_tokens exceeds N. +**Answers:** N tokens: a hard gate on the map, a soft row target on the task lenses two personalities depending on the verb: - default map / --query / --recall: a CI GATE — exit 3 if the emitted DOCUMENT's est_tokens exceeds N. -That is the map PLUS every block appended after it (///), each charged from the bytes it actually emits at the calibrated rate for what those bytes are — so --pack-top-n=3 --token-budget=600 gates on the ~67KB it would stream, not on the map alone. (test/tokenbudgetcheck.sh reports the live MAPE vs tiktoken o200k when tiktoken is installed; the estimate is calibrated, never exact — Claude's tokenizer is not public.) Within budget: exit 0, output unchanged. ASSERTS and fails, vs --max-tokens which shapes to fit — composable: set neither, either, or both (e.g. --max-tokens=16000 --token-budget=16K), but see --max-tokens above: the two Ns are measured in different units. Over budget, nothing of the artifact reaches stdout — only a small record naming withheld_est_tokens= vs budget=, the same vocabulary --recall uses, since est_tokens= is normatively about what a run PRINTED. On --recall the check likewise runs BEFORE a byte of the bundle is emitted: stdout gets the header line naming what was withheld, never the artifact just rejected. --json GATES AT A DIFFERENT NUMBER for the same request, and NOT by a fixed factor: the flag measures the DOCUMENT that was emitted, and whether JSON or XML is smaller flips with RESULT SIZE on this corpus. Small: JSON wins (MEASURED on src --top-k=20: est_tokens 1146 XML vs 899 JSON, ~22% smaller). Large: XML wins instead (MEASURED on src --top-k=200: est_tokens 9405 XML vs 9724 JSON, ~3% LARGER) — the crossover sits near top-k~100-150 here, so the same N can pass or fail differently under --json depending on dialect AND size — never assume one direction, measure the request you actually gate. - --for / --pack-task / --from-trace / --run-trace: SHAPES instead of gating — overrides that lens's own default payload budget and trims to fit, always exit 0. --for's header reports est_tokens="N" so its fit is checkable; --pack-task/--from-trace report their budget ledger in the header report line instead. On --for's auto bundle the ceiling is SPLIT, not handed to the signatures first: the sig side's claim caps at the default sig budget and the rest flows to the inline bodies, so a wider ceiling never serves fewer of them (see --for below). Its VERBATIM task echo is bytes no trim can shrink, so past some task length the header floor alone exceeds the ceiling: the lens drops the comment's DUPLICATE echo first (task_echo: dropped (ceiling); task= keeps the verbatim copy), then labels it over_ceiling (--recall: over_ceiling=1) — never a trim it did not actually do. +That is the map PLUS every block appended after it (///), each charged from the bytes it actually emits at the calibrated rate for what those bytes are — so --pack-top-n=3 --token-budget=600 gates on the ~67KB it would stream, not on the map alone. (test/tokenbudgetcheck.sh reports the live MAPE vs tiktoken o200k when tiktoken is installed; the estimate is calibrated, never exact — Claude's tokenizer is not public.) Within budget: exit 0, output unchanged. ASSERTS and fails, vs --max-tokens which shapes to fit — composable: set neither, either, or both (e.g. --max-tokens=16000 --token-budget=16K), but see --max-tokens above: the two Ns are measured in different units. Over budget, nothing of the artifact reaches stdout — only a small record naming withheld_est_tokens= vs budget=, the same vocabulary --recall uses, since est_tokens= is normatively about what a run PRINTED. On --recall the check likewise runs BEFORE a byte of the bundle is emitted: stdout gets the header line naming what was withheld, never the artifact just rejected. --json GATES AT A DIFFERENT NUMBER for the same request, and NOT by a fixed factor: the flag measures the DOCUMENT that was emitted, and whether JSON or XML is smaller flips with RESULT SIZE on this corpus. Small: JSON wins (MEASURED on src --top-k=20: est_tokens 1146 XML vs 899 JSON, ~22% smaller). Large: XML wins instead (MEASURED on src --top-k=200: est_tokens 9405 XML vs 9724 JSON, ~3% LARGER) — the crossover sits near top-k~100-150 here, so the same N can pass or fail differently under --json depending on dialect AND size — never assume one direction, measure the request you actually gate. - --for / --pack-task / --from-trace / --run-trace: a SOFT TARGET, always exit 0 — rows stop once it is reached, never a promise of bytes. --for charges N against its ranked rows only (never the header or a disclosure, so compact and full serve the same rows): rows are added in rank order while they fit, the rank 1..4 rows always ship, and the cut names its continuation (). The bodies, hops and tail fill from what the rows left. A target wider than the answer serves the default answer's rows. --pack-task keeps its section shares (an explicit 6000 serves the default's sections); --from-trace its section pricing. No lens drops a legend clause or the task echo for it, and none labels itself over_ceiling for it: est_tokens= may exceed N (budget_tokens= says it is the target asked). --pack-task without the flag keeps its hard 6K default (over_ceiling when it cannot fit). **Try it** @@ -170,7 +170,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`, `--sections`, `--recall`, `--handoff`, `--pr-context`, `--from-trace` @@ -207,7 +207,7 @@ $ ./build/ripwire . --help-task="write a cheerful release announcement" **Answers:** the task lens: say what you are doing, get the signatures and bodies to read first the task lens: ranked signatures + metrics framed for reuse. -The bundle enforces a ~7.5KB default payload budget (tail entries trim first; marks it: T rows handed to the trim, S printed). The r=1 (top-ranked) row carries next="--expand=FILE:NAME" — the one pasteable follow-up, the body that ends the search (defined here, not in the bundle's own header, which the token ladder does not charge). An explicit --token-budget=N overrides the default at the conservative byte rate (SHAPES, exit 0; see --token-budget above) and the header reports the delivered est_tokens. TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). The root discloses it: bundle="auto" bodies="N" (bodies="0" reason="budget" when none fit) — on EVERY auto-mode run: a ceiling the signatures alone exhaust still carries the attribute (legend and empty shell dropped there; only the attribute has reserved bytes), and --for --json, which serves no bodies by design, says so with "bundle":"sigs". Only the caller-chosen postures (--signatures-only, --detail=N) are attribute-free. ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. If the anchor's own body does not fit, the bundle serves nothing and says so, and the per-item over-budget comment names what was dropped. COMPACT ON THE CONCEPTUAL ROUTE: a query that anchors nothing (subtoken+body) gets the ranked map plus a section — the same candidate head's ONE-HOP callee signatures, the block a body carries — and NO body CDATA, disclosed as bundle="compact" bodies="0" reason="compact-route". Read the map, then --expand=SYM the one you want. A ranked function or method whose whole name is a word of the query (by name, not resolved) gets the first rows, past that head: up to 3, each listing its callees to 100 names (capped=1 next= past it); more are counted as qword_cut= with next=. --auto-bodies restores the body walk there. That shape discloses on every run too: a ceiling the signatures alone exhaust carries bundle="compact" bodies="0" reason="budget" — three distinct reasons, never collapsed (compact-route = the route chose edges, no_candidates = nothing scored, budget = the ceiling was spent). An explicit --token-budget=N is a hard ceiling, split so a wider ceiling never buys less: the signature side's claim is capped at the DEFAULT ~7.5KB sig budget and every byte beyond it flows to the enrichment — at any ceiling at or above the default's effective total the block is byte-identical to the default run's, so every body (or hop row) the default serves still fits. An explicit --pack-top-n is an explicit SIG posture and keeps the whole-ceiling sig claim. --compress composes: the served bodies (auto/anchor and --detail=N alike) go through the same comment-strip --expand uses, disclosed as compress="1" on the element (nothing to strip on the compact route). RANKING CONFIDENCE, disclosed not scored: the root always carries confidence="high|low" margin_pct="N" — derived from the SAME relevance-cliff gap statistic --adaptive cuts at (no new scorer, no behavior change; the --json dialect carries the same two keys). low means the ranking is FLAT (no material score cliff and more positive matches than the head shows) — treat the set as a starting point, not an answer; high means a material cliff inside the served head (margin_pct= is that drop as a whole percent) or every positive match already shown. COVERAGE, and the widening page: on a THIN answer only, the root also carries coverage="N" — the IDF-weighted share (whole percent) of the task's subtokens found in the top-ranked symbol's name, doc or body; an unmatched subtoken weighs as the rarest, so a "(#12147)" token honestly lowers it. THIN = coverage under 50, or a ranked head spread over fewer than 3 files: coverage= and its legend clause ride the root (present-only: a confident answer carries neither) and the r=1 row's next= names --for=TASK --limit=40 instead of --expand — the FILE-GRAIN WIDENING PAGE: …, ONE row per positive-score file (~100 B each), ranked file-first by score= = the IDF-weighted share of the task's subtokens the file's top 8 symbols cover between them (a term counts once however often it recurs, so one huge file cannot monopolise; ties by the best symbol's lens score, then path), n= its positive-score symbols, sym= its top symbols. --limit=N sets the rows, --offset=M pages (the house quintet + next= on a cut page). The page takes NO bundle-shaping flag (--json, --format=candidates, --detail, --signatures-only, --auto-bodies, --adaptive, --token-budget, --top-k …): each is refused beside --limit, never ignored. Measured on the routing-loop ladder (RocksDB, frozen 30): the follow-up that completes answers where a body cannot. / COLLAPSE TO A COUNTED STUB BY DEFAULT (L2, a disclosed cut, never a silent one): when the ranked set names an interface with implementors, or a HAS-A field edge into/out of it, the bundle used to spend the full // method contract or row list on it — the STUB instead says only how many rows the section held: (same shape for ). No stub for a section that would be EMPTY (an absent section stays absent). total= is that section's own pre-cap row count (packLego's post-dedup interface count; every matched HAS-A edge for compose, which caps nothing). next= carries the one restoring spelling that returns BOTH sections byte-identical to the un-stubbed render, in ONE call: --sections=lego,compose. +STOPS WHEN COMPLETE: the ranked head (: the relevance floor within the top 40, or --pack-top-n) is the answer and every row of it ships. A ~7.5KB payload TARGET shortens row text toward it (tail signatures and doc excerpts first; with S=T marks a shrink); rows served past it carry past_target="K" (the last K rows). Only a runaway guard (30KB, a bug signal) cuts rows: over_ceiling="1" and serving them all (S printed of the T rows handed to the trim). The r=1 (top-ranked) row carries next="--expand=FILE:NAME" — the one pasteable follow-up, the body that ends the search (defined here, not in the bundle's own header, which the token ladder does not charge). An explicit --token-budget=N is a soft row target at the conservative byte rate (exit 0; see --token-budget above) and the header reports the delivered est_tokens. TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). The root discloses it: bundle="auto" bodies="N" (bodies="0" reason="budget" when none fit) — on EVERY auto-mode run: a target the signatures alone exhaust still carries the attribute (with its legend and the empty shell), and --for --json, which serves no bodies by design, says so with "bundle":"sigs". Only the caller-chosen postures (--signatures-only, --detail=N) are attribute-free. ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. If the anchor's own body does not fit, the bundle serves nothing and says so, and the per-item over-budget comment names what was dropped. COMPACT ON THE CONCEPTUAL ROUTE: a query that anchors nothing (subtoken+body) gets the ranked map plus a section — the same candidate head's ONE-HOP callee signatures, the block a body carries — and NO body CDATA (save the owner body below), disclosed as bundle="compact" bodies="0" reason="compact-route". Read the map, then --expand=SYM the one you want. A ranked function or method whose whole name is a word of the query (by name, not resolved) gets the first rows, past that head: up to 3, each listing its callees to 100 names (capped=1 next= past it); more are counted as qword_cut= with next=. --auto-bodies restores the body walk there. The default serves every hop row. OWNER BODY: an explanatory question (how does, what does, ...) also carries ONE body after the hops, root bodies="1" reason="owner": a ranked callable whose whole name is a query word (the one calling another such owner first), else a method a ranked class owns (parser scope or span) named by a word, else no body - , by name, not resolved, not the only relevant body. Over 6000 B it is cut at a line end with next=. Under --token-budget it rides only when the rows at the target's cut leave room for it whole, else and reason="budget". MCP for serves the same body, --json none; RIPWIRE_NO_BODY_SLOT=1 turns it off (the A/B handle). That shape discloses on every run too: a target the signatures alone exhaust carries bundle="compact" bodies="0" reason="budget" — four distinct reasons, never collapsed (compact-route = the route chose edges, no_candidates = nothing scored, budget = the ceiling was spent, owner = the owner body rides). An explicit --token-budget=N is a soft target: the hop rows, bodies and file tail keep the share a hard N left them, rows fill the rest by rank; --pack-top-n sets the head the target fills. --compress composes: the served bodies (auto/anchor and --detail=N alike) go through the same comment-strip --expand uses, disclosed as compress="1" on the element (on the compact route, the owner body's). RANKING CONFIDENCE, disclosed not scored: the root always carries confidence="high|low" margin_pct="N" — derived from the SAME relevance-cliff gap statistic --adaptive cuts at (no new scorer, no behavior change; the --json dialect carries the same two keys). low means the ranking is FLAT (no material score cliff and more positive matches than the head shows) — treat the set as a starting point, not an answer; high means a material cliff inside the served head (margin_pct= is that drop as a whole percent) or every positive match already shown. COVERAGE, and the widening page: on a THIN answer only, the root also carries coverage="N" — the IDF-weighted share (whole percent) of the task's subtokens found in the top-ranked symbol's name, doc or body; an unmatched subtoken weighs as the rarest, so a "(#12147)" token honestly lowers it. THIN = coverage under 50, or a ranked head spread over fewer than 3 files: coverage= and its legend clause ride the root (present-only: a confident answer carries neither) and the r=1 row's next= names --for=TASK --limit=40 instead of --expand — the FILE-GRAIN WIDENING PAGE: …, ONE row per positive-score file (~100 B each), ranked file-first by score= = the IDF-weighted share of the task's subtokens the file's top 8 symbols cover between them (a term counts once however often it recurs, so one huge file cannot monopolise; ties by the best symbol's lens score, then path), n= its positive-score symbols, sym= its top symbols. --limit=N sets the rows, --offset=M pages (the house quintet + next= on a cut page). The page takes NO bundle-shaping flag (--json, --format=candidates, --detail, --signatures-only, --auto-bodies, --adaptive, --token-budget, --top-k …): each is refused beside --limit, never ignored. Measured on the routing-loop ladder (RocksDB, frozen 30): the follow-up that completes answers where a body cannot. / COLLAPSE TO A COUNTED STUB BY DEFAULT (L2, a disclosed cut, never a silent one): when the ranked set names an interface with implementors, or a HAS-A field edge into/out of it, the bundle used to spend the full // method contract or row list on it — the STUB instead says only how many rows the section held: (same shape for ). No stub for a section that would be EMPTY (an absent section stays absent). total= is that section's own pre-cap row count (packLego's post-dedup interface count; every matched HAS-A edge for compose, which caps nothing). next= carries the one restoring spelling that returns BOTH sections byte-identical to the un-stubbed render, in ONE call: --sections=lego,compose. **Try it** @@ -215,20 +215,20 @@ _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] ``` @@ -236,15 +236,15 @@ $ ./build/ripwire . --for="rankGraphTeleport" **Caveats (stated by the binary):** -- marks it: T rows handed to the trim, S printed). -- TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). -- ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. +- STOPS WHEN COMPLETE: the ranked head (: the relevance floor within the top 40, or --pack-top-n) is the answer and every row of it ships. +- with S=T marks a shrink); +- Only a runaway guard (30KB, a bug signal) cuts rows: over_ceiling="1" and serving them all (S printed of the T rows handed to the trim). ### `--sections=lego,compose` **Answers:** (with --for) opt back into the full / render the stub above replaces (with --for=TASK) restore the / sections the ranked bundle collapses to a counted stub by default — this IS the stub's own next= spelling, so pasting it back unchanged is enough. -A comma-separated, order-insensitive, closed set (lego and/or compose, each named at most once); every other section of the bundle is unaffected — the budget is the stubbed answer's. Under an explicit --token-budget the restored sections are charged in full, so can carry fewer rows there. +A comma-separated, order-insensitive, closed set (lego and/or compose, each named at most once); every other section of the bundle is unaffected — the budget is the stubbed answer's, under an explicit --token-budget too (a soft target charges the rows only). **Try it** @@ -252,15 +252,15 @@ _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 ) 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() ... [21 more line(s); run it to see the whole thing] ``` @@ -279,18 +279,21 @@ _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` - + + + + - +... [1 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for`, `--auto-bodies` @@ -311,17 +314,17 @@ _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() -struct FileHealth +struct FileHealth ... [20 more line(s); run it to see the whole thing] ``` @@ -344,16 +347,16 @@ _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… ... [20 more line(s); run it to see the whole thing] ``` @@ -376,15 +379,15 @@ _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() ... [21 more line(s); run it to see the whole thing] ``` @@ -407,15 +410,15 @@ _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 ) ... [21 more line(s); run it to see the whole thing] ``` @@ -438,15 +441,15 @@ _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>& 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 ) - + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) ... [22 more line(s); run it to see the whole thing] ``` @@ -468,7 +471,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -499,7 +502,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 ) @@ -528,9 +531,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 220 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 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 -━━ README.md (relevance 8.099) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 219796 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +━━ README.md (relevance 8.029) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -593,13 +596,13 @@ 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 1367678-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 1370299-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=957 -━━ commands.md (relevance 10.148) ━━ [sections: 1 of 163 selected (191 in doc), section-granular; whole doc 575700 B; lines="3810-3814"; dropped_by_budget=162] [truncated: 1865 of 7738 bytes] +━━ commands.md (relevance 10.110) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 586190 B; lines="3844-3848"; dropped_by_budget=163] [truncated: 1865 of 7739 bytes] ### `--field-affinity[=STRUCT]` **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. @@ -622,7 +625,7 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - + @@ -631,10 +634,10 @@ $ ./build/ripwire . --tree - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -693,9 +696,9 @@ _Stable (path/id) emit order — provider KV-cache hits across re-runs._ ``` $ ./build/ripwire . --order=stable --top-k=5 - + - + @@ -724,20 +727,20 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable ``` $ ./build/ripwire . --no-stable --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -757,11 +760,11 @@ _Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says ``` $ ./build/ripwire . --around=rankGraphTeleport - + - - + + @@ -780,7 +783,7 @@ $ ./build/ripwire . --around=rankGraphTeleport **Answers:** show SYM's direct callers (1-hop in-edges) who calls SYM (1-hop in-edges). -file:name disambiguates a same-named symbol across files (like --around/--lego); Scope::name picks one scope's definition — the sym= spelling edit-check prints resolves everywhere. Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=) +file:name disambiguates a same-named symbol across files (like --around/--lego); Scope::name picks one scope's definition — the sym= spelling edit-check prints resolves everywhere. Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=) A call the resolver DECLINED to bind whose candidates include SYM is a row: a call site, never an edge or a count= row (16 at most, file-first: dc_shown=, dc_next=) **Try it** @@ -797,12 +800,13 @@ $ ./build/ripwire . --callers=DoesNotExist **Caveats (stated by the binary):** - file:name disambiguates a same-named symbol across files (like --around/--lego); +- Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=) A call the resolver DECLINED to bind whose candidates include SYM is a row: a call site, never an edge or a count= row (16 at most, file-first: dc_shown=, dc_next=) ### `--callees=SYM` **Answers:** what SYM calls (1-hop out-edges). -file:name disambiguates like --callers +file:name disambiguates like --callers; each call it makes that the resolver declined to bind is a row, never an edge or a count= row **Try it** @@ -810,13 +814,19 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - - - - + + + + + + + + + + + ``` @@ -824,7 +834,8 @@ $ ./build/ripwire . --callees=rankGraphTeleport **Caveats (stated by the binary):** -- file:name disambiguates like --callers +- file:name disambiguates like --callers; +- each call it makes that the resolver declined to bind is a row, never an edge or a count= row ### `--uses=SYM` @@ -839,16 +850,16 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - - - - - - - + + + + + + - - + + + ``` @@ -873,18 +884,18 @@ _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] ``` @@ -910,19 +921,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] ``` @@ -945,12 +956,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - - + + + + + + ``` @@ -967,11 +978,11 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - - + + @@ -993,7 +1004,7 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - @@ -1077,7 +1088,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1138,7 +1149,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: 50dfd49c3 +at: 0e6edd91c (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1157,18 +1168,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 +1203,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1217,7 +1228,7 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - - + + - + - + @@ -1519,11 +1530,11 @@ _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 ) @@ -1552,16 +1563,16 @@ _Body-elided decl skeletons — recounted on this corpus. Measured as element by $ ./build/ripwire . --pack-signatures --top-k=10 - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -1585,7 +1596,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)" ); @@ -1612,11 +1623,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + - + + + - - + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1788,15 +1799,15 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - - + + + + + - - - + + + @@ -1825,18 +1836,18 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - - - - - - - - - - - + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1853,17 +1864,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1884,15 +1895,15 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - - - - - - + + + + + + + + - ``` @@ -1917,7 +1928,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1952,18 +1963,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] ``` @@ -1983,18 +1994,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] ``` @@ -2020,13 +2031,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] ``` @@ -2051,8 +2062,8 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - - + + @@ -2086,16 +2097,16 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - - - + + + + + + - - - + + + @@ -2123,19 +2134,19 @@ _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] ``` @@ -2158,11 +2169,11 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + @@ -2191,16 +2202,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - + - - - - - - + + + + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2223,16 +2234,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] ``` @@ -2249,19 +2260,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] ``` @@ -2278,19 +2289,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] ``` @@ -2316,7 +2327,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2346,7 +2357,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2372,7 +2383,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2399,19 +2410,19 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - - - + + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2508,19 +2519,19 @@ _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] ``` @@ -2622,15 +2633,15 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + - + @@ -2653,7 +2664,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2678,19 +2689,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] ``` @@ -2715,16 +2726,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2849 files · 27441 symbols · 26516 edges · 3248 modules (12310 call-graph isolated) +2963 files · 28378 symbols · 27578 edges · 3382 modules (12685 call-graph isolated) Root: `.` -Call-graph isolate provenance: 3292 declaration, 1982 header, 2365 source, 4671 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3248) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1272 symbols -- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 401 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 259 symbols +## Modules (call-graph clusters; showing 12 of 3382) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1310 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 407 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 229 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2740,14 +2751,14 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - - + + + - + + - + @@ -2772,17 +2783,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
8097"] - n102["src/infra
1280"] + n101["src
8427"] + n102["src/infra
1294"] end subgraph sg1 ["test"] - n103["test
5171"] - n392["test/legochargefix
546"] - n333["test/fixtures/recallpassage
314"] - n467["test/ownerhopfix/py/pkg
158"] - n280["test/expandmodefix
151"] - n282["test/expandsibsfix
149"] - n608["test/rubyschemafix
99"] + n103["test
5323"] + n450["test/legochargefix
546"] + n375["test/fixtures/recallpassage
314"] + n525["test/ownerhopfix/py/pkg
158"] + n322["test/expandmodefix
151"] + n324["test/expandsibsfix
149"] + n666["test/rubyschemafix
99"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2801,19 +2812,19 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - + + - + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2832,7 +2843,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2860,7 +2871,7 @@ $ ./build/ripwire . --quality-baseline (empty) ``` -**Shaped by:** `--allow-dirty`, `--quality-delta`, `--legend` +**Shaped by:** `--allow-dirty`, `--quality-delta`, `--quality-kinds`, `--legend` **Caveats (stated by the binary):** @@ -2897,7 +2908,7 @@ _The same clean verdict with what it folded printed back: every stale acknowledg ``` $ ./build/ripwire . --quality-delta --show-stale - @@ -2926,11 +2937,11 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` -**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--dmm`, `--quality-ack`, `--scope`, `--json` +**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--quality-kinds`, `--dmm`, `--quality-ack`, `--scope` **Caveats (stated by the binary):** @@ -2942,7 +2953,7 @@ $ ./build/ripwire . --quality-delta **Answers:** the same 12-kind report between two committed trees — a whole branch at once the same 12-kind report between two COMMITTED TREES instead of the working tree vs a baseline — the WAVE-level measurement (=A..B = tree B against tree A; -=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them. +=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them. UNDER MEMORY PRESSURE IT ANSWERS PART OF THE QUESTION rather than nothing: when the guard stops either commit's full tree, or projects that the target phase will not fit the limit, it ingests only both commits' CHANGED files and runs the kinds those decide; the header names the rest (kinds_skipped=, ingest_scope="changed-files", memory_need=, also_skipped=) and kinds_next= replays exactly them. Exit 2 when a row gates (the skipped kinds only add rows), else 6 — partial, never 0; exit 5 only when even the changed files do not fit or cannot stand in for the trees (a symlink, an export attribute). --quality-ack (it needs every kind's rows) skips the projection: only a real memory stop refuses it (exit 5, ledger untouched). **Try it** @@ -2951,11 +2962,11 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` -**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--dmm`, `--quality-ack`, `--scope`, `--json` +**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--quality-kinds`, `--dmm`, `--quality-ack`, `--scope` **Caveats (stated by the binary):** @@ -2963,6 +2974,29 @@ $ ./build/ripwire . --quality-delta - Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. - ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. +### `--quality-kinds=K[,K...]` + +**Answers:** (with --quality-delta=A..B) run only the named kinds — the replay a partial answer hands back (with --quality-delta=A..B) run ONLY the named kinds, comma-separated, of the twelve: api-surface, complexity, dead-code, defect-shape, duplication, error-masking, nesting, new-clone-of-reused-helper, params, placeholder, short-horizon-churn, verbosity. + +A named kind's rows are exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why="selector"), stale acks are judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet). A selection the changed files cannot answer (whole-tree kinds, with or without defect-shape) is never pre-empted by the memory projection: it runs in full, and only a real memory stop refuses it (exit 5) — so the kinds_next= replay is not degraded again. + +**Try it** + +_A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B._ + +``` +$ ./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code + +. at=: commit+dirty+shallow. available=0: no score at all (dmm=UNAVAILABLE, reason= says why); never read as 1.000 or 0.000. combine=pooled: root dmm= is summed good over summed good+bad of the 3 properties (ripwire's own). low_loc=/low_cx=/low_params=: a unit is LOW risk at or under these lines / cyclomatic / params. base_units=/base_volume=/target_units=/target_volume=: units with a body and their line span per side. reason=: why no score (no unit's size, complexity or params moved; a tree failed to parse ...). --> -

@@ -3005,7 +3039,7 @@ $ ./build/ripwire . --quality-delta --quality-ack --ack-only=zzznope (empty) ``` -**Shaped by:** `--ack-only`, `--scope`, `--legend` +**Shaped by:** `--quality-delta`, `--quality-kinds`, `--ack-only`, `--scope`, `--legend` **Caveats (stated by the binary):** @@ -3046,7 +3080,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3076,14 +3110,14 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - - - - - + + + + + + ``` @@ -3130,12 +3164,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":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"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":748,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3151,12 +3185,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":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"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":748,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3202,12 +3236,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":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"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":748,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3266,11 +3300,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":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 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":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true}}]} +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3284,7 +3318,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; 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_at= is its call sites as file:line tokens (p= stays the line where the caller is defined). +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), possible-callers (none of either, but a call the resolver declined could have meant SYM: its rows), 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_at= is its call sites as file:line tokens (p= stays the line where the caller is defined). **Try it** @@ -3300,8 +3334,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; - 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. ### `--slice=SYM[:VAR]` @@ -3316,19 +3350,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] ``` @@ -3353,17 +3387,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] @@ -3407,10 +3441,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:9117 +$ ./build/ripwire . --at=src/graph.h:9279 - - + + ``` @@ -3434,7 +3468,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3460,11 +3494,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3539,7 +3573,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3573,14 +3607,14 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - + @@ -3609,17 +3643,17 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - - + + - + - + ... [2 more line(s); run it to see the whole thing] @@ -3644,11 +3678,11 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - + + + + + ... [14 more line(s); run it to see the whole thing] ``` @@ -3674,9 +3708,9 @@ _The same page with every t= line whole: the page above previews each line over ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text - - - + + + @@ -3702,10 +3736,10 @@ _The whole hit list asked for by name: under the same 60-row cap HEAD's referenc ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - + + - + @@ -3734,7 +3768,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3815,10 +3849,10 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - - - + + + @@ -3827,7 +3861,7 @@ $ ./build/ripwire . --field-affinity - + ... [17 more line(s); run it to see the whole thing] ``` @@ -3850,19 +3884,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - - -... [18 more line(s); run it to see the whole thing] + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit` @@ -3886,19 +3918,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - - -... [18 more line(s); run it to see the whole thing] + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit` @@ -3921,19 +3951,17 @@ _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] + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--whereis`, `--doc-drift` @@ -3957,7 +3985,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3992,10 +4020,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:9117 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9158 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1751 - #3 0x102e01a44 in main src/main.cpp:4169 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9279 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9320 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 + #3 0x102e01a44 in main src/main.cpp:4173 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -4022,7 +4050,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4112,7 +4140,7 @@ _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 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 ) @@ -4149,13 +4177,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. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> - + - + 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 ) @@ -4182,11 +4210,11 @@ _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 ) @@ -4247,7 +4275,7 @@ lego:Vehicle **Answers:** check the environment: stale binary, grammars, cache health, git, index identity environment self-check: binary-vs-PATH staleness, grammar tags.scm compile, cache-dir health, git reachability, tree-sitter version, INDEX IDENTITY, and TRACKED-BINARY staleness (a committed binary whose last commit is a git-history ANCESTOR of a same-directory/same-stem source's last commit — never mtime, which a fresh clone stamps at checkout time). -"Dependent source" is a NAMING heuristic (same dir, same filename stem, e.g. tool <-> tool.cpp) — ripwire parses no build system, so a binary built from a differently-named or differently-located source is silently out of scope, neither flagged nor cleared. Single-root only. DIAGNOSTIC, not deterministic (env-dependent by design); exit 0 iff all ok, else 1. Root reports ; each child row carries the BOOLEAN ok="0|1". passed= is the root's count (it was spelled ok= until the vocabulary pass, which collided with the child bool). A FAILING row (ok="0") also carries hint=, the derived verdict (which of self=/which= is stale and the fix, which grammar(s) failed to compile, why the cache dir isn't writable, ...) — a passing row never carries hint=. The index-cache row states the INDEX-VERSION CONTRACT — cache_version=, parser_ver_lean=, parser_ver_rich=, artifact_arch= — the four numbers that decide whether a committed --index-out artifact is reusable at all, and which appear in no other output. lean=/rich= then say whether THIS binary can open the artifact this root would consume (source="auto" its per-root blob, "cache-flag" the file you named, "disabled" under --no-cache), naming WHICH guard refused it: ok | absent | not-regular | unreadable | truncated | not-a-cache | format-version | parser-version | artifact-arch | checksum | corrupt-frame. BOTH families are reported because a team that commits only the lean artifact gets no warm hit on --for/--exemplar/--metrics/--uses. This is a FORMAT verdict about an artifact, never a freshness verdict about an index: every invocation re-validates each file, so a non-ok lean= costs speed, never correctness — the run cold-parses instead. Only an artifact you NAMED with --cache= and this binary cannot read is ok="0" (a missing auto blob is the ordinary cold-start miss, not sickness). The root carries TWO shas, and they answer different questions: at= is the TREE's HEAD (+dirty) right now, built_from= is the commit THIS BINARY was compiled from (byte-identical to --version's own built_from=). They differ from the moment you commit until the next build — normal, so a mismatch is reported, never gated; a stale PATH copy shadowing a fresh build is the binary-path row's job and is decided on inode/mtime/size, not on this sha. The git-config-trust row reads the checkout's OWN core.fsmonitor as this process saw it at startup (fsmonitor="hook|builtin|off|unset"): a HOOK-form value is a command git would run on every read-only call ripwire makes, so the process appends core.fsmonitor=false to git's GIT_CONFIG_COUNT override for its own git children (neutralised="1", and a stderr line carrying git_harden=fsmonitor-hook); the boolean forms need no override (neutralised="0"). Independently of this row, every git command ripwire runs carries git's no-optional-locks and core.fsmonitor=false so no monitor of either form runs for its read-only calls. Not a verdict on your setup — ok="1" always; the neutralisation is the verdict. +"Dependent source" is a NAMING heuristic (same dir, same filename stem, e.g. tool <-> tool.cpp) — ripwire parses no build system, so a binary built from a differently-named or differently-located source is silently out of scope, neither flagged nor cleared. Single-root only. DIAGNOSTIC, not deterministic (env-dependent by design); exit 0 iff all ok, else 1. Root reports ; each child row carries the BOOLEAN ok="0|1". passed= is the root's count (it was spelled ok= until the vocabulary pass, which collided with the child bool). A FAILING row (ok="0") also carries hint=, the derived verdict (which of self=/which= is stale and the fix, which grammar(s) failed to compile, why the cache dir isn't writable, ...) — a passing row never carries hint=. The index-cache row states the INDEX-VERSION CONTRACT — cache_version=, parser_ver_lean=, parser_ver_rich=, artifact_arch= — the four numbers that decide whether a committed --index-out artifact is reusable at all, and which appear in no other output. lean=/rich= then say whether THIS binary can open the artifact this root would consume (source="auto" its per-root blob, "cache-flag" the file you named, "disabled" under --no-cache), naming WHICH guard refused it: ok | absent | not-regular | unreadable | truncated | not-a-cache | format-version | parser-version | artifact-arch | checksum | corrupt-frame. BOTH families are reported because a team that commits only the lean artifact gets no warm hit on --for/--exemplar/--metrics/--uses. This is a FORMAT verdict about an artifact, never a freshness verdict about an index: every invocation re-validates each file, so a non-ok lean= costs speed, never correctness — the run cold-parses instead. Only an artifact you NAMED with --cache= and this binary cannot read is ok="0" (a missing auto blob is the ordinary cold-start miss, not sickness). The root carries TWO shas, and they answer different questions: at= is the TREE's HEAD (+dirty) right now, built_from= is the commit THIS BINARY was compiled from (byte-identical to --version's own built_from=). They differ from the moment you commit until the next build — normal, so a mismatch is reported, never gated; a stale PATH copy shadowing a fresh build is the binary-path row's job and is decided on inode/mtime/size, not on this sha. The git-config-trust row reads the checkout's OWN core.fsmonitor as this process saw it at startup (fsmonitor="hook|builtin|off|unset"): a HOOK-form value is a command git would run on every read-only call ripwire makes, so the process appends core.fsmonitor=false to git's GIT_CONFIG_COUNT override for its own git children (neutralised="1", and a stderr line carrying git_harden=fsmonitor-hook); the boolean forms need no override (neutralised="0"). Independently of this row, every git command ripwire runs carries git's no-optional-locks and core.fsmonitor=false so no monitor of either form runs for its read-only calls. The same row counts the filter drivers the repository's own config defines (filter_drivers=); when there are any, filter_scope="local|all" (all: a git older than 2.26 cannot report scope, so your own drivers are pinned too) and filter_pinned="1" (every git child runs them with an empty command, so files are read as stored; stderr carries git_harden=filter-driver) or "refused" (a name git's -c cannot spell: git is not run at all). Not a verdict on your setup — ok="1" always; the neutralisation is the verdict. **Try it** @@ -4255,16 +4283,16 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - - - - - - - + + + + + + + + ``` @@ -4289,21 +4317,20 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - - - - - - - + + + + + + + + - -... [1 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):** @@ -4326,7 +4353,7 @@ $ ./build/ripwire . --skipped - @@ -4412,19 +4439,19 @@ _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] ``` @@ -4447,22 +4474,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~8 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - - - - + + + + -... [12 more line(s); run it to see the whole thing] + + + + + + + +... [5 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4524,7 +4551,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4552,7 +4579,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=80b7e589253a8e76 entries=855 +ripwire legend dictionary ripwire.dict/v1 dictv=f83935bb22c0b728 entries=885 : 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=) @@ -4604,18 +4631,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] ``` @@ -4638,10 +4665,10 @@ _Drop matching paths (repeatable) before ranking._ ``` $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + - - + + @@ -4669,20 +4696,20 @@ _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] ``` @@ -4703,20 +4730,20 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ ``` $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4753,20 +4780,20 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co ``` $ ./build/ripwire . --no-cache --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4784,20 +4811,20 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses ``` $ ./build/ripwire . --no-ignore --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4820,20 +4847,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 - + - - + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4849,7 +4876,7 @@ $ ./build/ripwire . --max-file-size=8K --top-k=3 **Answers:** the memory guard's limit (default 65% of this machine's memory, env RIPWIRE_MAX_MEMORY) the memory guard is on for every run and silent on normal ones: it measures this process's footprint (on Linux, in a cgroup v2 leaf with a memory.max, the leaf's unreclaimable charge when that is larger) at most once per 5 s, from 5 s into an ingest. -Past its lines the crawl (growth of limit/8) or the parse (half the limit) stops, and the default map answers from what was built, disclosed in its header (memory_stop=, memory_parsed=, memory_limit=; a parse stop keeps the first K slots it claimed of its work order — uncached files first, largest first within a tier, or path order when every grammar-bearing file was an ingest-cache hit — so a partial map repeats for a given memory_parsed=K); critical OS pressure stops them too. Every other verb (--html, --mermaid, --expand, --outline and, after a crawl stop, --in included) refuses a partial index, and so does any verb whose own internal ingest was cut; at the limit itself ripwire exits 5 with one line naming it. Nothing derived from a partial ingest is cached. The default is 65% of physical RAM (or of the cgroup's memory.max when lower); this flag, or RIPWIRE_MAX_MEMORY when the flag is absent, replaces it; below 64M is refused. +Past its lines the crawl (growth of limit/8) or the parse (half the limit) stops, and the default map answers from what was built, disclosed in its header (memory_stop=, memory_parsed=, memory_limit=; a parse stop keeps the first K slots it claimed of its work order — uncached files first, largest first within a tier, or path order when every grammar-bearing file was an ingest-cache hit — so a partial map repeats for a given memory_parsed=K); critical OS pressure stops them too. Every other verb (--html, --mermaid, --expand, --outline and, after a crawl stop, --in included) refuses a partial index, and so does any verb whose own internal ingest was cut; at the limit itself ripwire exits 5 with one line naming it. --quality-delta=A..B alone answers from both commits' changed files instead (exit 2 or 6, see its row). Nothing derived from a partial ingest is cached. The default is 65% of physical RAM (or of the cgroup's memory.max when lower); this flag, or RIPWIRE_MAX_MEMORY when the flag is absent, replaces it; below 64M is refused. **Shaped by:** `--mcp` @@ -4900,20 +4927,20 @@ _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 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4956,10 +4983,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":" | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 692 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) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **685 gate scripts**, all of which exist on disk. +naming **692 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`. @@ -7005,7 +7005,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 685. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 692. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/docs/LIMITS.md b/docs/LIMITS.md index b83ac804a..4ed964749 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 | | --- | --- | --- | --- | -| 253 | 98 | 149 | **104** | +| 259 | 102 | 151 | **108** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 253 + 7 is the 260 constants this generator parses out of `src/`. +are not counted as caps, and 259 + 7 is the 266 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. **139 of 253 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 114 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 259 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 120 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 98 files that declare a cap — the 253 caps counted above, and no parameter. +One table for each of the 102 files that declare a cap — the 259 caps counted above, and no parameter. ### `src/abicheck.h` @@ -204,6 +204,23 @@ Discloses: `capped`, `reads_capped` | `kMaxEnvNameLen` | `128` | BOUNDARY | longest plausible environment-variable name | | `kMaxSitesShown` | `8` | OUTPUT | sites per gate; a DEFAULT, raisable by --limit=N (effectiveRowCap), lifted by --detail | +### `src/declinedsites.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kDeclinedSiteCandCap` | `8` | — | cand= entries per row on --legend=full; cand_more= counts the rest | +| `kDeclinedSiteCap` | `16` | — | rows per answer by default; --limit=N raises it (dc_next= does) | + +### `src/defattrib.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kDefTableCap` | `16` | — | — | + ### `src/didyoumean.h` Discloses: **none** @@ -324,6 +341,14 @@ Discloses: `capped`, `hosts_capped` | `kMaxFlipRows` | `25` | OUTPUT | per emitted list; a DEFAULT --limit=N raises and --detail lifts | | `kMaxNearMisses` | `5` | OUTPUT | "did you mean" suggestions on an unknown gate name; --limit=N raises it | +### `src/forowner.h` + +Discloses: `capped` + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kForBodySlotMaxBytes` | `6000` | — | — | + ### `src/forpage.h` Discloses: `capped` @@ -369,6 +394,14 @@ Discloses: `importers_capped` | `kMaxTerminals` | `16` | — | >16 is the CALLER's usage error; the core CLAMPS (never ASSUMEs on hostile input) | | `kMemberSpellingsShown` | `6` | OUTPUT | — | +### `src/grepcarrier.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kGrepCarrierShown` | `3` | — | reads_at= sites and vals= entries printed per row (reads=/vals_total= count all) | + ### `src/handlershape.h` Discloses: **none** @@ -893,7 +926,8 @@ Discloses: `capped`, `calls_capped`, `entries_capped`, `files_capped`, `importer | `kForAutoBodyBudgetBytes` | `6000` | — | — | | `kForCapTailSigBytes` | `96` | — | — | | `kForCompactSurfaceBudgetBytes` | `1000` | — | — | -| `kForFileTailShownCap` | `24` | — | — | +| `kForFileTailPageRows` | `40` | — | FOLD C: page 0 of the file page (forpage.h kForPageRowsDefault) | +| `kForFileTailShownCap` | `24` | — | the rank rows (deep-tail d2): files outside the shown head | | `kForLensDefaultTopN` | `40` | — | — | | `kForPayloadBudgetBytes` | `7500` | — | — | | `kLegoClosureRowCap` | `200` | OUTPUT | — | diff --git a/docs/TUNING.md b/docs/TUNING.md index 8836074e1..2ffd53f40 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 161 | 160 | 112 | 12 | **37** | 75 | +| 167 | 166 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **161 cap -declarations** under **160 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **167 cap +declarations** under **166 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 160 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **37 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 166 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **43 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, -`kChurnMergeBombMaxFiles`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, -+31 more). 111 + 12 + 37 = 160, and `emit` refuses to render a partition that does not add up. The -`tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 161" would be wrong in both halves -at once, which is the shape of error a generated table exists to prevent. +`kChurnMergeBombMaxFiles`, `kDeclinedSiteCandCap`, `kDeclinedSiteCap`, `kDefTableCap`, +37 more). +111 + 12 + 43 = 166, and `emit` refuses to render a partition that does not add up. The `tunable` +column above reads 112 rather than 111 because one name the sweep classified is no longer declared +in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 167" would be wrong in both halves at once, +which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 161-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 167-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/docs/captures/COMMANDS_showcase_2026-10-16.md b/docs/captures/COMMANDS_showcase_2026-10-16.md new file mode 100644 index 000000000..5d28b9d2d --- /dev/null +++ b/docs/captures/COMMANDS_showcase_2026-10-16.md @@ -0,0 +1,7718 @@ +# ripwire — every verb, run for real + +- **Date:** 2026-10-16 (regenerated capture; supersedes any older `docs/captures/COMMANDS_showcase_*.md`) +- **Lives in `docs/captures/`** — a directory the crawl/retrieval lenses SKIP (`kCrawlSkipDirs`, src/ingest.h): a generated doc that quotes every verb's output out-scores the source for any query about the tool and was measured at 77% of `--recall` on this repo when it sat at the root. `test/argvdiffcheck.sh` harvests its `## `-heading command lines as differential vectors — keep that format. +- **Version:** `ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=0e6edd91c)` +- **Repo:** the ripwire repo @ `0e6edd91` — **CLEAN — `git status --porcelain` is empty**. The diff-aware verbs (`--situ`/`--test-gate`/`--quality-delta`/`--pr-context`/`--map-diff`/`--edit-check`) answer a question about the WORKING TREE, so that condition is part of their answer and every one of their captions below states which tree it recorded against. A clean tree is the honest default for a showcase, so they appear TWICE: once here on the clean tree (their empty/exit-0 shape) and once in the final section against a throwaway `git clone --local` sandbox carrying one deliberate regression, so their real gating shapes are visible without writing a byte into the read-only repo. +- **Corpus:** the ripwire repo itself (dogfood), via `./build/ripwire` +- **Sandbox diff** (the last section only): `.ripwire_notes | 2 ++ + .ripwire_quality_acks | 3 ++- + src/infra/sortutil.h | 31 ++++++++++++++++++++++++++++--- + 3 files changed, 32 insertions(+), 4 deletions(-)` — one preexisting function made deeply nested, one function's arity changed 1 -> 2, one copy-paste duplicate helper, one new 8-parameter public function. + +**How to read the blocks:** ripwire's real XML output is minified — often ONE long line. For scanability, long minified lines are displayed re-wrapped with a line break at every tag seam (`><`). Header COMMENT lines (the legends) always appear in full — they are exempt from the per-line cut; any OTHER display line over 300 bytes is cut with a `… [line truncated: N more bytes]` marker, which can hit a long root element or row. `--plan-lanes` emits JSON and is re-wrapped at object seams the same way. Long outputs are cut to their first ~30 display lines with a `… [N more display lines; full output is M bytes]` marker giving the true size. Exit codes are recorded when non-zero; wall time when >1s. + +**Not run (and why):** `ripwire ` (network clone), `--listen` / `--mcp-token` / `--allow-remote-edits` (the HTTP-server posture; `--mcp` itself IS captured in its own section as a one-shot stdio JSON-RPC exchange, and `wrap claude` shows the wiring), `--arch --baseline[-update]` (state writer against the read-only repo — `--note-add` / `--quality-baseline` / `--quality-ack` / the three edit verbs / `--edit-plan` ARE shown, inside the throwaway sandbox clone; `--index-out` / `--pin-census` write to scratch), `--eval-mined` (needs a `minedpair.jsonl` artifact from `bench/mine_traces.py`; none present in the tree), `--refetch` (git-url only), `--lsp` (the editor transport — its one-shot dialogues are gated by `test/lspcheck.sh`, not by showcase blocks), `--max-memory` (the memory guard is silent on this repo by construction; its stops are gated by `test/memguardcheck.sh`), `--force` (wrap-only modifier), `--scan-skills` bare form (would sweep `~/.claude/skills`; the explicit-DIR form is shown instead), `--help` (212 lines — read it from the binary). + + +--- + +# understand a codebase cold + +## `./build/ripwire .` + +*The default ranked symbol map — start here when landing cold in a repo.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [728 more display lines; full output is 23573 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 8048 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5 --no-inventory` + +*The same capped map without its inventory tier: --top-k=5 leaves files out, so the default map appends (program entries, and the files the ranked rows left out) after its rows; --no-inventory drops that tier, and the ranked rows are the same.* + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.* + +````` + + + + + + + + + + +````` + +## `./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** + +````` + + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=9496 > 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.63s** + +````` + + + + +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, 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 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 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::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::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] +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::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +struct IngestFileScan +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 +… [112 more display lines; full output is 17179 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: 1.69s** + +````` + + + + + +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; +… [15 more display lines; full output is 4549 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.54s** + +````` + + + + + +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& … [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 ) +### 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 +inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 +#### The convergence disclosure contract +… [149 more display lines; full output is 22737 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.41s** + +````` + + + + + +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.55s** + +````` + + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +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::uint32_t kMaxKotlinStringNestDepth = 128u +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 ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [92 more display lines; full output is 15198 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.59s** + +````` + + + + +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 runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +struct NameCorpusStats +inline bool hasIdentifierShape( std::string_view w ) noexcept +struct LensRanking +struct Bm25Params +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +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::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [128 more display lines; full output is 17938 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.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 ) + +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 runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +struct NameCorpusStats +inline bool hasIdentifierShape( std::string_view w ) noexcept +struct LensRanking +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +struct Bm25Params +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +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::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [136 more display lines; full output is 17956 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.* + +````` + + + + + + + + + +````` + +## `./build/ripwire . --exemplar="format byte sizes for humans"` + +*The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* + +````` + + + + + 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 ); +}]]> +````` + +## `./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.* + +````` + + + + +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.* + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +````` +ripwire recall — "quality delta gating exit codes" — 113 relevant of 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 + +━━ README.md (relevance 8.029) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; 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. + + + +━━ CHANGELOG.md (relevance 7.518) ━━ [sections: 1 of 105 selected (423 in doc), section-granular; whole doc 954807 B; lines="139-154"; dropped_by_budget=104] [truncated: 1597 of 3826 bytes] +### Changed — `--quality-delta=A..B` answers the kinds it can under memory pressure, and exits 6 when that answer has no gating row (#387 follow-up) + +A tripped memory guard in the range form used to give exit 5 and nothing, so on a large repository a four-file change +… [129 more lines, 11714 bytes total] +````` + +## `./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 1370299-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 + +━━ commands.md (relevance 10.110) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 586190 B; lines="3844-3848"; dropped_by_budget=163] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` + +**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. + +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)) +````` + +stderr: + +````` +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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [368 more display lines; full output is 11197 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 2.21s** + +````` +(empty) +````` + +Artifact written: + +````` + 156961 /aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 8066 bytes on 1 raw line(s)] +````` + + +--- + +# 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [160 more display lines; full output is 7694 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1909 more display lines; full output is 52379 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport` + +*Who calls SYM (1-hop in-edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --legend=full` + +*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.* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=DoesNotExist` + +*Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* + +**exit code: 1** — **wall time: 1.21s** + +````` + + +````` + +stderr: + +````` +ripwire: --callers symbol not found: DoesNotExist +````` + +## `./build/ripwire . --callees=rankGraphTeleport` + +*What SYM calls (1-hop out-edges).* + +````` + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --uses=rankGraphTeleport` + +*The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [28 more display lines; full output is 3648 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5587 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5452 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").* + +````` + + + + + + + + +````` + +## `./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex` + +*Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --impact=rankGraphTeleport` + +*Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [69 more display lines; full output is 6322 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +````` + + + + + + + + + + + + + +````` + +## `./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: 1.28s** + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: 0e6edd91c + (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: 2.84s** + +````` + + +````` + +## `./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.* + +````` + +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: 1.21s** + +````` + +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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 3220 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: 1.58s** + +````` + + + + + +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 ) +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +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.inl", # the counting … [line truncated: 76 more bytes on this line] +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 +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [156 more display lines; full output is 21122 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: 88.8% fewer bytes at top-10, 86.2% at top-50, 87.4% at top-100 (re-derived 2026-10-10 when the C++ standard-member gate stopped std::vector/std::string calls such as out.push_back() and s.empty() from binding to in-tree namesakes by name: the ranked top-10/50 lost one-line container accessors (svector::push_back, WidePath::c_str, every empty) and gained one-line forwarding helpers and macros (kindIs, ASSUME, formatTo, emitTo) whose signature is nearly the whole body, so less of the set is elidable, from 92.7/88.7/87.6; before that, 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [144 more display lines; full output is 14801 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.* + +````` + + + + +& 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).* + +````` + + + + +& 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.* + +````` + + + + +: files the ranked rows show / do not (sum = files=), each named or counted below. : a program entry by its language's convention (C/Go/Rust main, Java/C# static main, a script main its module scope calls, __main__.py), callees as c rows; manifest entries not read. : n= unshown files under dir p=; f= names its code files, the rest (tests, docs, config, test/doc/example dirs) counted. declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry. external=K: K calls proven outside the tree, no edge. locality_pinned=K: K calls pinned by locality alone (a guess). extent_suspect_syms=K: K defs failed containment, corpus-wide. macro_blanked_files=K: K files indexed from a macro-blanked re-parse. prov=scip|binding|import|split|final-segment: how that 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). 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. : a non-test row an indexed test transitively reaches (absent otherwise, never 0). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. 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. locals_floor=1: locals= is a floor. role=hub: in= is 8 or more. s in= out= cx= ccx= loc= params= nest= cbo= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, callers + co-changed files. ev=/ev_why=: essential complexity (2+: jumps block extract-method; absent: 1) / the jumps behind it, tag:count. ev_floor=1: ev= is a FLOOR; noreturn calls, macro-hidden exits and unresolved gotos are unseen. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. l=N: start line; only on a same-name overload's row, one row per body (bodyless decls fold into overloads=). --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [89 more display lines; full output is 10597 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [908 more display lines; full output is 24123 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 2.44s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5766 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 2.43s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [307 more display lines; full output is 16168 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 2.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 2.48s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5108 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [764 more display lines; full output is 111448 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 1.92s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [686 more display lines; full output is 70498 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` + +*User lint rules (YAML, ast-grep style) from a directory.* + +````` + + + + +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:399, 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.* + +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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [189 more display lines; full output is 16769 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=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [135 more display lines; full output is 7330 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [271 more display lines; full output is 12308 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +2963 files · 28378 symbols · 27578 edges · 3382 modules (12685 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 3382) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1310 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 407 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 229 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:9077 [pack,allow,insert]** — 182 symbols +- **test/ownerhopfix/py/pkg::big_handler_001@broadcast.py:3:72** — 106 symbols +- **src/infra::fail@os_win32.cpp:63:2553 [open,get,close]** — 89 symbols +- **src::relForHash@model.h:1674:137245 [clone,read,add]** — 62 symbols +- **test::no@estchargecheck.sh:51:3991 [skip]** — 57 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,call,compute]** — 52 symbols +- **src::sliceKindIs@slice.h:368:24239 [match,collect,push]** — 48 symbols +- **src::tokText@defectshape.h:128:5308 [scan,find,join]** — 41 symbols +- **test/ownerhopfix/gain/pkg::target_01@fanout.py:74:1458 [push]** — 41 symbols + +## God files (most depended-on; showing 10 of 217) +- `src/model.h` — 93 dependents +- `src/infra/emit.h` — 86 dependents +- `src/infra/Diagnostics.h` — 61 dependents +- `src/serialize.h` — 42 dependents +- `src/graph.h` — 41 dependents +… [35 more lines, 3923 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [60 more display lines; full output is 9257 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mermaid` + +*Module (directory) dependency graph as a Mermaid diagram.* + +````` +%% 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
8427"] + n102["src/infra
1294"] + end + subgraph sg1 ["test"] + n103["test
5323"] + n450["test/legochargefix
546"] + n375["test/fixtures/recallpassage
314"] + n525["test/ownerhopfix/py/pkg
158"] + n322["test/expandmodefix
151"] + n324["test/expandsibsfix
149"] + n666["test/rubyschemafix
99"] + n648["test/rubyattrsfix
85"] + n501["test/massfix
77"] + end + n73["docs
844"] + n2[".github
614"] + n0["(root)
578"] + subgraph sg5 ["bench"] + n4["bench
545"] + 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"] +… [19 more lines, 1580 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 2.05s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [930 more display lines; full output is 101731 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.* + +````` + + + + +````` + +## `./build/ripwire . --exercises=test/regression.sh` + +*Which symbols a TEST FILE exercises — the reverse direction of --affected.* + +````` + + + +````` + +## `./build/ripwire . --community=0` + +*Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* + +````` + + + +````` + +## `./build/ripwire . --quality-delta` + +*On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* + +**wall time: 14.04s** + +````` + + +````` + +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 --show-stale` + +*The same clean verdict with what it folded printed back: every stale acknowledgement as an row and the full header. The regression-free answer above keeps only stale= and stale_by_kind= (and stale_next= names this call), and folds only when that saves a quarter of the bytes. Refused without --quality-delta.* + +**wall time: 14.32s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [122 more display lines; full output is 15040 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: 1.83s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +**wall time: 1.26s** + +````` + + + +````` + +## `./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: 3.69s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.41s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 15.21s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [167 more display lines; full output is 23731 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: 2.10s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./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: 81.27s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [33517 more display lines; full output is 4640245 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: 1.96s** + +````` +(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: 63.72s** + +````` + + + + + + + + + + + + + + + +````` + +## `./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: 8.32s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 8.16s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 13355 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code` + +*A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.* + +**wall time: 10.31s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14368 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: 13.93s** + +````` + + + + + + + +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [382 more display lines; full output is 23480 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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** + +````` +(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.15s** + +````` +{"v":1,"verb":"plan-lanes","at":"0e6edd91c","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":2963,"symbols":28378,"edges":275 … [line truncated: 338 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":654,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1678,"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/editcheck.h","n":"editCheckPriceRoot","scope":"rw","key":"33219bb04158a6a8","id":"./src/editcheck.h::rw::editCheckPriceRoot","id_addressable":true,"id_collides_with":0,"l":1189,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":10,"churn":52,"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":2,"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":1,"cx":8,"ccx":8,"churn":32,"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":0,"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":3030,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":11,"churn":447,"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":356,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":19,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":690,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":447,"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":5664,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":409,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":48}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":32}, +{"p":"./src/editcheck.h","symbols":1,"churn":52,"ccx":10,"hotspot_rank":39}, +{"p":"./src/main.cpp","symbols":2,"churn":447,"ccx":12,"hotspot_rank":4}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":409,"ccx":2,"hotspot_rank":5}, +{"p":"./src/pageview.h","symbols":1,"churn":19,"ccx":2,"hotspot_rank":146}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":37}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":9,"hotspot_rank":23}]},"blast_radius":{"reaches":47,"files_total":16,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editcheck.h","./src/editplan.h","./src/editpreview.h","./src/flipimpact.h","./src/main.cpp","./src/mcp.h … [line truncated: 182 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-terra","reasoning":"high","rule":"moderate-cross-module","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":1,"max_ccx":11,"sum_ccx":61,"ambiguous_calls":6,"blast_reaches":47,"contract_touches":0,"conflicts":0,"untested":47 … [line truncated: 142 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/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3320,"ord":0,"overloads":1,"amb" … [line truncated: 42 more bytes on this line] +{"p":"./src/main.cpp","n":"kJsonRideAlongFlags","scope":"","key":"2a90806fa5a580c1","id":null,"id_addressable":false,"id_collides_with":0,"l":3242,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":447,"tested":0}, +… [67 more display lines; full output is 20350 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: 2.25s** + +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":"0e6edd91c","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28378,"edges":27578,"ambiguous":2970,"unresolved":13765},"carve":null,"core":{"files" … [line truncated: 4 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":654,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":409,"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":3320,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"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":10,"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":1,"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":1678,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":80,"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/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5664,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":409,"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":503,"ccx":0,"hotspot_rank":6}, +{"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":32}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":99}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":409,"ccx":2,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":60}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":4,"hotspot_rank":23}], +"symbols_total":3321,"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":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":6,"module_span":6,"max_ccx":38,"sum_ccx":64,"ambiguous_calls":12,"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":123,"l":279,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":14, … [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":2941,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"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":3320,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"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":3020,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":123,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"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":47,"tested":0}, +{"p":"./src/cli.h","n":"serveHelpSelector","scope":"rw","key":"4b247ee064fc4e94","id":"./src/cli.h::rw::serveHelpSelector","id_addressable":true,"id_collides_with":0,"l":3085,"ord":0,"overloads":1,"amb":1,"cx":4,"ccx":3,"churn":503,"tested":0}, +{"p":"./src/query.h","n":"tryParsePredicateOnAll","scope":"query","key":"786acc4dbfec612b","id":"./src/query.h::query::tryParsePredicateOnAll","id_addressable":false,"id_collides_with":1,"l":113,"ord":0,"overloads":2,"amb":0,"cx":1,"ccx":0,"churn":20,"tested":0}, +… [62 more display lines; full output is 18943 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.18s** + +````` + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + +… [227 more display lines; full output is 23768 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.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [253 more display lines; full output is 24966 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: 1.42s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [207 more display lines; full output is 23815 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:9279 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9320 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 + #3 0x102e01a44 in main src/main.cpp:4173 + #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 4908 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=.* + +````` + + + + +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 + +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 +struct AgentTarget + + + + + + + + + +… [59 more display lines; full output is 9114 bytes on 51 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.* + +````` + 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. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> + + + + + +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 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 ) +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +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.inl", # the counting … [line truncated: 76 more bytes on this line] +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 +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [117 more display lines; full output is 17086 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.62s** + +````` +(empty) +````` + +Artifact written: + +````` + 454590 /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=/aux/batch2.txt` + +*One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* + +**wall time: 1.67s** + +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 ) +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 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] +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) +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 std::string oracleCachePath( const std::string& root, const std::string& headSha ) +inline constexpr std::array<std::string_view, 8> kCacheRuleNames = +… [105 more display lines; full output is 48989 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: 8.16s** + +````` + + + + + + + + + + +````` + + +--- + +# 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.13s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~8 --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.84s** + +````` + + + + + + + + + + + + + + + + + + + +````` + +## `./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).* + +````` + + +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 +1184,9318,9964,171,1424,1463,1750 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 +src/mcpindex.h:1288,src/graph.h:9321,src/graph.h:10015,src/eval.h:328,src/main.cpp:1438,src/main.cpp:1477 src/main.cpp:1478 src/main.cpp:1492,src/main.cpp:1870 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +**wall time: 1.40s** + +````` + + + +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":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1184","sites_at":["src/mcpindex.h:1288"]}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:9318","sites_at":["src/graph.h:9321"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:9964","sites_at":["src/graph.h:10015"]}, +{"t":"fn","n":"runEval","p":"src/eval.h:171","sites_at":["src/eval.h:328"]}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1424","sites_at":["src/main.cpp:1438"]}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1463","sites_at":["src/main.cpp:1477","src/main.cpp:1478","src/main.cpp:1492"]}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1750","sites_at":["src/main.cpp:1870"]}]} +````` + +## `./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

--callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +**wall time: 2.20s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 7906 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 7886 bytes on 1 raw line(s)] +````` + +## `./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.27s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +**wall time: 2.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7783 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3` + +*Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* + +**wall time: 2.20s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7783 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: cache : over its 2048 MiB budget — evicted 252 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) +````` + +Artifact written: + +````` + 25291183 /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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [17 more display lines; full output is 4201 bytes on 1 raw line(s)] +````` + +## `./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: 1.50s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 14.20s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 3.3% 7.1% 10.0% + BM25 15.5% 19.7% 24.2% + BM25sub 15.4% 22.2% 31.3% + BM25body 7.4% 24.6% 34.9% + fused 7.8% 15.6% 27.1% + anchored 7.4% 24.6% 34.9% + same-dir 0.8% 6.6% 10.4% + random 0.2% 0.3% 0.7% <- floor (random ranking over F=2963 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: 136.12s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=7057 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.680 54.6% 84.6% 90.0% + subtoken doc-phrase 0.909 88.8% 92.9% 94.0% + name-exact name 0.894 81.7% 95.7% 97.1% + name-exact doc-phrase 0.014 0.6% 1.9% 2.9% + anchored name 0.686 56.1% 83.7% 88.4% + anchored doc-phrase 0.905 88.2% 92.9% 93.6% + routed name 0.896 81.9% 95.9% 97.3% + routed doc-phrase 0.908 88.8% 92.7% 93.7% + note: routing chose name-exact on 3987/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=/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: 19.80s** + +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=/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 '/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.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.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.663) + 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, 3665 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="" +# +# ...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, 4058 bytes total] +````` + +## `./build/ripwire --version` + +*Version + short build info.* + +````` +ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=0e6edd91c) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:9279` + +*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:9279` + +*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* + +````` + + + + + + + + + + +````` + +## `./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** + +````` +(empty) +````` + +stderr: + +````` +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 13440 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.* + +````` + + + + + +````` + +## `./build/ripwire . --verify="unused(rankGraphTeleport)"` + +*A claim that is FALSE — the refuted shape, with the references that refute it.* + +````` + + + + + + + + + + + + +````` + +## `./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.* + +````` + + + +````` + +## `./build/ripwire . --verify="frobnicate(x)"` + +*An unparseable claim — the refusal names the accepted shapes.* + +**exit code: 1** + +````` +(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.* + +````` + + + + + + + + + + + + + + + + + +````` + +## `./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: 1.04s** + +````` + + + + + + + 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).* + +````` + + + + + + + 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: 1.05s** + +````` + + + + + + + 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** + +````` +(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.01s** + +````` + + + + + + + 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: 1.32s** + +````` + +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: 1.26s** + +````` + +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( stats, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: a blob-stream slice was never read — comp +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( reads[ k ], BranchBlobReadBack::DisclosureWhy::NotReadBackWhole, "whereis: a blob the pa +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( Diagnostics::answerUnchanged, "lsTree lists the same rows per ref: only slower", "crossref: +DISCLOSE( out, CMakeScan::DisclosureWhy::SymlinkEscapesRoot, "flags: a CMake file's symlink target leaves the root — f +… [73 more display lines; full output is 17834 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).* + +````` + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +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::uint32_t kMaxKotlinStringNestDepth = 128u +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 ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [92 more display lines; full output is 15083 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.62s** + +````` + + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +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::uint32_t kMaxKotlinStringNestDepth = 128u +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 ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [103 more display lines; full output is 15416 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.52s** + +````` + + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +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::uint32_t kMaxKotlinStringNestDepth = 128u +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 ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [133 more display lines; full output is 18630 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: 1.41s** + +````` + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfS … [line truncated: 15 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size…< … [line truncated: 3 more bytes on this line] +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 ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +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 std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +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] +… [146 more display lines; full output is 17633 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.46s** + +````` + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfS … [line truncated: 15 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size…< … [line truncated: 3 more bytes on this line] +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 ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +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 std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +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] +… [146 more display lines; full output is 17384 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.* + +````` + + + + + + + + + +````` + +## `./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: 3.02s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 2.59s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [424 more display lines; full output is 45219 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7783 bytes on 1 raw line(s)] +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7783 bytes on 1 raw line(s)] +````` + +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 /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** + +````` + + + + + + const&, float) src/graph.h:9279 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9320 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 + #3 0x102e01a44 in main src/main.cpp:4173 + #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: 288 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.* + +````` + + + + +````` + +## `./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: 2.52s** + +````` + + + + +````` + +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: 4.73s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [187 more display lines; full output is 15657 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.* + +````` + + + + + + + + + + + +````` + +## `./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.* + +````` + + + + + + + + + + + +````` + +## `./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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 11233 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: 2.38s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 7601 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.84s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 7487 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.24s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [522 more display lines; full output is 46880 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.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 4349 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: 10.09s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [139 more display lines; full output is 14815 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: 2.21s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [643 more display lines; full output is 72557 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.* + +````` + + +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 7800 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=f83935bb22c0b728 entries=885 +: 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.candidate-page/v1 : budgeted-bundle candidate page: a resumable window over the ranked candidate set the bundle cuts, for task= under budget_tokens=, one tier per page, ranks global across pages +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, +… [856 more display lines; full output is 91215 bytes on 886 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: 1.74s** + +````` + + + + + + + + + + +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] ] +… [608 more display lines; full output is 73286 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: 1.77s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [728 more display lines; full output is 66142 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: 1.95s** + +````` + + + +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.84s** + +````` + + + +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 error; +goto error; +goto error; +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: 1.77s** + +````` +(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: 1.84s** + +````` +(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: 1.95s** + +````` +{"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}}, +… [12033 more display lines; full output is 1694823 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 6012 +````` + +## `./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: 3.90s** + +````` + + +

+

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

+

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

+

+ +````` + +## `./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: 2.72s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [827 more display lines; full output is 54886 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.76s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.81s** + +````` +(empty) +````` + +Artifact written: + +````` + 156966 /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: 4.17s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (25291183 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (52704048 bytes, rich family) +````` + +Artifact written: + +````` + 25291183 /aux/ci_index.lean.ripwirecache + 52704048 /aux/ci_index.rich.ripwirecache + 77995231 total +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7783 bytes on 1 raw line(s)] +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7783 bytes on 1 raw line(s)] +````` + +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. + 118900 /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** + +````` + + + + + + + + + + + + + +````` + +## `./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** — **wall time: 1.08s** + +````` + + + + + + + +````` + +## `./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: 7.39s** + +````` + + + + + + + + + + + + + +````` + +## `./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 `/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: 4.34s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: 0e6edd91c+dirty + [1] blast radius: 330 symbols across 73 files transitively depend on these changes (showing 8 of 73 files — shown=8 total=73 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=73) + counts_floor=1 graph_ambiguous=2970 graph_unresolved=13765 graph_unindexed=261 (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 missing … [line truncated: 45 more bytes on this line] + src/mcpverbs.h (34 dependent symbols) + src/slice.h (30 dependent symbols) + src/quality.h (16 dependent symbols) + src/verbs_quality.h (16 dependent symbols) + src/crossref.h (14 dependent symbols) + src/verbs_for.h (12 dependent symbols) + src/editcheck.h (9 dependent symbols) + src/resolve.h (9 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=759 — 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="4692": + (none — 4692 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: 3.44s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 23.71s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [131 more display lines; full output is 16920 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=full` + +*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: 11.03s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [131 more display lines; full output is 21912 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: 11.13s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":149,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"0e6edd91c+dirty","renames":60,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 239 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":"25c9f0504fd376a4","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"}, +… [128 more display lines; full output is 15464 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: 7.33s** + +````` +(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 — 1 clone row(s) reconstructed from the current tree, 215 re-derived, 18 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 56 left legacy (member set not found cloning here — a floor, not proof it is gone), 1311 ineligible (no cu … [line truncated: 200 more bytes on this line] +ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing accepted, but ./.ripwire_quality_acks was not in canonical form (ack provenance backfill / legacy rows) and has been re-serialised +````` + +## `./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: 7.40s** + +````` +(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, 216 re-derived, 18 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 56 left legacy (member set not found cloning here — a floor, not proof it is gone), 1311 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 +````` + +## `./build/ripwire . --quality-delta` + +*Re-run after the partial ack: acked=3, the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 11.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [130 more display lines; full output is 16623 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: 1.37s** + +````` + + + + + + +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey --legend=full` + +*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: 1.48s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 3.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [90 more display lines; full output is 8650 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: 1.82s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 2.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [307 more display lines; full output is 16168 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.68s** + +````` + + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1274, 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: 1.80s** + +````` + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1274, 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: 3.99s** + +````` + + +

+

+ +````` + +## `./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: 10.74s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [133 more display lines; full output is 16890 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: 6.82s** + +````` +(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: 2.96s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.03s** + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 keep this branch-free — it sits inside the PageRank sort comparator 0e6edd91cfe8421531ef2906554fceb722ae49ff lane/cr-harvest-28 +````` + +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-16 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 0e6edd91cfe8421531ef2906554fceb722ae49ff lane/cr-harvest-28 +````` + +## `./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.* + +````` + + + + + + + + + + + + + +````` + +## `./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: 3.13s** + +````` +{"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":2970,"graph_unresolved":13765,"graph_unindexed":261,"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":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"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=/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: 1.35s** + +````` +{"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=/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: 3.02s** + +````` +{"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":2970,"graph_unresolved":13765,"graph_unindexed":261,"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":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"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=/aux/payload_note.h` + +*An unknown TARGET refuses and leaves every file byte-identical.* + +**exit code: 1** — **wall time: 1.29s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found +````` + +## `./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.50s** + +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":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false," … [line truncated: 353 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: 2.85s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 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":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true}}]} +````` + +## `./build/ripwire . --edit-plan=/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: 10.58s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [127 more display lines; full output is 15987 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: 7.17s** + +````` +(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: 10.29s** + +````` +(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 27498 per-symbol rows, 28380 indexed symbols total) +````` + +Artifact written: + +````` + 4663100 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer 4f91846275151921d1985d769d4fd85dbf032735b96a2db0639ffbed3c4a9d5e +head 0e6edd91cfe8421531ef2906554fceb722ae49ff +dirty 1 +absorbed 2 +ccx 67c15011ee8b 0 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699b +````` + +## `./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: 11.07s** + +````` + + +````` + + +--- + +# 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 soft row target (the CLI --for --token-budget). The header carries route= (which ranker … [line truncated: 1061 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: 841 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: 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] +{"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: 2135 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: 1233 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: 1475 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= 47450 (~tokens at 4 bytes/token: 11862 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +whereis desc_bytes= 701 schema_bytes= 941 required=['symbol'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +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= 836 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= 453 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=[] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] +lego desc_bytes= 360 schema_bytes= 651 required=['type'] +grep desc_bytes= 445 schema_bytes= 562 required=['pattern'] +memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] +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=[] +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/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: 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: 2135 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: 1233 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= 14191 (~tokens at 4 bytes/token: 3547 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +from_trace desc_bytes= 836 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= 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.85s** + +````` +{"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= capped= (capped=1 cut). at=: commit+dirty+shallow. next= … [line truncated: 6517 more bytes on this line] +"_index":"[index: files=2963 symbols=28378 hash=2e14003a]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":""; +} // …as its own comment node, for the surfaces whose legend is written beside the rows (a / block's ) inline std::string viaNameLegendComment() { diff --git a/src/grepcarrier.h b/src/grepcarrier.h new file mode 100644 index 000000000..2d2762957 --- /dev/null +++ b/src/grepcarrier.h @@ -0,0 +1,344 @@ +#pragma once + +// grepcarrier.h — what a grep hit's CARRIER holds and who reads it (L4, train 28). +// +// WHY. A literal often sits in a variable or constant rather than a function: `DRIVER = get_environ( "TEXTUAL_DRIVER" )`, +// or a C initialiser `cmd_split_window_entry = { .name = "split-window", … .exec = cmd_split_window_exec }`. The grep +// row then names that variable with callers="0" — a call count on a thing nobody calls — and the answer stops one +// hop short of what the reader wanted: the place the value is READ (app.py's `constants.DRIVER`) or the FUNCTION the +// initialiser stores (`.exec = cmd_split_window_exec`). Both facts are already in the index; this header reads them. +// +// THE RULE (labels from the index, never from text): +// * a carrier is an enclosing definition of kind Var (a variable, constant or initialiser) — the parser's kind; +// * reads= / reads_at= are the uses verb's role="read" sites for the carrier's NAME (matched by name, the uses verb's own +// unit), counted only where that verb can see reads at all: an ingest that captured value uses and a language whose +// reads reach the use-site table (countfloor.h valueUsesArmedFor), never a C-family header, whose readers are `.c` +// files the uses verb never reads (the countFloorNext rule), and only on a page whose TOP hit sits in such a carrier +// (grepCarriersWantReads — the CLI pays a second ingest for them). Anywhere else the attributes are ABSENT: unknown, never 0; +// * vals= are the function values the carrier itself stores: value references (valuerefindex.h) made inside the +// carrier's definition whose name resolves to an indexed function, in source order, as slot=fn@line: the slot key as +// the value index normalised it, and the line of the ONE definition the name resolves to (@path:line when that +// definition is in another file) — the hop the reader wants next. A name resolving to several definitions keeps no +// @: which one the slot holds is not something the index decided. +// The CLI (verbs_grep.h) and the MCP grep twin (mcpverbs.h grepEnclosingJson) both call grepCarriers, so the two +// surfaces cannot disagree about a row. + +#include "countfloor.h" // valueUsesArmedFor — where reads reach the use-site table +#include "filter.h" // pathTierIndexOver, compareTierThenPath — the uses verb's base row order +#include "model.h" +#include "nextverb.h" // nextFlag — the root next= spelling +#include "sarif.h" // rootRelativeUri — the root-relative spelling every grep path takes +#include "search.h" // GrepEncRow — the rows this decorates +#include "valuerefindex.h" // ValueRefIndex — madeIn / targetsOf, the stored function values +#include "infra/sortutil.h" // sortutil::svLess — the byte order every string_view search here uses +#include "infra/Diagnostics.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +inline constexpr std::size_t kGrepCarrierShown = 3; // reads_at= sites and vals= entries printed per row (reads=/vals_total= count all) + +struct GrepCarrierSite +{ + std::string path; // the READS ingest's own file spelling (the CLI's may be a second, rich ingest of the same root) + std::uint32_t line = 0; +}; + +struct GrepCarrierVal +{ + std::string slot; // the normalised key without its leading '.', "" for a plain assignment + std::string fn; // the function name as written + std::string path; // the definition's file when it is not the carrier's own, else "" + std::uint32_t line = 0; // the definition's line; 0 = the name resolves to several definitions (no @) +}; + +struct GrepCarrier +{ + bool isCarrier = false; // every definition on the row is a Var + bool readsKnown = false; // reads= is a count the uses verb could make (see the rule above) + std::uint32_t reads = 0; // role="read" sites spelled like the carrier's name + std::vector at; // the first kGrepCarrierShown of them, in tier, path, line order (not the order the uses verb prints) + std::vector vals; // the first kGrepCarrierShown stored function values + std::uint32_t valsTotal = 0; // every stored function value on the row +}; + +// Does the uses verb see reads of a definition of `s`'s language at that path? (countfloor.h countFloorNext's rule.) +inline bool grepCarrierReadsVisible( const IngestResult& ing, const Symbol& s ) noexcept +{ + const std::string_view path = s.fileId < ing.files.size() ? std::string_view( ing.files[ s.fileId ] ) : std::string_view(); + return valueUsesArmedFor( s.lang ) && !path.ends_with( ".h" ); +} + +inline bool isGrepCarrierRow( const IngestResult& ing, const GrepEncRow& row ) noexcept +{ + return !row.ids.empty() && std::ranges::all_of( row.ids, [ & ]( NodeId id ) { return id < ing.symbols.size() && ing.symbols[id].kind == SymKind::Var; } ); +} + +// The row holding the TOP hit's enclosing definition — the row the root next= is decided on — or rows.size() when the +// top hit has no enclosing definition or no row holds it. +inline std::size_t grepTopHitRow( std::span hits, std::span rows ) noexcept +{ + if( hits.empty() || hits[0].enclosingId == kNoNode ) + { + return rows.size(); + } + const auto row = std::ranges::find_if( rows, [ & ]( const GrepEncRow& r ) { return std::ranges::find( r.ids, hits[0].enclosingId ) != r.ids.end(); } ); + return std::size_t( row - rows.begin() ); +} + +// Does this page count reads at all? Only when the TOP hit's row is a carrier the uses verb reads for (a language whose +// reads reach the use-site table, not a header): that row decides the root next=, so it is the one whose reads are worth +// a rich ingest (the CLI's second ingest is paid here and only here). Once counted, every readable carrier row on the +// page gets reads=; on a page that does not count, a lower carrier keeps its floor_next= (the uses verb on its name) as +// the clue. The CLI and the MCP twin both ask this, so the two surfaces count reads on the same pages. +inline bool grepCarriersWantReads( const IngestResult& ing, std::span hits, std::span rows ) noexcept +{ + const std::size_t top = grepTopHitRow( hits, rows ); + return top < rows.size() && isGrepCarrierRow( ing, rows[top] ) && grepCarrierReadsVisible( ing, ing.symbols[ rows[top].ids.front() ] ); +} + +namespace grepcarrier +{ + +// The value-reference sites that replace a read row in the uses verb (valuerefindex.h UsesValueFilter): a Value +// reference spelled like `names[k]` that resolves to a function. Built only when such a reference exists, so the +// common case (a variable name no function shares) never builds the reads ingest's value index. +inline std::vector valueSitesOf( const IngestResult& rd, std::span names ) +{ + std::vector candidates; + for( std::uint32_t i = 0; i < rd.references.size(); ++i ) + { + const Reference& r = rd.references[i]; + if( r.role == RefRole::Value && !r.fieldName.starts_with( '@' ) && std::ranges::binary_search( names, std::string_view( r.calleeName ), sortutil::svLess ) ) + { + candidates.push_back( i ); + } + } + std::vector sites; + if( candidates.empty() ) + { + return sites; + } + const ValueRefIndex vri( rd ); + for( const std::uint32_t i : candidates ) + { + if( !vri.targetsOf( i ).empty() ) + { + sites.push_back( ( std::uint64_t( rd.references[i].fileId ) << 32 ) | rd.references[i].startByte ); + } + } + std::ranges::sort( sites ); + return sites; +} + +} // namespace grepcarrier + +namespace grepcarrier +{ + +// One stored function value as vals= prints it: the slot key without its leading '.', the name as written, and the one +// definition it resolves to (none when it resolves to several). `targets` is vri.targetsOf of that reference. +inline GrepCarrierVal storedVal( const IngestResult& ing, const Reference& ref, std::span targets ) +{ + const std::string_view rel = ref.composeRel; + GrepCarrierVal val{ std::string( rel.starts_with( '.' ) ? rel.substr( 1 ) : rel ), ref.calleeName, {}, 0 }; + if( targets.size() == 1 && targets[0] < ing.symbols.size() ) + { + const Symbol& def = ing.symbols[ targets[0] ]; + val.line = def.line; + if( def.fileId != ref.fileId && def.fileId < ing.files.size() ) + { + val.path = ing.files[ def.fileId ]; + } + } + return val; +} + +// vals=: the stored function values of every carrier on the page, attributed to the row whose definition made them — +// ONE madeIn pass for the page (source order: madeIn is ascending reference order, file then byte within one ingest). +inline void fillVals( const IngestResult& ing, const ValueRefIndex& vri, std::span rows, std::span carrierIds, + std::span out ) +{ + for( const std::uint32_t v : vri.madeIn( carrierIds, RefRole::Value ) ) + { + const Reference& ref = ing.references[v]; + const std::vector targets = vri.targetsOf( v ); + if( ref.fieldName.starts_with( '@' ) || targets.empty() ) + { + continue; // a decorator row is a fact about a definition; an unresolved name is not a function value + } + const auto row = std::ranges::find_if( rows, [ & ]( const GrepEncRow& rw ) { return std::ranges::find( rw.ids, ref.fromSymbol ) != rw.ids.end(); } ); + if( row == rows.end() || !out[ std::size_t( row - rows.begin() ) ].isCarrier ) + { + continue; // madeIn was asked for carrier ids only; a row that is no carrier holds none of them + } + GrepCarrier& c = out[ std::size_t( row - rows.begin() ) ]; // a definition sits on one row (grepEnclosingRows groups by chain) + if( ++c.valsTotal <= kGrepCarrierShown ) + { + c.vals.push_back( storedVal( ing, ref, targets ) ); + } + } +} + +struct ReadHit +{ + std::uint32_t name; // index into the sorted, unique name list + std::uint32_t fileId; // into the reads ingest + std::uint32_t line; +}; + +// The uses verb's role="read" rows (collectUseSites' filters) for every name in `names` (sorted, unique), in ONE pass +// over the reads ingest, sorted by name, then tier, path, line (source before tests) — the order +// the uses verb starts from before it ranks; its printed order is rankUseSites', which reads_at= does not follow. +inline std::vector readSitesOf( const IngestResult& rd, std::span names ) +{ + const std::vector valueSites = valueSitesOf( rd, names ); + std::vector hits; + for( const Reference& r : rd.references ) + { + if( r.role != RefRole::Read || r.isCompose || r.isDocLink || r.lang == Lang::Markdown ) + { + continue; + } + const auto it = std::ranges::lower_bound( names, std::string_view( r.calleeName ), sortutil::svLess ); + if( it == names.end() || *it != r.calleeName || std::ranges::binary_search( valueSites, ( std::uint64_t( r.fileId ) << 32 ) | r.startByte ) ) + { + continue; // another name, or a site the uses verb serves as role="value", not a read + } + hits.push_back( { std::uint32_t( it - names.begin() ), r.fileId, r.line } ); + } + const std::vector tierOfFile = pathTierIndexOver( rd, hits, []( const ReadHit& h ) { return h.fileId; } ); + std::ranges::sort( hits, [ & ]( const ReadHit& a, const ReadHit& b ) + { + if( a.name != b.name ) + { + return a.name < b.name; + } + const int c = compareTierThenPath( rd, tierOfFile, a.fileId, b.fileId ); + return c != 0 ? c < 0 : a.line < b.line; + } ); + return hits; +} + +// reads=/reads_at= for every readsKnown row, from one readSitesOf pass over the reads ingest. +inline void fillReads( const IngestResult& ing, const IngestResult& rd, std::span rows, std::span out ) +{ + std::vector names; + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( out[r].readsKnown ) + { + names.push_back( ing.symbols[ rows[r].ids.front() ].name ); + } + } + std::ranges::sort( names, sortutil::svLess ); // byte order: the default string_view < aborts the Linux G1 leg (portablebuildcheck #6) + names.erase( std::unique( names.begin(), names.end() ), names.end() ); + const std::vector hits = readSitesOf( rd, names ); + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( !out[r].readsKnown ) + { + continue; + } + const std::string_view name = ing.symbols[ rows[r].ids.front() ].name; + const std::uint32_t k = std::uint32_t( std::ranges::lower_bound( names, name, sortutil::svLess ) - names.begin() ); + ASSUME( k < names.size() && names[k] == name, "every readsKnown row queued its name" ); + const auto [ first, last ] = std::ranges::equal_range( hits, k, {}, &ReadHit::name ); // hits are sorted by name first + out[r].reads = std::uint32_t( last - first ); + for( auto h = first; h != last && out[r].at.size() < kGrepCarrierShown; ++h ) + { + out[r].at.push_back( { rd.files[ h->fileId ], h->line } ); + } + } +} + +} // namespace grepcarrier + +// One carrier verdict per row. `ing` is the ingest the rows came from; `vri` its value index (vals=); `reads` the +// ingest whose role="read" references count (nullptr = none captured: reads= stays unknown on every row). Each pass +// walks its table ONCE for the whole page, never per row. +inline std::vector grepCarriers( const IngestResult& ing, const ValueRefIndex& vri, std::span rows, + const IngestResult* reads ) +{ + std::vector out( rows.size() ); + std::vector carrierIds; // every carrier definition on the page, for ONE madeIn pass + bool anyReads = false; + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( !isGrepCarrierRow( ing, rows[r] ) ) + { + continue; + } + out[r].isCarrier = true; + out[r].readsKnown = reads != nullptr && grepCarrierReadsVisible( ing, ing.symbols[ rows[r].ids.front() ] ); + anyReads = anyReads || out[r].readsKnown; + carrierIds.insert( carrierIds.end(), rows[r].ids.begin(), rows[r].ids.end() ); + } + if( !carrierIds.empty() ) + { + grepcarrier::fillVals( ing, vri, rows, carrierIds, out ); + } + if( anyReads ) + { + EXPECTS( reads != nullptr, "a row is readsKnown only when a reads ingest was handed in" ); + grepcarrier::fillReads( ing, *reads, rows, out ); + } + ENSURES( out.size() == rows.size() ); + return out; +} + +// The root next= on an uncut answer whose TOP hit sits in a carrier with reads= — the uses verb on that carrier's name +// (the spelling floor_next= uses, countfloor.h countFloorNext), because where the value is read is the hop the at verb +// on the hit's own line cannot give. "" otherwise: the at verb on the top hit stays the follow-up. The CLI grep root +// and the MCP grep twin both call it; each caller keeps its cut-answer rule (the next page) ahead of this one. +inline std::string grepCarrierNext( const IngestResult& ing, std::span hits, std::span rows, + std::span carriers ) +{ + const std::size_t top = grepTopHitRow( hits, rows ); + if( top >= rows.size() || top >= carriers.size() || !carriers[top].readsKnown || carriers[top].reads == 0 ) + { + return {}; + } + return nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ); +} + +// The two list attributes as plain text items (the emitters escape them for their own dialect): reads_at= as +// path:line, vals= as slot=fn@line / slot=fn@path:line / slot=fn — paths root-relative exactly when the hit rows' are. +struct GrepCarrierText +{ + std::vector readsAt; + std::vector vals; +}; + +inline GrepCarrierText grepCarrierText( const GrepCarrier& c, bool singleRoot, std::string_view rootPrefix ) +{ + const auto rel = [ & ]( const std::string& p ) { return std::string( singleRoot ? sarif::rootRelativeUri( p, rootPrefix ) : std::string_view( p ) ); }; + GrepCarrierText t; + if( c.readsKnown && c.reads > 0 ) + { + for( const GrepCarrierSite& site : c.at ) + { + t.readsAt.push_back( rel( site.path ) + ":" + std::to_string( site.line ) ); + } + } + for( const GrepCarrierVal& v : c.vals ) + { + std::string item = v.slot.empty() ? v.fn : v.slot + "=" + v.fn; + if( v.line != 0 ) + { + item += "@" + ( v.path.empty() ? std::string() : rel( v.path ) + ":" ) + std::to_string( v.line ); + } + t.vals.push_back( std::move( item ) ); + } + return t; +} + +} // namespace rw diff --git a/src/handoff.h b/src/handoff.h index 48b9d6ce1..98265c854 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -248,7 +248,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In }; const std::string branch = gitOneLine( root, "rev-parse --abbrev-ref HEAD" ); - const std::string subject = gitOneLine( root, "log -1 --format=%s" ); + const std::string subject = gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%s" ); const std::string at = gitstamp::atAttr( root ); auto [ changedMask, gitOk ] = gitDiffChangedMask( root, ing ); diff --git a/src/infra/os.h b/src/infra/os.h index b5077e458..dd4ad8705 100644 --- a/src/infra/os.h +++ b/src/infra/os.h @@ -503,6 +503,12 @@ struct dirwatch_event // seconds, plus once at each hard-line check between phases. // mem_footprint: bytes this process holds. macOS: TASK_VM_INFO phys_footprint (what Activity Monitor and the // jetsam killer count — compressed and swapped pages included). Linux: resident pages from /proc/self/statm. +// mem_footprint_peak: the most this process has held so far, in mem_footprint's own measure (qd-memory-degrade: the +// range form's projection reads the base phase's PEAK, which the readings between phases miss). macOS: TASK_VM_INFO +// ledger_phys_footprint_peak, read only when the kernel returned a structure long enough to hold it (rev3+); Linux: +// VmHWM from /proc/self/status (the resident high-water mark, the peak of what statm reports). 0 = unknown, and a +// caller then must not project from it. Never cgroup memory.peak: it is lifetime and leaf-wide (an earlier CI step's +// high-water mark), and it counts the page cache measuredFootprint deliberately leaves out. // mem_physical: bytes of physical RAM. macOS hw.memsize; Linux _SC_PHYS_PAGES x _SC_PAGESIZE. // mem_cgroup_max: the cgroup v2 memory.max of this process's OWN (leaf) cgroup (Linux; 0 = none, "max", or unreadable). // Known floor: a limit set on an ANCESTOR cgroup (a systemd slice's MemoryMax) and cgroup v1's memory.limit_in_bytes @@ -517,16 +523,30 @@ struct dirwatch_event // reaches 20%, warning when "some avg10" does. Integer percent only — no float parse, no locale. #if defined( __APPLE__ ) -inline std::uint64_t mem_footprint() +// the Mach reader the two footprint bodies below share. A static member of a struct, like the Linux branch's procfs: a +// helper of those bodies, not a name of this layer (the Windows branch has no Mach to declare it for). +struct machvm { - task_vm_info_data_t info {}; - mach_msg_type_number_t count = TASK_VM_INFO_COUNT; - if( ::task_info( ::mach_task_self(), TASK_VM_INFO, reinterpret_cast( &info ), &count ) != KERN_SUCCESS ) + // TASK_VM_INFO's footprint: phys_footprint, or with `peak` ledger_phys_footprint_peak — read only when the kernel filled + // a structure long enough to hold it (rev3+): an older kernel's stops before the field, and a zero-filled read is not + // a peak. 0 when unknown. + static std::uint64_t footprint( bool peak ) { - return 0; + task_vm_info_data_t info {}; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + if( ::task_info( ::mach_task_self(), TASK_VM_INFO, reinterpret_cast( &info ), &count ) != KERN_SUCCESS ) + { + return 0; + } + if( !peak ) + { + return std::uint64_t( info.phys_footprint ); + } + return count >= TASK_VM_INFO_REV3_COUNT && info.ledger_phys_footprint_peak > 0 ? std::uint64_t( info.ledger_phys_footprint_peak ) : 0; } - return std::uint64_t( info.phys_footprint ); -} +}; +inline std::uint64_t mem_footprint() { return machvm::footprint( false ); } +inline std::uint64_t mem_footprint_peak() { return machvm::footprint( true ); } inline std::uint64_t mem_physical() { std::uint64_t bytes = 0; @@ -634,6 +654,27 @@ inline std::uint64_t mem_footprint() const long pageSize = ::sysconf( _SC_PAGESIZE ); return pages > 0 && pageSize > 0 ? std::uint64_t( pages ) * std::uint64_t( pageSize ) : 0; } +inline std::uint64_t mem_footprint_peak() +{ + char buf[ 4096 ]; // /proc/self/status is ~1.4 KiB; VmHWM sits in its first third + if( procfs::read_small( "/proc/self/status", buf, sizeof( buf ) ) == 0 ) + { + return 0; + } + const std::string_view text( buf ); + const std::size_t at = text.find( "\nVmHWM:" ); + if( at == std::string_view::npos ) + { + return 0; + } + std::string_view rest = text.substr( at + 7 ); + while( !rest.empty() && ( rest.front() == ' ' || rest.front() == '\t' ) ) + { + rest.remove_prefix( 1 ); + } + const long long kib = procfs::leading_int( rest ); // "VmHWM:\t 123456 kB" + return kib > 0 ? std::uint64_t( kib ) * 1024u : 0; +} inline std::uint64_t mem_physical() { const long pages = ::sysconf( _SC_PHYS_PAGES ); @@ -687,6 +728,7 @@ inline int mem_pressure() } #else inline std::uint64_t mem_footprint() { return 0; } +inline std::uint64_t mem_footprint_peak() { return 0; } inline std::uint64_t mem_physical() { return 0; } inline std::uint64_t mem_cgroup_max() { return 0; } inline std::uint64_t mem_cgroup_unreclaimable() { return 0; } @@ -1082,10 +1124,12 @@ int dirwatch_open(); int dirwatch_add( int watchFd, int dirFd, dirwatch_event* change ); int dirwatch_poll( int watchFd, dirwatch_event* events, int eventCount, const ::timespec* timeout ); -// ── memory (see the POSIX branch): PrivateUsage from GetProcessMemoryInfo, ullTotalPhys from GlobalMemoryStatusEx, -// no cgroup (0, both cgroup helpers), and QueryMemoryResourceNotification on a low-memory notification object created -// once (3 when the system signals low memory, else 1). +// ── memory (see the POSIX branch): PrivateUsage from GetProcessMemoryInfo (its peak: PeakPagefileUsage, the same commit +// charge's high-water mark), ullTotalPhys from GlobalMemoryStatusEx, no cgroup (0, both cgroup helpers), and +// QueryMemoryResourceNotification on a low-memory notification object created once (3 when the system signals low +// memory, else 1). std::uint64_t mem_footprint(); +std::uint64_t mem_footprint_peak(); std::uint64_t mem_physical(); [[gnu::always_inline]] inline std::uint64_t mem_cgroup_max() { return 0; } [[gnu::always_inline]] inline std::uint64_t mem_cgroup_unreclaimable() { return 0; } diff --git a/src/infra/os_win32.cpp b/src/infra/os_win32.cpp index 6e1e61045..d73efb008 100644 --- a/src/infra/os_win32.cpp +++ b/src/infra/os_win32.cpp @@ -2804,8 +2804,9 @@ int setsockopt_nosigpipe( int, const void*, socklen_t ) { return 0; } // ── memory (#350's guard): each a single call, 0 when it fails ───────────────────────────────────────────── // PrivateUsage is the commit charge this process owns, the Windows analogue of phys_footprint: it counts what the -// process allocated whether or not it is resident, which is the quantity that runs a machine out of memory. -std::uint64_t mem_footprint() +// process allocated whether or not it is resident, which is the quantity that runs a machine out of memory. commitCharge +// reads it, or with `peak` PeakPagefileUsage (the peak of PagefileUsage, which equals PrivateUsage); 0 when the call fails. +static std::uint64_t commitCharge( bool peak ) { PROCESS_MEMORY_COUNTERS_EX counters {}; counters.cb = sizeof( counters ); @@ -2813,8 +2814,10 @@ std::uint64_t mem_footprint() { return 0; } - return std::uint64_t( counters.PrivateUsage ); + return std::uint64_t( peak ? counters.PeakPagefileUsage : counters.PrivateUsage ); } +std::uint64_t mem_footprint() { return commitCharge( false ); } +std::uint64_t mem_footprint_peak() { return commitCharge( true ); } std::uint64_t mem_physical() { diff --git a/src/ingest.cpp b/src/ingest.cpp index cad9f5bac..bd545d759 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -194,6 +194,7 @@ extern "C" #include "ingest_docs.h" #include "ingest_names.h" #include "ingest_binds.h" +#include "ingest_inert.h" #include "ingest_elixir.h" #include "ingest_clojure.h" #include "ingest_ruby.h" // Ruby Rails/DSL capture lane (schema columns, Jbuilder typing, factories, class-side DSL walks) - carved verbatim 2026-10-12 diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 3901d1c1c..1728b1e83 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -1033,6 +1033,54 @@ inline void rubyForEachDeclaredSymbol( TSNode call, RubyDeclared kind, std::stri } ); } +// ── Subscript = operator-method call (RefRole::Subscript; queries/{ruby,python}/tags.scm @reference.subscript) ─────────── +// An element access `recv[ args ]` runs the receiver class's subscript OPERATOR METHOD. Which method is the language's +// own spelling, kept HERE as data rather than per-language code: the read form, and the form an assignment's target runs. +// C++ `operator[]` could ride the same row and the same graph.h proof (its Rules 1/2/2b type `this`, a local and a field); +// it is not armed: its element accesses are mostly builtin arrays, pointers and std containers, whose answers this lane +// keeps byte-identical. Every other indexed language has no user-definable subscript method (C, Go, Java, Bash, Lua) +// or spells it otherwise (C# indexers `this[ ]`, Swift `subscript`, Rust `Index::index`, Kotlin `operator fun get`, +// JS/TS proxies) and records nothing for it. +struct SubscriptOperatorNames +{ + Lang lang; + std::string_view read; // `recv[ k ]` + std::string_view write; // `recv[ k ] = v` +}; +inline constexpr SubscriptOperatorNames kSubscriptOperatorMethods[] = { + { Lang::Python, "__getitem__", "__setitem__" }, + { Lang::Ruby, "[]", "[]=" }, +}; + +inline const SubscriptOperatorNames* subscriptOperatorNamesOf( Lang lang ) noexcept +{ + const auto* const it = std::ranges::find( kSubscriptOperatorMethods, lang, &SubscriptOperatorNames::lang ); + return it == std::ranges::end( kSubscriptOperatorMethods ) ? nullptr : it; +} + +// The operator method element access `access` runs: the write form when it is an assignment's whole target (`h[ k ] = v`), +// "" where no one name is right — a target inside a multiple assignment (`a[ 0 ], b = …`), a `del x[ k ]` (Python's +// `__delitem__`, not read), an annotation (`x: Dict[ str, int ]`, a type, not a value). A compound assignment +// (`h[ k ] += 1`, `h[ k ] ||= v`) runs both forms and is recorded as the read one, the setter capture's floor. +inline std::string_view subscriptOperatorName( TSNode access, const SubscriptOperatorNames& names ) noexcept +{ + const TSNode parent = ts_node_parent( access ); + if( ts_node_is_null( parent ) ) + { + return names.read; + } + const char* pt = ts_node_type( parent ); + if( kindIs( pt, "assignment" ) ) + { + return ts_node_eq( fieldChild( parent, NodeField::Left ), access ) ? names.write : names.read; + } + const bool multiTarget = kindIs( pt, "pattern_list" ) || kindIs( pt, "tuple_pattern" ) || kindIs( pt, "list_pattern" ) + || kindIs( pt, "left_assignment_list" ) || kindIs( pt, "destructured_left_assignment" ); + const bool notValue = kindIs( pt, "delete_statement" ) || kindIs( pt, "type" ) || kindIs( pt, "generic_type" ) + || ( kindIs( pt, "expression_list" ) && kindIs( ts_node_type( ts_node_parent( parent ) ), "delete_statement" ) ); + return multiTarget || notValue ? std::string_view {} : names.read; +} + // One call whose receiver is typed (RubyBareCallWalk): the call node's start (its reference's startByte) and method, // whether the receiver is an (identifier) — the reference's NamedVar — or a value, and what it builds; for a local, the // scope and name whose binding types are read once the walk has seen every binding (`local` non-empty, `type` filled by @@ -1091,6 +1139,8 @@ class RubyBareCallWalk void noteLetType( TSNode letCall, std::string_view name ); void noteImplicitSubject(); void noteTypedCall( TSNode call ); + void noteTypedSubscript( TSNode access ); + void noteTypedSend( std::uint32_t at, TSNode recv, std::string_view method ); void noteDeclaredCall( TSNode call ); void noteNamedReceiver( std::uint32_t at, std::string_view method, std::string_view name ); const RubyTypedSite* siteOf( const RawRef& r ) const; @@ -1161,6 +1211,10 @@ inline void RubyBareCallWalk::visit( const Item& item, ChildCursor& cursor ) noteTypedCall( n ); noteDeclaredCall( n ); } + else if( kindIs( t, "element_reference" ) ) + { + noteTypedSubscript( n ); + } binding_.clear(); if( const std::optional role = rubyLocalRole( t ) ) { @@ -1321,13 +1375,25 @@ inline void RubyBareCallWalk::noteImplicitSubject() // a call whose receiver is typed: a value written in place, a let RSpec runs there, or a local of the scope (read at the end) inline void RubyBareCallWalk::noteTypedCall( TSNode call ) { - const TSNode recv = fieldChild( call, NodeField::Receiver ); - const std::string_view method = fieldChildTextOfKind( call, NodeField::Method, "identifier", src_ ); + noteTypedSend( ts_node_start_byte( call ), fieldChild( call, NodeField::Receiver ), fieldChildTextOfKind( call, NodeField::Method, "identifier", src_ ) ); +} + +// an element access `recv[ k ]` sends its receiver the operator method its reference names (subscriptOperatorRef), typed +// the way a call's receiver is +inline void RubyBareCallWalk::noteTypedSubscript( TSNode access ) +{ + const SubscriptOperatorNames* names = subscriptOperatorNamesOf( Lang::Ruby ); + ASSUME( names != nullptr, "kSubscriptOperatorMethods carries a Ruby row" ); + noteTypedSend( ts_node_start_byte( access ), fieldChild( access, NodeField::Object ), subscriptOperatorName( access, *names ) ); +} + +// the typed-receiver record of one send at `at`: `method` on `recv` +inline void RubyBareCallWalk::noteTypedSend( std::uint32_t at, TSNode recv, std::string_view method ) +{ if( ts_node_is_null( recv ) || method.empty() ) { return; } - const std::uint32_t at = ts_node_start_byte( call ); const bool ident = kindIs( ts_node_type( recv ), "identifier" ); if( !ident || !scopes_.visible( pattern::nodeText( recv, src_ ) ) ) { @@ -1456,7 +1522,7 @@ inline void RubyBareCallWalk::sendTargets( std::size_t firstRef, std::size_t end inline const RubyTypedSite* RubyBareCallWalk::siteOf( const RawRef& r ) const { const bool ident = r.recv == RecvKind::NamedVar; - if( r.lang != Lang::Ruby || r.role != RefRole::Call || ( !ident && r.recv != RecvKind::FieldOfVar ) ) + if( r.lang != Lang::Ruby || ( r.role != RefRole::Call && r.role != RefRole::Subscript ) || ( !ident && r.recv != RecvKind::FieldOfVar ) ) { return nullptr; } @@ -1938,11 +2004,18 @@ inline RecvShape receiverOf( TSNode nameNode, Lang lang, std::string_view src ) // RubyClassObjects). A qualified constant's whole path; the path through the described class for `described_class` and // for `described_class::Worker`; empty for a final segment alone, and for a path with any other head that is no constant // (`self.class::Foo`). +inline std::string rubyWrittenPathOfReceiver( TSNode recv, std::string_view final, std::string_view src ); inline std::string rubyReceiverWrittenPath( TSNode nameNode, std::string_view final, std::string_view src ) { const TSNode call = calleeAccessParent( nameNode ); - const TSNode recv = ts_node_is_null( call ) || !kindIs( ts_node_type( call ), "call" ) ? TSNode {} : fieldChild( call, NodeField::Receiver ); - std::string written; + return rubyWrittenPathOfReceiver( ts_node_is_null( call ) || !kindIs( ts_node_type( call ), "call" ) ? TSNode {} : fieldChild( call, NodeField::Receiver ), + final, src ); +} + +// rubyReceiverWrittenPath's answer for the receiver node itself (an element access's `object:` reads it too) +inline std::string rubyWrittenPathOfReceiver( TSNode recv, std::string_view final, std::string_view src ) +{ + std::string written; if( ts_node_is_null( recv ) ) { return written; @@ -1961,6 +2034,43 @@ inline std::string rubyReceiverWrittenPath( TSNode nameNode, std::string_view fi return written == final ? std::string {} : written; } +// The RefRole::Subscript reference of element access `access` (whose receiver is `recv`), or nullopt where it can name no +// class: a language with no operator row, a form subscriptOperatorName names nothing for, a receiver whose class no rule +// can type — a literal (`{ a: 1 }[ :a ]`, `[ 1, 2 ][ 0 ]`), a call result in Python, another element access. The +// receiver is read as a member call's is (classifyReceiver), so graph.h's receiver rules read the same fields; a Ruby +// receiver too rich for them stays FieldOfVar, which the typing walk may still type (`Report.new[ :x ]`, +// RubyBareCallWalk::noteTypedSend). +inline std::optional subscriptOperatorRef( TSNode access, TSNode recv, std::uint32_t fileId, Lang lang, std::string_view src ) +{ + const SubscriptOperatorNames* names = subscriptOperatorNamesOf( lang ); + const std::string_view op = names == nullptr ? std::string_view {} : subscriptOperatorName( access, *names ); + if( op.empty() || ts_node_is_null( recv ) ) + { + return std::nullopt; + } + RecvShape rs = classifyReceiver( recv, lang, src, /*allowChain=*/ true ); + if( rs.kind == RecvKind::None && lang != Lang::Ruby ) + { + return std::nullopt; + } + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( access ); + r.line = ts_node_start_point( access ).row + 1; + r.lang = lang; + r.role = RefRole::Subscript; + r.name = op; + r.recv = rs.kind == RecvKind::None ? RecvKind::FieldOfVar : rs.kind; // receiverOf's "too rich" member shape + r.recvVar = std::move( rs.var ); + r.fieldName = std::move( rs.field ); + if( lang == Lang::Ruby && r.recv == RecvKind::NamedVar ) + { + r.fieldName = rubyWrittenPathOfReceiver( recv, r.recvVar, src ); // a constant's path as written (parser version 137) + } + ENSURES( r.recv != RecvKind::None, "a subscript always has a receiver: graph.h's receiver rules read it as a member call" ); + return r; +} + // Java method references are not ordinary member-access nodes. The pinned grammar's first named // child is the receiver and deliberately uses the SAME `identifier` node for a simple type and a // variable. Preserve that uncertainty for graph.h: simple and dotted type candidates are retained diff --git a/src/ingest_cache.h b/src/ingest_cache.h index d56bf7605..2dbf3e06f 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -140,7 +140,12 @@ 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 = 31; // 31: train 27 — lane lego-transitive's record format below (the lane numbered it 30; +constexpr std::uint32_t kCacheVersion = 32; // 32: train 28 — the RefRole byte a record holds gained two appended values: lane + // ruby-pair's Subscript (9) and PR #380's Inert (10; 9 on the PR's branch, which predates + // Subscript). readRef validates the byte against kRefRoleCount, so no record changes + // shape, but a branch build's blob would read its role 9 as the other role here; 32 + // sits above every live value (31 on main and every lane). + // 31: train 27 — lane lego-transitive's record format below (the lane numbered it 30; // 31 is above every live value: the lane's branch builds wrote c30 blobs at 162). // 30: lane lego-transitive fix 1 — an INHERIT RawRef appends `baseBind` (u32: the // same-file declaration its base binds to, ingest_relations.h localBaseBinding); @@ -320,13 +325,30 @@ constexpr std::uint32_t kCacheVersion = 31; // 31: train 27 — lane l // (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 = 173; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 176; // bump on any grammar/.scm/extraction change + // 176 = 2026-10-15 (train 28): strictly above every number a train-28 binary used — main 173 + // (slice 4a, rich 174), lane grep-carrier 173 (value refs through ?? || && / or and, rich 174), + // lane ruby-pair 174 (subscript references + Ruby main-object mixins, rich 175), PR #380 145 (Elixir inert + // regions, RefRole::Inert), the other lanes 171. Rich file tag 177. + // 145 = PR #380 (issue #357, @LaimaWu; branch based on 143, 144 then main's rich tag): Elixir quote + // live holes and counted inert call candidates (RefRole::Inert, appended after ruby-pair's + // Subscript at the train, so its byte is 10, not the branch's 9). // 173 = 2026-10-15 (slice 4a, #400): the Ruby mixin directives (`include` / `extend` / `prepend`) // move off the captureIncludes walk onto the shared base-relations path — // queries/ruby/tags.scm captures the directive as @reference.mixin and captureTagsFacts // emits each constant's inherit reference through emitBaseRef (captureRubyMixinBases retired). // Output byte-identical; the extraction path changed, so the number moves. 173, not 172: 172 is // 171's rich file tag. Rich file tag 174. kCacheVersion stays 31. + // 173 = 2026-10-14 (train 28 lane grep-carrier): value-reference capture walks the + // value-preserving operators (JS ?? || &&, Python or/and, JS ??= ||= &&=), so the + // Value rows a cache holds change. 173, not 172: a lean 172 would read 171's rich + // blobs, and a concurrent lane may take 172 — the train renumbers. Rich file tag 174. + // 174 = lane ruby-pair (train 28, 2026-10-14; the train renumbers): Ruby and Python element + // accesses record RefRole::Subscript references (tags.scm @reference.subscript, ingest_binds.h + // subscriptOperatorRef; a NEW role byte, which readRef validates against kRefRoleCount), the + // Ruby typing walk types their receivers, and a Ruby file's top-level `include M` / `extend M` + // records LocalBindKind::RubyMainMixin (an appended kind). 172/173 skipped: rich file tag 172 + // is 171's. Rich file tag 175. kCacheVersion stays 31. // 171 = 2026-10-13 (PR #349 lands, @tag-und-nacht): Clojure grammar, tags.scm and extraction // (ingest_clojure.h) and the qualified-call namespace in Reference::qualifier. The branch built // 156, 158 and 169 (rich file tag 170); main is 167 (rich file tag 168). 171, not 168: a lean 168 diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 028b5aefb..4ab76702e 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -151,18 +151,19 @@ bool elixirMetadataAttribute( std::string_view name ) noexcept return elixirTypedAttribute( name ) || std::find( std::begin( metadata ), std::end( metadata ), name ) != std::end( metadata ); } +bool elixirLiveHole( TSNode hole, TSNode site, std::string_view src ) noexcept; + /// Decide whether a candidate definition or call capture represents supported executable Elixir syntax. /// role and name are non-null query captures into src. Reject quoted and dynamic syntax and metadata /// references; retain executable defaults and attributes while excluding declaration heads and patterns. bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std::string_view src ) noexcept { - // Quoted syntax and module attributes (notably @spec/@type) are not runtime call sites. + // Module attributes (notably @spec/@type) are not runtime call sites. + // The shared inert-region decision runs after this capture's other syntax filters, + // so quoted definition heads and special forms never inflate declined=. for( TSNode parent = ts_node_parent( role ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { - if( elixirTarget( parent, src ) == "quote" ) - { - return false; - } + if( elixirLiveHole( parent, role, src ) ) { break; } // outer quoted declarations do not constrain the evaluated argument if( std::strcmp( ts_node_type( parent ), "unary_operator" ) == 0 && nodeFieldText( parent, NodeField::Operator, src ) == "@" ) { if( elixirMetadataAttribute( elixirAttribute( parent, src ) ) @@ -215,8 +216,9 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: } } constexpr std::string_view special[] = { "defimpl", "defstruct", "defexception", "defoverridable", "alias", "import", "require", "use", - "quote", "unquote", "unquote_splicing", "case", "cond", "for", "if", "unless", "with", "receive", "try" }; + "unquote", "unquote_splicing", "case", "cond", "for", "if", "unless", "with", "receive", "try" }; if( elixirFunctionKeyword( target ) || elixirModuleKeyword( target ) + || ( target == "quote" && !ts_node_is_null( elixirBody( role, src ) ) ) || std::find( std::begin( special ), std::end( special ), target ) != std::end( special ) ) { return false; @@ -224,6 +226,7 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: bool inDefault = false; for( TSNode parent = ts_node_parent( role ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { + if( elixirLiveHole( parent, role, src ) ) { break; } // outer quoted declarations do not constrain the evaluated argument if( std::strcmp( ts_node_type( parent ), "binary_operator" ) == 0 && nodeFieldText( parent, NodeField::Operator, src ) == "\\\\" ) { const TSNode value = fieldChild( parent, NodeField::Right ); @@ -301,15 +304,46 @@ std::uint32_t elixirVisibilityEnd( TSNode scope, std::string_view src ) noexcept return end; } -bool elixirQuoted( TSNode node, std::string_view src ) noexcept +// An unquote belongs to its nearest quote body. bind_quoted disables holes by +// default; an explicit literal unquote option takes precedence. Unknown options +// cannot prove a live hole, so retain the inert call candidate instead. +bool elixirLiveHole( TSNode hole, TSNode site, std::string_view src ) noexcept { - for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + const auto target = elixirTarget( hole, src ); + if( ( target != "unquote" && target != "unquote_splicing" ) || !elixirContains( elixirFirstArgument( hole ), site ) ) { return false; } + for( TSNode parent = ts_node_parent( hole ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { - if( elixirTarget( p, src ) == "quote" ) { return true; } + if( elixirTarget( parent, src ) != "quote" || !elixirContains( elixirBody( parent, src ), hole ) ) + { + continue; + } + const TSNode option = elixirKeywordValue( parent, "unquote:", src ); + if( !ts_node_is_null( option ) ) + { + return nodeTextOf( option, src ) == "true"; + } + return ts_node_is_null( elixirKeywordValue( parent, "bind_quoted:", src ) ); } return false; } +bool elixirInert( TSNode node, std::string_view src ) noexcept +{ + return inInertRegion( node, [ & ]( TSNode ancestor, TSNode site ) noexcept + { + const auto target = elixirTarget( ancestor, src ); + if( elixirLiveHole( ancestor, site, src ) ) + { + return InertBoundary::Live; + } + if( target == "quote" && elixirContains( elixirBody( ancestor, src ), site ) ) + { + return InertBoundary::Inert; + } + return InertBoundary::None; + } ); +} + std::string elixirFunctionName( std::string_view name, std::uint32_t arity ) { return std::string( name ) + "/" + std::to_string( arity ); @@ -344,8 +378,10 @@ struct ElixirContext } for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) { - if( elixirModuleKeyword( elixirTarget( p, src ) ) || elixirTarget( p, src ) == "defimpl" ) + if( ( elixirModuleKeyword( elixirTarget( p, src ) ) || elixirTarget( p, src ) == "defimpl" ) + && !elixirInert( p, src ) ) { + // Quoted module declarations are AST, not the lexical module of an evaluated hole. return moduleName( p, depth + 1 ); } } @@ -493,6 +529,7 @@ struct ElixirContext { for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) { + if( elixirLiveHole( p, node, src ) ) { return {}; } // patterns outside the live hole are inert syntax if( elixirNodeIs( p, "unary_operator" ) && nodeFieldText( p, NodeField::Operator, src ) == "^" ) { return {}; } if( elixirFunctionKeyword( elixirTarget( p, src ) ) ) { @@ -590,8 +627,8 @@ struct ElixirContext std::uint32_t size = 0; const char* name = ts_query_capture_name_for_id( query, capture.index, &size ); const std::string_view label( name, size ); - if( label == "elixir.context" && !elixirQuoted( capture.node, src ) ) { calls.push_back( capture.node ); } - if( label == "reference.bare" && !elixirQuoted( capture.node, src ) ) { identifiers.push_back( capture.node ); } + if( label == "elixir.context" && !elixirInert( capture.node, src ) ) { calls.push_back( capture.node ); } + if( label == "reference.bare" && !elixirInert( capture.node, src ) ) { identifiers.push_back( capture.node ); } } } std::sort( calls.begin(), calls.end(), []( TSNode a, TSNode b ) { return ts_node_start_byte( a ) < ts_node_start_byte( b ); } ); diff --git a/src/ingest_inert.h b/src/ingest_inert.h new file mode 100644 index 000000000..34103bbbc --- /dev/null +++ b/src/ingest_inert.h @@ -0,0 +1,37 @@ +#pragma once +#if !defined( RIPWIRE_INGEST_TU ) +#error "ingest_inert.h is a section of ingest.cpp; include it only there" +#endif + +namespace rw +{ +namespace +{ + +// A live hole consumes its enclosing inert boundary. Keep walking: that quote +// may itself be inert syntax inside another quote. A nested quote inside a live +// hole becomes inert again until its own hole is entered. Language syntax stays +// in the callback; every tags and context capture uses this same walk. +enum class InertBoundary : std::uint8_t { None, Inert, Live }; + +template +bool inInertRegion( TSNode node, BoundaryOf boundaryOf ) noexcept +{ + std::size_t liveHoles = 0; + for( TSNode ancestor = ts_node_parent( node ); !ts_node_is_null( ancestor ); ancestor = ts_node_parent( ancestor ) ) + { + switch( boundaryOf( ancestor, node ) ) + { + case InertBoundary::Inert: + if( liveHoles == 0 ) { return true; } + --liveHoles; + break; + case InertBoundary::Live: ++liveHoles; break; + case InertBoundary::None: break; + } + } + return false; +} + +} // namespace +} // namespace rw diff --git a/src/ingest_model.h b/src/ingest_model.h index 1d14889f6..28f0048d7 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -335,7 +335,7 @@ inline void mintModuleScopeOwners( IngestResult& result, std::vector& ra { continue; } - if( r.role != RefRole::Call && r.role != RefRole::Macro ) + if( r.role != RefRole::Call && r.role != RefRole::Macro && r.role != RefRole::Inert ) { continue; } @@ -939,7 +939,7 @@ inline void emitReferences( IngestResult& result, std::vector& rawRefs, // one that does not, so one reference's spelling would depend on an unrelated fact about its file; // and --affected's import tier and graph.h's Binding tables both already read that kNoNode. const bool refTakesModuleScope = !r.isInherit && !r.isDocLink && !r.isCompose - && ( r.role == RefRole::Call || r.role == RefRole::Macro ); + && ( r.role == RefRole::Call || r.role == RefRole::Macro || r.role == RefRole::Inert ); ref.fromSymbol = refTakesModuleScope ? refSweep.find( r.fileId, r.startByte ) : refSweep.findOwnedDef( r.fileId, r.startByte ); } diff --git a/src/ingest_relations.h b/src/ingest_relations.h index 66916c35d..2d55b3bfe 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -2016,6 +2016,7 @@ inline std::string_view rubyNamedDirective( TSNode n, std::string_view src, std: // records, in SOURCE order (the same shape as elixirAliasGroup). A non-constant argument (`include // Object.const_get(:X)`, `include mod`) contributes nothing; the constant ones beside it still do. The constant // NODES: captureIncludes reads each one's text for its Include record, and the @reference.mixin capture its byte. +inline std::vector rubyConstantArguments( TSNode n ); inline std::vector rubyMixinTargets( TSNode n, std::string_view src ) { std::vector out; @@ -2024,7 +2025,14 @@ inline std::vector rubyMixinTargets( TSNode n, std::string_view src ) { return out; } - const TSNode args = fieldChild( n, NodeField::Arguments ); + return rubyConstantArguments( n ); +} + +// The constant (or constant-path) arguments of call `n`, in source order — a mixin directive's targets +inline std::vector rubyConstantArguments( TSNode n ) +{ + std::vector out; + const TSNode args = fieldChild( n, NodeField::Arguments ); if( ts_node_is_null( args ) ) { return out; @@ -2110,7 +2118,8 @@ inline bool rubyAttrAtClassBodyLevel( TSNode n, std::string_view src ) noexcept // graph.h's inheritance graph and base walk read it unchanged. `extend` reaches the class's singleton, not its // instances; the graph keeps one method namespace per class (instance and class methods alike), so it is an ancestor // there too. So does one inside a module body's `included do … end` (rubyInConcernIncludedBlock). A directive anywhere -// else — a method body, another block, a file's top level — emits none. Since lane lego-transitive the reference is also an +// else — a method body, another block, a file's top level — emits none (a top-level one mixes into Ruby's main object: +// ingest_ruby.h captureRubyMainMixins records it for --lego's main row instead). Since lane lego-transitive the reference is also an // IMPLEMENTOR edge: graph.h's implementors builder admits a Ruby module as a base, so --lego=M lists every class (and // module) that mixes M in, and test/rubyinheritcheck.sh floor (b) is closed. // SLICE 4a moved the READ off the captureIncludes walk: queries/ruby/tags.scm captures the directive's call shape as diff --git a/src/ingest_ruby.h b/src/ingest_ruby.h index 278254d86..aef0c8a29 100644 --- a/src/ingest_ruby.h +++ b/src/ingest_ruby.h @@ -676,6 +676,42 @@ inline void captureRubyClassMixins( TSNode root, std::uint32_t fileId, std::stri } ); } +// Parser version 174 (lane ruby-pair; test/subscriptopcheck.sh): a TOP-LEVEL `include M` / `extend M` / `self.extend M`. +// Outside every class and module, self is Ruby's main object, and these make it answer M's methods — rake extends main +// with Rake::DSL so a Rakefile's bare `task` / `file` reach it. graph.h lists main as M's implementor (``). Ruby-only: main is Ruby's top-level self; no other indexed language has an object that a file's top level +// mixes a module into (Python/JS module scope is a namespace, not an instance). Read: a statement of the program itself, +// receiver-less or on `self`, and each constant argument. Not read (stated floors, test/subscriptopcheck.sh): a +// directive inside a top-level `if` / `begin` or a block, `prepend` (main has none), a non-constant argument. +inline void captureRubyMainMixins( TSNode root, std::uint32_t fileId, std::string_view src, std::vector& binds ) +{ + if( ts_node_is_null( root ) || ( src.find( "extend" ) == std::string_view::npos && src.find( "include" ) == std::string_view::npos ) ) + { + return; + } + ChildCursor cursor( root ); + forEachNamedChild( root, cursor.cur, [ & ]( TSNode n ) + { + if( !kindIs( ts_node_type( n ), "call" ) ) + { + return true; + } + const TSNode recv = fieldChild( n, NodeField::Receiver ); + const std::string_view verb = fieldIdentifierText( n, NodeField::Method, src ); + if( ( verb != "include" && verb != "extend" ) || !( ts_node_is_null( recv ) || kindIs( ts_node_type( recv ), "self" ) ) ) + { + return true; + } + for( const TSNode c : rubyConstantArguments( n ) ) + { + binds.push_back( RawBind { .fileId = fileId, .startByte = ts_node_start_byte( c ), .lang = Lang::Ruby, .kind = LocalBindKind::RubyMainMixin, + .spanStart = ts_node_start_point( c ).row + 1, .importedName = std::string( verb ), + .typeName = rubyConstantText( c, src ) } ); + } + return true; + } ); +} + // ─── FactoryBot's factory definitions (parser version 132, test/rubytypedrecvcheck.sh) ───────────────────────────────── // `factory :user do … end` inside a `FactoryBot.define` (or `FactoryGirl.define`) block defines what `create( :user )` // builds, and its class is FactoryBot's own rule: the `class:` option as written — a constant, or a string naming one (its diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 614dd81db..c16b8127d 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -2051,6 +2051,28 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t if( nameTxt == "/" && elixirNodeIs( parent, "unary_operator" ) && nodeFieldText( parent, NodeField::Operator, src ) == "&" ) { continue; } } + if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) + { + // Keep call/operator/named-capture candidates as declined facts, never edges. Heads, + // special forms and non-call operators already failed their syntax filters. Bare AST + // names remain variable-shaped syntax unless they explicitly name a function capture. + if( isRef && ( refCapSv == "reference.call" || refCapSv == "reference.operator" + || ( refCapSv == "reference.bare" && elixir.namedCapture( nameNode ) ) ) ) + { + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( roleNode ); + r.line = nameRow + 1; + r.lang = Lang::Elixir; + r.role = RefRole::Inert; + auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); + if( refCapSv == "reference.operator" ) { count = elixirNodeIs( roleNode, "binary_operator" ) ? 2 : 1; known = true; } + r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); + refs.push_back( std::move( r ) ); + } + continue; + } + if( isDef ) { if( le.lang == Lang::Elixir && defCapSv == "definition.attribute" ) { kind = SymKind::Var; } @@ -2407,6 +2429,16 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t } else if( isRef && full ) { + // An element access: @name is its RECEIVER, and the site names the receiver class's subscript operator + // method (ingest_binds.h subscriptOperatorRef) — never a call by itself (RefRole::Subscript). + if( refCapSv == "reference.subscript" ) + { + if( std::optional sub = subscriptOperatorRef( roleNode, nameNode, fileId, le.lang, src ) ) + { + refs.push_back( std::move( *sub ) ); + } + continue; + } // H4: a C++ cast keyword is not a call — see isCppCastKeyword. Valid input, skipped, no alert. if( le.lang == Lang::Cpp && isCppCastKeyword( nameTxt ) ) { @@ -2584,6 +2616,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t captureRubyHelperMethods( root, fileId, src, binds ); captureRubySingletonAccessors( root, fileId, src, binds ); captureRubyClassMixins( root, fileId, src, binds ); + captureRubyMainMixins( root, fileId, src, binds ); if( le.ext == kJbuilderExt ) { typeRubyJbuilderLocal( fileId, refs ); diff --git a/src/ingest_valuerefs.h b/src/ingest_valuerefs.h index 8670585a1..21d262a1a 100644 --- a/src/ingest_valuerefs.h +++ b/src/ingest_valuerefs.h @@ -66,6 +66,37 @@ inline constexpr std::size_t kVrTextCap = 96; // a written slot / callee // truncateUtf8WithEllipsis's (that header is not reachable from the ingest side): "…" only when text was really cut — // a text of exactly kVrTextCap bytes is whole — and never inside a UTF-8 sequence (a lead byte without its // continuation bytes would reach into=/through= as an invalid character). +// VALUE-PRESERVING OPERATORS (L4, train 28): a binary operator whose RESULT is one of its operands, so a function named +// there lands in the slot the whole expression fills — `this.getPath = options.getPath ?? getPath` stores getPath. +// One shared walk (transparent() below) reads this table, per family, as data: +// * leftLands: the LEFT operand can be the result. `f || g`, `f ?? g`, `f or g` yield f (a function is truthy and +// never nullish); `f && g`, `f and g` yield g, never f — a function on the left of && only decides, so it is no row. +// * the right operand always can. +// Families with no row here, and why: C/C++ and Go `||` / `&&` yield a truth value (int or bool), never an operand, so +// a function there is a test, not a stored value. The conditional `c ? f : g` (C, C++, JS/TS; Python `f if c else g`) +// and parentheses are handled by transparent() for every family already. Value references are captured for the C, JS, +// Python and Go families only (model.h kValueRefFamilyOfLang); Ruby, Java, C#, Swift, Rust, Bash have no +// value-reference capture at all, so they have no operator to walk either. (Ruby `a || m` needs none: a bare `m` is a +// call there and is already an edge; Ruby's function value is `method(:m)`, a different shape.) +struct VrPassOp +{ + VrFam fam; + std::string_view nodeKind; + std::string_view op; + bool leftLands; +}; +inline constexpr VrPassOp kVrPassOps[] = { + { VrFam::Js, "binary_expression", "??", true }, + { VrFam::Js, "binary_expression", "||", true }, + { VrFam::Js, "binary_expression", "&&", false }, + { VrFam::Py, "boolean_operator", "or", true }, + { VrFam::Py, "boolean_operator", "and", false }, +}; +// The JS logical ASSIGNMENTS (`x ??= f`, `x ||= f`, `x &&= f`) store the right operand into the left exactly as `=` +// may: classify() reads them as an assignment. No other family has a value-preserving compound assignment +// (C `x |= f` and Python `x |= f` are bitwise/set operators whose result is not f). +inline constexpr std::string_view kVrJsLogicalAssignOps[] = { "\?\?=", "||=", "&&=" }; // escaped: unescaped, those three characters spell a trigraph + inline std::string vrClean( std::string_view s ) { std::string out; @@ -975,9 +1006,28 @@ class ValueRefWalk } return c.field != 0 && ( c.field == m_fConsequence || c.field == m_fAlternative ); } + return c.field != 0 && passesOperator( p, c ); + } + + // A value-preserving binary operator (kVrPassOps) whose operand `c` can be the expression's result. + bool passesOperator( const VrAnc& p, const VrAnc& c ) const noexcept + { + for( const VrPassOp& po : kVrPassOps ) + { + if( po.fam == m_fam && std::string_view( p.kind ) == po.nodeKind && opText( p.node ) == po.op ) + { + return c.field == m_fRight || ( po.leftLands && c.field == m_fLeft ); + } + } return false; } + bool isLogicalAssignment( TSNode n ) const noexcept + { + const std::string_view op = opText( n ); + return m_fam == VrFam::Js && std::ranges::find( kVrJsLogicalAssignOps, op ) != std::ranges::end( kVrJsLogicalAssignOps ); + } + // The callee of the call whose argument list is anc[j] (the list itself), as written, plus its bare name. std::pair calleeOf( std::size_t j ) const { @@ -1391,7 +1441,8 @@ class ValueRefWalk s.scope = 'f'; return !nm.empty(); } - if( ( kindIs( pt, "assignment_expression" ) || kindIs( pt, "assignment" ) ) && c.field == m_fRight ) + if( ( kindIs( pt, "assignment_expression" ) || kindIs( pt, "assignment" ) + || ( kindIs( pt, "augmented_assignment_expression" ) && isLogicalAssignment( p.node ) ) ) && c.field == m_fRight ) { const TSNode lhs = child( p.node, m_fLeft ); const char* lt = ts_node_type( lhs ); diff --git a/src/main.cpp b/src/main.cpp index 42637b9ec..d177204da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,6 +43,7 @@ #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 "grepcarrier.h" // L4: a grep carrier's reads=/reads_at=/vals= (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 @@ -2039,7 +2040,7 @@ int runDefaultMap( const MainDispatch& d ) { return {}; } - serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls ); + serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls, g.inertDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); return measured.isWhole ? std::string( measured.bytes ) : std::string(); }; @@ -2052,7 +2053,7 @@ int runDefaultMap( const MainDispatch& d ) DISCLOSE( maxTokensFit, rw::MapAnnotations::MaxTokensFit::DisclosureWhy::ProbeUnmeasured, "runDefaultMap: open_memstream failed for the --max-tokens fit probe — the map is emitted unshaped and its ceiling unverified" ); return 0; } - serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls ); + serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls, g.inertDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); if( !measured.isWhole ) { @@ -2852,7 +2853,7 @@ int runDefaultMap( const MainDispatch& d ) } else { - serialize( out, ing, rank, g.outOff, g.outTargets, mapTopK, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, &mapEstTokens, payloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls ); + serialize( out, ing, rank, g.outOff, g.outTargets, mapTopK, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, &mapEstTokens, payloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls, g.inertDeclinedCalls ); } } else @@ -4074,7 +4075,10 @@ static int finishCompactCapture( const rw::Config& cfg, std::string& doc, int rc }; // the DEFAULT posture shapes what it can and leaves the rest exactly as it was emitted (cfg.legendDefaulted); // only an ASKED --legend=compact refuses an answer the dialect cannot shape - switch( rw::applyCompactDialect( doc, compactLegendHint( cfg, doc ) ) ) + // forbudget-soft: an explicit --token-budget on a task lens is a SOFT target — the settle never labels it "over" + const bool budgetIsSoft = cfg.tokenBudget > 0 + && ( !cfg.forTask.empty() || ( !cfg.packTask.empty() && cfg.partitionCount == 0 ) || !cfg.fromTrace.empty() || !cfg.runTrace.empty() ); + switch( rw::applyCompactDialect( doc, compactLegendHint( cfg, doc ), budgetIsSoft ) ) { case rw::CompactOutcome::Rewritten: case rw::CompactOutcome::AlreadyCompact: diff --git a/src/mcp.h b/src/mcp.h index 70f88fc62..dd58213ac 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1325,7 +1325,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo + mcprefuse::toolMetadataFor( "situational_awareness", pathIsRequired ) + "}," "{\"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. " + std::string( kAtSeedRebindClause ) + "\"," + mcprefuse::toolMetadataFor( "mentions", pathIsRequired ) + "}," - "{\"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 answered, and why), confidence=/margin_pct= (how sharp the ranked head is — low means a starting point, not an answer) and lens= (the per-row columns this dialect does not measure).\"," + "{\"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 soft row target (the CLI --for --token-budget). The header carries route= (which ranker answered, and why), confidence=/margin_pct= (how sharp the ranked head is — low means a starting point, not an answer) and lens= (the per-row columns this dialect does not measure).\"," + mcprefuse::toolMetadataFor( "for", pathIsRequired ) + "}," "{\"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 name, or file:name to disambiguate; " + std::string( kAtSeedShortClause ) + "\"," + mcprefuse::toolMetadataFor( "lego", pathIsRequired ) + "}," @@ -1368,9 +1368,9 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // L4 — the one-call orientation front door + B11 verb parity. `explore` is // the SAME handler as the CLI --pack-task; the older name `pack_task` still dispatches (tools/call // name=="pack_task" works) but is not separately advertised here — see mcp.h's kMcpVerbTable comment. - "{\"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 gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional (default 6000); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + "{\"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 gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional soft target (default 6000, hard); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + mcprefuse::toolMetadataFor( "explore", pathIsRequired ) + "}," - "{\"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 and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional.\"," + "{\"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 and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional (soft).\"," + mcprefuse::toolMetadataFor( "from_trace", pathIsRequired ) + "}," "{\"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 a proof: call edges are matched by NAME, so a same-named callee this tool does not index can flag a caller that never touches the edited symbol at all, and a clean tree can carry a nonzero incompatible= with nothing edited. status is one of unchanged / new-symbol / contract-change, and callers= is itself a FLOOR — 'no incompatible caller' is not proof of safety either. Fast and targeted; for the same question over a WHOLE diff use quality_delta. symbol = the def name (file:name to disambiguate); at= names the commit compared against. " + std::string( kAtSeedShortClause ) + " limit/offset page the unflagged caller rows only. PRE-APPLY PREVIEW: pass new_body to ask the same question about a replacement that has NOT been written — it is spliced in memory, re-parsed, and the answer carries preview=1. Nothing is written; a payload that does not parse, or does not define the symbol, is refused.\"," + mcprefuse::toolMetadataFor( "edit_check", pathIsRequired ) + "}," @@ -1673,7 +1673,11 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo if( legendCompactPosture ) { compacted = text; - rw::applyCompactDialect( compacted, mcpCompactLegendHint( name ) ); + // forbudget-soft: a caller's budget_tokens on a task lens is a SOFT target — never labelled "over" by the settle + const bool budgetIsSoft = budgetArg.isPresent + && ( name == "for" || name == "from_trace" + || ( ( name == "explore" || name == "pack_task" ) && partitionCount == 0 ) ); + rw::applyCompactDialect( compacted, mcpCompactLegendHint( name ), budgetIsSoft ); body = &compacted; } if( legendRefPosture ) @@ -2103,6 +2107,13 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo pathsUsageError = true; // the skip-flag: no dispatch, no getIndex() } } + // The git-config trust boundary for the root(s) this request names (githarden.h, hardenRequestRoot): + // probed HERE, once per root, after every refusal above and before the dispatch chain, so each verb + // is covered whether or not it reaches getIndex() — and every member root of a `paths` workspace. + if( !pathsUsageError && !path.empty() ) + { + hardenRequestRoot( path ); + } if( !pathsUsageError && isMcpEditVerb( name ) ) { if( std::string missingEditArg = missingArgMsg(); !missingEditArg.empty() ) diff --git a/src/mcpindex.h b/src/mcpindex.h index 6315a4f97..90c0dbf07 100644 --- a/src/mcpindex.h +++ b/src/mcpindex.h @@ -19,6 +19,7 @@ #include "serialize.h" #include "search.h" #include "gitmine.h" +#include "githarden.h" // githarden::hardenLateRoot — a request-named root's filter drivers are pinned before its first git child (hardenRequestRoot) #include "lexical.h" #include "recall.h" #include "valuerefs.h" // reference-as-value round: the per-index ValueRefIndex cache (valueRefIndexOf) @@ -956,6 +957,24 @@ inline std::string mcpWorkspaceKey( const std::vector& rootArgs, st return key; } +// The git-config trust boundary for a root a REQUEST names (githarden.h): probe it, once, before anything runs git +// there. A registered multi-root workspace KEY is not a directory, so each of its member roots is probed instead. +// Called from the MCP dispatch for every request's resolved `path` (so verbs that never reach getIndex() are +// covered), and again from getIndex() as a backstop for the LSP server; repeat calls cost one lookup. +inline void hardenRequestRoot( const std::string& root ) +{ + const auto wsIt = mcpWorkspaceRegistry().find( root ); + if( wsIt == mcpWorkspaceRegistry().end() ) + { + githarden::hardenLateRoot( root ); + return; + } + for( const WorkspaceRoot& r : wsIt->second ) + { + githarden::hardenLateRoot( r.arg ); + } +} + // ─── Phase-M: qsnap PREFETCH ────────────────────────────────────────────────────────────────────────── // // A FILE-CACHE WARMER, NOT an index mechanism — it never touches this McpIndex. When a request observes that @@ -1164,6 +1183,7 @@ inline const ValueRefIndex& valueRefIndexOf( const McpIndex& ix ) // the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no graph rebuild). inline const McpIndex& getIndex( const std::string& root ) { + hardenRequestRoot( root ); // a root (or workspace) first named in a request: pin its filter drivers before any git child runs there McpIndex& ix = mcpIndexSlot(); // hot path. The FS-event watcher (Feature 1) lets us SKIP the directory-mtime sweep when it proves no diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f3e720b51..e979d6dcb 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -20,10 +20,12 @@ #include "filter.h" // §P4: rankTierSymbolMultipliers — the fixture/present tier down-weight the CLI ranking lenses apply #include "redact.h" // RedactCounts — the per-request redaction tally threaded through the body/doc verbs #include "forpage.h" // L-W: the --for file page, coverage= and the thin rule — shared with the CLI twin +#include "forowner.h" // for-body-slot: the one owner body of an explanatory question — shared with the CLI twin #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 "grepcarrier.h" // L4: a grep enclosing row's carrier reads/vals (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) @@ -451,7 +453,7 @@ inline std::string analyzeToString( const std::string& root, int topK, bool stab // §B4 family, and mcpclidiffcheck is the gate that keeps the two surfaces one. /*ann=*/rw::MapAnnotations{ .prDisclosure = ix.prDisclosure, .codeFirstRows = ix.isCleanWorkingSet, .inventoryTier = ix.isCleanWorkingSet }, /*statsFirstScreen=*/true, anRootArg, &ix.g.locPinOut, ix.g.externalCalls, &ix.g.declinedOut, ix.g.gateDeclinedCalls, - ix.g.outNameOnly.empty() ? nullptr : &ix.g.outNameOnly, ix.g.stdMemberDeclinedCalls ); } ); + ix.g.outNameOnly.empty() ? nullptr : &ix.g.outNameOnly, ix.g.stdMemberDeclinedCalls, ix.g.inertDeclinedCalls ); } ); } // `rank_by` verb (lane/t10-mcp-coverage): the MCP twin of --rank-by=pagerank|authority|hub|rrf — the SAME @@ -519,7 +521,7 @@ inline std::string rankByText( const std::string& root, std::string_view mode, i /*extraPayloadTokens=*/0, /*ann=*/rw::MapAnnotations{ .rankByLabel = rankByLabel, .prDisclosure = disclosure, .codeFirstRows = rankByLabel == nullptr, .inventoryTier = rankByLabel == nullptr }, // pagerank: the map scope's code-first pick /*statsFirstScreen=*/true, rbRootArg, &ix.g.locPinOut, ix.g.externalCalls, &ix.g.declinedOut, ix.g.gateDeclinedCalls, - ix.g.outNameOnly.empty() ? nullptr : &ix.g.outNameOnly, ix.g.stdMemberDeclinedCalls ); } ); + ix.g.outNameOnly.empty() ? nullptr : &ix.g.outNameOnly, ix.g.stdMemberDeclinedCalls, ix.g.inertDeclinedCalls ); } ); } // ─── the cross-branch + dark-content MCP twins (`whereis`, `stray_content`, `flags`) ─── @@ -699,6 +701,28 @@ inline std::string calledByCutJson( std::string_view name, std::size_t total, co // tier-then-path order — and renders it with the same disclosures, paged through the same pageview.h // window every other paged MCP verb uses. `page` defaults to {} ⇒ the default cap, exactly as the CLI's // un-paged call behaves. Gates: test/mcpattrparitycheck.sh, test/mcpcontractcheck.sh's paging arm. +// attrib-multidef (factored out of symbolQueryJson at train 28, byte-identical): the definition table listing the groups the +// calledBy window names and, on find_symbol, the calls window too (all of them up to the cap), then its reading when it rides. +// `callsWindow` is empty (begin == end) on find_referencing_symbols, which marks the calledBy window only. +inline std::string symbolQueryDefTableJson( const IngestResult& ing, const DefGroups& groups, const RowDefTokens& calledByTokens, + const RowDefTokens& callsTokens, const PageWindow& calledByWindow, const PageWindow& callsWindow, + std::string_view rootPrefix ) +{ + std::vector named; + defTableMarkNamed( named, groups, calledByTokens, calledByWindow.begin, calledByWindow.end ); + if( callsWindow.end > callsWindow.begin ) + { + defTableMarkNamed( named, groups, callsTokens, callsWindow.begin, callsWindow.end ); + } + const DefTableListing listing = defTableListing( groups, named ); + std::string out = defTableJson( ing, groups, listing, rootPrefix, /*mcp=*/true ); + if( groups.multi() ) + { + out += nextFieldJson( defTableLegend( true, listing.cut ), "def_note" ); // its reading, only when the table rides + } + return out; +} + inline std::string symbolQueryJson( const std::string& root, const std::string& name, bool referencingOnly, McpPageArgs page = {} ) { @@ -731,7 +755,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& bool anyVia = false; // FE-B: an entry carried "via":"name" — the answer then carries via_note, its reading RowCallSites calledBySites{}; // CALLSITE-AT: the calledBy window's sites (set once the window is known, below) - const auto symObj = [ & ]( NodeId id, bool via ) -> std::string + const auto symObj = [ & ]( NodeId id, bool via, const std::string& attrib = std::string() ) -> std::string { anyVia = anyVia || via; const Symbol& s = ing.symbols[id]; @@ -746,17 +770,25 @@ 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) + + ( via ? ",\"via\":\"name\"" : "" ) // FE-B: the CLI row's via="name", the same edge bit (defattrib.h, one pass) + + attrib // attrib-multidef: the CLI row's of_def= / prov="split" + "}"; }; + // attrib-multidef (defattrib.h): calledBy rows are callers (the callers computation's attribution), calls rows callees + const OneHopAttribution& calledByAttr = referencingOnly ? chRows.attr : chCallers.attr; + const OneHopAttribution& callsAttr = chRows.attr; + bool anySplit = false; // a row carried "prov":"split" — the answer then carries split_note, its reading const auto rowArray = [ & ]( const std::vector& ids, const PageWindow& w, bool callersSide ) -> std::string { + const OneHopAttribution& at = callersSide ? calledByAttr : callsAttr; std::string a = "["; for( std::size_t i = w.begin; i < w.end; ++i ) { if( i != w.begin ) { a += ","; } + anySplit = anySplit || at.splitArm( i ); // CALLSITE-AT: a calledBy row carries its call sites (the CLI --callers row's sites_at=); a calls row never does. - a += withRowSites( symObj( ids[i], rowNameOnly( g, chRows.matches, ids[i], callersSide ) ), calledBySites, i - w.begin, callersSide ); + a += withRowSites( symObj( ids[i], at.nameOnly( i ), ofDefKeyJson( at.groups, at.tokens, i ) + splitArmKeyJson( at.splitArm( i ) ) ), + calledBySites, i - w.begin, callersSide ); } a += "]"; return a; @@ -774,6 +806,14 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& calledBySites = rowCallSites( ing, chRows.matches, std::span( calledBy ).subspan( calledByWin.begin, calledByWin.end - calledByWin.begin ), VrRender{ sqSingleRoot, sqRootPrefix } ); char pab[ kPageDisclosureCap ]; + // lane declined-sites (declinedsites.h): the CLI root's rows for the direction count= describes (find_symbol: the + // declines its definitions make; find_referencing_symbols: the declined calls that could have meant them), cut by the + // same `limit` the rows take — one computation (callhierarchy.h ordered them), the CLI's own row builder. + const DeclinedSiteCut sqDcCut = declinedSiteCut( chRows.declinedSites.size(), page.limit, referencingOnly ? "--callers=" : "--callees=", name, + std::size_t( rowCap ) ); + const std::vector sqDc = declinedSiteViews( ing, g, std::span( chRows.declinedSites ).first( sqDcCut.shown ), + referencingOnly, chRows.matches, chRows.attr.groups, VrRender{ sqSingleRoot, sqRootPrefix }, + /*candidates=*/false ); std::string out = "{"; if( sqSingleRoot ) @@ -795,6 +835,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + ",\"hop_tested\":" + std::to_string( chTested.tested ) + ",\"hop_untested\":" + std::to_string( chTested.untested ) + declinedCallsKeyJson( chRows.declinedCalls ) // the CLI root's declined_calls=, for the direction count= describes + + declinedSiteCutKeyJson( sqDcCut ) // the CLI root's dc_shown= / dc_next=, absent when nothing was cut + declinedIfaceKeyJson( chRows.declinedIface ) // the CLI root's declined_iface= (callers direction only) + stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ) // the CLI callees answer's line (callees direction only; absent when none) + nextFieldJson( nextFlag( referencingOnly ? "--uses=" : "--expand=", chNextSelector ) ); // P3 (L7): the CLI root's next= (mcpattrparitycheck) @@ -815,6 +856,10 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // which is where the disclosure has to be — the same posture bodyless_defs= already holds here. out += unprovenDefsKeyJson( chRows.unprovenDefs ); out += crossKindKeyJson( chRows.crossKind ); // hono-07: the CLI root's cross_kind=, self-named like unproven_defs above + // attrib-multidef: the CLI root's table, in this surface's row vocabulary (symbolQueryDefTableJson). The `symbol` object + // above stays the first definition — def_table[0] — and this table rides beside it. + out += symbolQueryDefTableJson( ing, chRows.attr.groups, calledByAttr.tokens, callsAttr.tokens, referencingOnly ? pwPrimary : pwSecond, + referencingOnly ? PageWindow{} : pwPrimary, sqRootPrefix ); out += pageDisclosure( pab, sizeof( pab ), pwPrimary.end - pwPrimary.begin, rowTotal, pwPrimary.end, page.limit, page.offset, discloseCap, kJsonPageSyntax ); if( !referencingOnly ) @@ -830,6 +875,9 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& { out += rw::viaNameNoteJson(); // FE-B: the reading of "via":"name", only in an answer that carries it } + out += nextFieldJson( splitArmLegend( anySplit ), "split_note" ); // attrib-multidef Q1: likewise for "prov":"split" + out += declinedSitesJson( sqDc, referencingOnly, /*mcp=*/true ); // lane declined-sites: the CLI rows, never in calls/calledBy or count + out += nextFieldJson( declinedSitesLegend( declinedSitesLegendFlags( sqDc, referencingOnly, sqDcCut ), /*mcp=*/true ), "declined_note" ); // their reading, only when they ride out += nextFieldJson( callSitesAtLegend( calledBySites.any, calledBySites.anyCut, kCallSitesPerRowCap ), "sites_note" ); // CALLSITE-AT: its reading, only when a row carries it (nextFieldJson: "" -> no key, JSON-escaped) // 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 @@ -887,11 +935,56 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // under --handles mints (search.h grepEncHandleCandidate decides WHICH row may carry one; handleFor mints it from the index's // own byte hash) — or `handle_omitted` with the CLI legend's reason. Before this a grep answer named enclosing symbols that // fetch_body could not take, so the agent re-read every file it had just searched. -inline std::string grepEnclosingJson( const McpIndex& ix, std::span hits ) +// L4: the page's rows and their carrier verdict (grepcarrier.h), computed once by the caller (its next= reads them +// too), plus the root spelling the carrier's paths take. +struct GrepEncPageJson +{ + std::span rows; + std::span carriers; // parallel to rows + bool singleRoot = false; + std::string_view rootPrefix; +}; + +// The L4 carrier keys of one enclosing row — the CLI reads=/reads_at=/vals=/vals_total= twins, from the same +// grepCarrierText. "" on a row that is no carrier or carries neither. +inline std::string grepCarrierJson( const GrepCarrier& c, bool singleRoot, std::string_view rootPrefix ) +{ + if( !c.isCarrier ) + { + return {}; + } + const GrepCarrierText t = grepCarrierText( c, singleRoot, rootPrefix ); + const auto jsonList = []( const std::vector& items ) + { + std::string list = "["; + for( std::size_t k = 0; k < items.size(); ++k ) + { + list += ( k > 0 ? ",\"" : "\"" ) + mcpdetail::jsonEscape( items[k] ) + "\""; + } + return list + "]"; + }; + std::string out; + if( !t.readsAt.empty() ) + { + out += ",\"reads\":" + std::to_string( c.reads ) + ",\"reads_at\":" + jsonList( t.readsAt ); + } + if( !t.vals.empty() ) + { + out += ",\"vals\":" + jsonList( t.vals ); + if( c.valsTotal > c.vals.size() ) + { + out += ",\"vals_total\":" + std::to_string( c.valsTotal ); + } + } + return out; +} + +inline std::string grepEnclosingJson( const McpIndex& ix, const GrepEncPageJson& page ) { - const IngestResult& ing = ix.ing; - const Graph& g = ix.g; - const std::vector encRows = grepEnclosingRows( ing, g, hits ); + const IngestResult& ing = ix.ing; + const Graph& g = ix.g; + const std::span encRows = page.rows; + const std::span carriers = page.carriers; if( encRows.empty() ) { return {}; @@ -905,6 +998,7 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span floors = callerFloors( ing, g, rowIds, &valueRefIndexOf( ix ) ); ENSURES( floors.size() == encRows.size() ); + EXPECTS( carriers.size() == encRows.size(), "the caller computed one carrier verdict per row (grepCarriers)" ); std::string out = ",\"enclosing\":["; for( std::size_t r = 0; r < encRows.size(); ++r ) { @@ -926,6 +1020,7 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span hits = grepEnrich( ing, std::span( collected.raw ).subspan( grepPage.begin, rowCount ), 0, 0 ); + // R1 rows and L4's carrier verdict, once, before the root: next= reads them (grepCarrierNext) and so does the + // enclosing block. The MCP index is the RICH family (mcpindex.h ingests with value uses), so its own references + // carry the reads — no second ingest here, unlike the lean CLI run. Reads are counted on the SAME pages the CLI counts + // them (grepCarriersWantReads: the top hit's carrier), so the two surfaces print the same reads on every page, single + // root or workspace, although this index could count them on every page. + const std::vector encRowsJ = grepEnclosingRows( ing, ix.g, std::span( hits ) ); + const std::vector carriersJ = grepCarriers( ing, valueRefIndexOf( ix ), encRowsJ, + grepCarriersWantReads( ing, hits, encRowsJ ) ? &ing : nullptr ); + const std::string carrierNextJ = grepCarrierNext( ing, hits, encRowsJ, carriersJ ); // §R-J: the CLI's unindexed_files_scanned=/unindexed block twin (search.h grepCollectAux) — literal-only, // matching this verb. No --max-file-size equivalent on the MCP surface, so this uses the same @@ -1219,6 +1323,8 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat // P3 (L7): the CLI grep root's next= — the same three-way rule (verbs_grep.h), mcpclidiffcheck LENS2 + nextFieldJson( ( grepPage.end < collected.raw.size() || collected.isBudgetReached ) ? nextFlag( "--grep=", pattern ) + " --offset=" + std::to_string( grepPage.end ) + " --legend=compact" + : !carrierNextJ.empty() + ? carrierNextJ : !collected.raw.empty() && grepPage.begin < collected.raw.size() ? nextFlag( "--at=", std::string( pathForJ( collected.raw[ grepPage.begin ].fileId ) ) + ":" + std::to_string( collected.raw[ grepPage.begin ].line ) ) : nextFlag( "--for=", pattern ) ) @@ -1278,7 +1384,7 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat singleRootJ, rootPrefixJ ); // R1 (the 2026-08-12 usage mine): the CLI / twins, appended AFTER "hits" so the // historic key order three other gates read (files,total,shown,capped) is byte-untouched. - out += grepEnclosingJson( ix, std::span( hits ) ); + out += grepEnclosingJson( ix, GrepEncPageJson{ encRowsJ, carriersJ, singleRootJ, rootPrefixJ } ); if( collected.raw.empty() ) { out += grepSuggestJson( ing, pattern ); @@ -1857,7 +1963,10 @@ inline void spliceForLegoCountLegend( std::string& doc, bool carries ) } } -inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) +// forbudget-soft: budget_tokens= is a SOFT target here too (the CLI twin's rule) — est_tokens may exceed it and nothing is +// labelled "over" for it. The one over_ceiling this surface can carry is the runaway guard's (`guardOver`: the default head +// passed kForRunawayGuardBytes); its reading (forRunawayGuardNote) was spliced with the clauses by the assembly. +inline void priceForTaskRoot( std::string& doc, bool guardOver ) { if( doc.empty() ) { @@ -1865,14 +1974,9 @@ inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) } std::size_t estTokens = 0; std::string rootAttrs = rw::pricedRootAttr( doc.size(), rw::kBytesPerTokenDefault, 0, &estTokens ); - if( budgetTokens > 0 && estTokens > budgetTokens ) + if( guardOver ) { - const std::size_t legendEnd = doc.find( " -->" ); - if( legendEnd != std::string::npos ) - { - doc.insert( legendEnd, rw::kOverCeilingLegend ); - rootAttrs = rw::pricedRootAttr( doc.size(), rw::kBytesPerTokenDefault, 0, &estTokens ); - } + rootAttrs = rw::pricedRootAttr( doc.size() + sizeof( " over_ceiling=\"1\"" ) - 1, rw::kBytesPerTokenDefault, 0, &estTokens ); rootAttrs += " over_ceiling=\"1\""; } rw::spliceRootAttrs( doc, rootAttrs ); @@ -1898,6 +2002,7 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref const std::vector& impure; RedactCounts* redact; std::size_t sigsBudget; + std::size_t rowDropBudget; // forbudget-soft: where step F aims (the guard / the soft target) std::size_t fixedBytes; std::size_t budgetTokens; const notes::NoteIndex& noteIndex; @@ -1912,8 +2017,49 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref rw::SigLensRules lensRules; // train 26b: the --for lens rules (LB-A floor, e=, docs reorder), // decided once in forTaskText (the noRoute/task/budget verdicts live there) const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render + const LexTermEvidence& evidence; // FOLD C: the term evidence the tail's page rows rank by + const Graph& g; // for-body-slot: the owner body's edges + ForBodySlotOwner slotOwner; // for-body-slot: the one owner body (forowner.h), none when not asked }; -inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFromRows, std::string headerStr, std::string legoStr, +// round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST — the CLI twin's rule): under budget_tokens the sections are scoped and +// served from the rows at the target cut, and the rows past the cut fill what the sections served before the tail ( rows, +// , , : `sectionsBytes`) leave of T — never below the cut. The tail rides on top on this twin, as it always +// did. Re-rendered only when that narrows the rows; the first render's redaction count is replaced, never added to. +template< class Render > +inline void mcpForSoftRefill( const McpForAssemblyInputs& in, const Render& renderSigs, std::string& sigsStr, rw::SigsCutReport& cut, + const rw::SigsCutContinuation* next, std::size_t sectionsBytes, + const std::optional& before, const std::optional& after ) +{ + if( in.budgetTokens == 0 || cut.pastTarget == 0 ) + { + return; + } + const std::size_t rowDrop = forSoftRowDropFill( budgetBytesForTokens( in.budgetTokens ), sectionsBytes ); + if( rowDrop >= in.rowDropBudget ) + { + return; + } + redactTakeBack( in.redact, before, after ); + cut = rw::SigsCutReport {}; + cut.continuationRequest = next; + cut.rowDropBudgetBytes = rowDrop; + sigsStr = captureXml( renderSigs ); // assembleMcpFor's own render seam (its renderToString) +} + +// round C (RESERVE SECTIONS FIRST, the CLI twin's rule): the --sections= posture the stub plan reads — under budget_tokens the two +// sections ride whole when they fit what the rows at the cut (and ) left of the budget (serialize.h forSoftSectionsOptIn) +inline std::string_view mcpSoftSections( const McpForAssemblyInputs& in, const std::string& sigsStr, const rw::SigsCutReport& cut, + std::size_t sectionsBytes ) +{ + if( in.budgetTokens == 0 ) + { + return in.sections; + } + const std::size_t atCut = sigsStr.size() - std::min( sigsStr.size(), cut.pastTargetBytes ); + return forSoftSectionsOptIn( in.sections, sectionsBytes, 0u, + forSoftSectionsShare( budgetBytesForTokens( in.budgetTokens ), atCut + in.routeStr.size() ) ); +} +inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::string headerStr, std::string legoStr, std::string composeStr, std::vector> legoScoped, std::size_t legoPreCapCount ) { const IngestResult& ing = in.ing; @@ -1942,9 +2088,9 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr { return McpForAssembly{}; // the answer buffer could not be opened: an internal error, never "not found" } + // forbudget-soft: exempt in every regime (no byte promise to pay into: a target + the runaway guard, or a soft target) const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, - /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false, - /*ledgerGapBytes=*/0u, payFromRows }; + /*json=*/false, /*pasteHandle=*/false }; // 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 @@ -1953,8 +2099,16 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr 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 ) + mcpSigsCut.rowDropBudgetBytes = in.rowDropBudget; // forbudget-soft: IN — the CLI twin's two budgets + mcpSigsCut.pastTargetDisclosed = budgetTokens == 0; + // R2-AF (round 2, S4): first rows inside the root, right after the legend (forHdrRowsXml); rendered before the rows so the + // round C refill below prices them + const std::string mcpHdrXml = forHdrRowsXml( ing, mcpForHdrRows, flRootArg ); // forpage.h: the CLI twin's one render + std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail's rank rows exclude these files + // round C: the render is named so a soft target can narrow its rows past the cut once the sections are served (below); the + // redaction tally around it is snapshotted so a second render replaces this one's count instead of adding to it + const std::optional redactBeforeSigs = redact != nullptr ? std::optional( *redact ) : std::nullopt; + const auto renderSigs = [ & ]( 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) @@ -1970,7 +2124,97 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr &mcpSigsCut, // cut-fix lane A: docs_dropped= / shrunk readings; // knob-honesty-068: carries mcpSigsNext IN SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: the CLI twin's row spelling - } ); + }; + std::string sigsStr = renderToString( renderSigs ); + const std::optional redactAfterSigs = redact != nullptr ? std::optional( *redact ) : 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. + // 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, sigsRowsAtTarget( sigsStr, mcpSigsCut ), mcpLegoRootPrefix ) ) // forbudget-soft + { + legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &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= + // 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; + { + // the CLI twin's one plan (rw::planForSectionStubs): this surface always renders whole, so rendered=true + const ForStubbableSection mcpLegoServedIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = true, + .renderedBytes = legoStr.size() }; + const ForStubbableSection mcpComposeServedIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = true, + .renderedBytes = composeStr.size() }; + // round C (reserve first, the CLI twin's rule): under budget_tokens the sections ride whole when they fit what the rows + // at the cut left of the budget + const ForSectionStubPlan plan = planForSectionStubs( mcpSoftSections( in, sigsStr, mcpSigsCut, legoStr.size() + composeStr.size() ), + task, mcpLegoServedIn, mcpComposeServedIn ); + // lego-charge (train 26c seam): the charge plan the budget follows (forTaskText's mcpSigChargePlan, the same + // rule: rw::forSectionsSigChargePlan), recomputed here from the sections AS SERVED — the count-floor lego re-render + // above may have changed the section's size since forTaskText charged the budget, and the invariant below is the + // two planners' agreement on one input: the served stub never exceeds the stub the charge plan collapses it to. + const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, /*explicitCeiling=*/false, task, mcpLegoServedIn, mcpComposeServedIn ); + const bool mcpLegoWillStub = plan.legoWillStub; + const bool mcpComposeWillStub = plan.composeWillStub; + // 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 || plan.lego.stubXml.size() < legoStr.size(), + "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); + ENSURES( !mcpComposeWillStub || plan.compose.stubXml.size() < composeStr.size(), + "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); + ENSURES( !( mcpLegoWillStub && mcpSigChargePlan.legoWillStub ) || plan.lego.stubXml.size() <= mcpSigChargePlan.lego.stubXml.size(), + "lego-charge MCP twin: the served lego stub is longer than the stub the budget was charged for" ); + if( mcpLegoWillStub ) + { + legoStr = plan.lego.stubXml; + } + if( mcpComposeWillStub ) + { + composeStr = plan.compose.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; + } + + // round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST — the CLI twin's rule): under budget_tokens the sections are + // scoped and served from the rows at the target cut (above), and the rows past the cut fill what the sections served + // before the tail ( rows, , , ) leave of T — never below the cut. The tail rides on top here, + // as it always did on this twin. Re-rendered only when that narrows the rows; the first render's redaction count is + // replaced, never added to. + // for-body-slot (the CLI twin's rule, forowner.h): the one owner body. At the default it rides on top; under budget_tokens it + // is funded from what the rows at the cut and the sections before it left of T, rides only whole, else is skipped with its + // next=, and the rows' refill below charges what it served — so the rows past the cut give way to it. + BodySlotRender mcpSlot; + if( in.slotOwner.any() ) + { + std::optional share; + if( budgetTokens > 0 ) + { + const std::size_t atCut = sigsStr.size() - std::min( sigsStr.size(), mcpSigsCut.pastTargetBytes ); + share = forSoftSectionsShare( budgetBytesForTokens( budgetTokens ), mcpHdrXml.size() + atCut + legoStr.size() + composeStr.size() + routeStr.size() ); + } + mcpSlot = renderForBodySlot( BodySlotInputs{ ing, in.g.outOff, in.g.outTargets, in.g.outNameOnly, lensRank, flRootArg, redact, notesPtr, + /*viaLegendInHead=*/false, /*compress=*/false, &in.g.outProv, /*splitLegendInHead=*/false }, + in.slotOwner, share ); + } + mcpForSoftRefill( in, renderSigs, sigsStr, mcpSigsCut, &mcpSigsNext, + mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + mcpSlot.xml.size(), + redactBeforeSigs, redactAfterSigs ); // 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 @@ -2004,7 +2248,18 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr const std::size_t closeAt = headerStr.rfind( " -->" ); if( closeAt != std::string::npos ) { - headerStr.insert( closeAt, rw::kForBudgetBytesNote ); + // forbudget-soft: "target" where the head ran past it (the CLI twin's choice) + headerStr.insert( closeAt, mcpSigsCut.pastTarget > 0 ? rw::kForBudgetBytesTargetNote : rw::kForBudgetBytesNote ); + } + } + // forbudget-soft: the runaway guard's reading, where it cut the default head (the CLI twin's clause; priceForTaskRoot puts + // over_ceiling="1" on the root) + if( mcpSigsCut.rowDropCut && budgetTokens == 0 ) + { + const std::size_t closeAt = headerStr.rfind( " -->" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, rw::forRunawayGuardNote() ); } } // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped, docs_after_code=, the continuation's) — @@ -2035,106 +2290,22 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr } } 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. - // 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, &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= - // 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; - { - // the CLI twin's one plan (rw::planForSectionStubs): this surface always renders whole, so rendered=true - const ForStubbableSection mcpLegoServedIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = true, - .renderedBytes = legoStr.size() }; - const ForStubbableSection mcpComposeServedIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = true, - .renderedBytes = composeStr.size() }; - const ForSectionStubPlan plan = planForSectionStubs( sections, task, mcpLegoServedIn, mcpComposeServedIn ); - // lego-charge (train 26c seam): the charge plan the budget follows (forTaskText's mcpSigChargePlan, the same - // rule: rw::forSectionsSigChargePlan), recomputed here from the sections AS SERVED — the count-floor lego re-render - // above may have changed the section's size since forTaskText charged the budget, and the invariant below is the - // two planners' agreement on one input: the served stub never exceeds the stub the charge plan collapses it to. - const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, budgetTokens > 0, task, mcpLegoServedIn, mcpComposeServedIn ); - const bool mcpLegoWillStub = plan.legoWillStub; - const bool mcpComposeWillStub = plan.composeWillStub; - // 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 || plan.lego.stubXml.size() < legoStr.size(), - "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); - ENSURES( !mcpComposeWillStub || plan.compose.stubXml.size() < composeStr.size(), - "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); - ENSURES( !( mcpLegoWillStub && mcpSigChargePlan.legoWillStub ) || plan.lego.stubXml.size() <= mcpSigChargePlan.lego.stubXml.size(), - "lego-charge MCP twin: the served lego stub is longer than the stub the budget was charged for" ); - if( mcpLegoWillStub ) - { - legoStr = plan.lego.stubXml; - } - if( mcpComposeWillStub ) - { - composeStr = plan.compose.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( mcpHdrXml.data(), 1, mcpHdrXml.size(), mem ); // R2-AF: rendered above, before the rows (round C prices it) const bool mcpLegoCountAttrs = legoStr.find( rw::kLegoCountAttrPrefix ) != std::string::npos; // 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 ); 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. + // DEEP-TAIL d2, MCP twin: the same model + renderer the CLI --for uses (forpage.h; FOLD C: page 0 of the file page) — + // it rides on top at the default row cap in every regime here, as it did before (this twin funds no trailing section + // from budget_tokens but /), and a cut tail carries the page after its last row as next=. { std::vector tailEsc; - const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), - kForFileTailShownCap, tailEsc ); + const FileTail mcpTail = computeFileTail( ing, lensRank, in.evidence, mcpShownIds, flRootArg, task ); + const std::string tailStr = renderFileTailXml( mcpTail, mcpTail.paths.size(), tailEsc ); // every row computeFileTail collected std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); } + std::fwrite( mcpSlot.xml.data(), 1, mcpSlot.xml.size(), mem ); // for-body-slot: "" when no body rides rw::emitRaw( mem, "" ); std::optional answer = mcpAnswerText( stream ); if( !answer ) @@ -2508,6 +2679,15 @@ inline std::optional forTaskText( const std::string& root, const st // resolution, both surfaces. Rows are emitted right after headerStr is flushed, below; the legend // clause (present-only, never ceiling-dropped — this dialect has no ceiling ladder at all) goes here. const std::vector mcpForHdrRows = rw::forNamedHeaderRows( ing, task ); + // for-body-slot (the CLI twin's rule, forowner.h): an explanatory question on the conceptual route carries ONE owner body. + // The bundle sentence then says so (kMcpForBundleSigsOwnerLegend replaces "no inline bodies") and the slot clause rides; both + // are exempt from the sigs charge below exactly as far as they differ from the sentence they replace, so the served rows are + // byte-identical to RIPWIRE_NO_BODY_SLOT=1's. + const ForBodySlotOwner mcpSlotOwner = ( std::string_view( mcpRouteTag ) == "subtoken+body" && forBodySlotAsked( task ) ) + ? forBodySlotOwner( ing, ix.g.outOff, ix.g.outTargets, task, lensSurfaceIds, lensRank ) + : ForBodySlotOwner{}; + const std::string_view mcpBundleLegend = mcpSlotOwner.any() ? rw::kMcpForBundleSigsOwnerLegend : rw::kMcpForBundleSigsLegend; + const std::string_view mcpBodySlotLegend = mcpSlotOwner.any() ? rw::kForBodySlotLegend : std::string_view(); std::string headerStr = rootOpenStr + "", kImpactLegendOpen, kPageRaiseCapClause, reach.empty() ? "" : kImpactDepthLegend, // 0.6.5: exactly when d=/by_depth= ride, as on the CLI - kImpactImportTierLegend, + ( std::string( kImpactImportTierLegend ) + importersNextLegend( !imports.next.empty() ) ).c_str(), // cut-fix E, 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 - declinedCallsLegendWithGates( declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI + declinedCallsLegendWithGates( declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ) + declinedSitesImpactPointer( declinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI (+ where its sites are listed) declinedIfaceLegend( declinedIface > 0 ), // likewise declined_iface=, as on the CLI ( std::string( valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs=, as on the CLI + valueRefsDepthLegendFor( imValueRefs ) // ... and the depth disclosure, as on the CLI - + impactRollupLegend( !imFiles.rows.empty(), imports.umbrella, impactRollupHedged( imFiles ) ) ).c_str(), // idea #6, as on the CLI + + impactRollupLegend( !imFiles.rows.empty(), imports.umbrella, impactRollupHedged( imFiles ), !imports.next.empty() ) // idea #6; FIX1 R1, as on the CLI + + defTableLegend( imAttr.groups.multi(), imListing.cut ) ).c_str(), // attrib-multidef, 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, @@ -3016,21 +3182,22 @@ inline std::optional impactText( const std::string& root, const std const std::string imRootPrefix = imSingleRoot ? sarif::rootPrefixOf( root ) : std::string(); const std::string imRootAttr = imSingleRoot ? ( " root=\"" + ex( root ) + "\"" ) : std::string(); rw::emitTo( mem, "", - ex( symbol ).c_str(), seeds.size(), reach.size(), unprovenDefsAttrXml( unprovenDefs ).c_str(), // H1: where the CLI root carries it + ex( symbol ).c_str(), seeds.size(), reach.size(), unprovenDefsAttrXml( unprovenDefs ) + defsListedAttrXml( imAttr.groups, imListing ), // H1: where the CLI root carries it; attrib-multidef 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() ) + valueRefsDepthAttrXml( ing, imValueRefs.depthCut, 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) + rw::emitRaw( mem, defTableXml( ing, imAttr.groups, imListing, imSingleRoot ? std::string_view( imRootPrefix ) : std::string_view() ).c_str() ); // attrib-multidef emitImpactFileRollupXml( mem, ing, imFiles, imSingleRoot ? std::string_view( imRootPrefix ) : std::string_view() ); // idea #6, as on the CLI for( std::size_t i = ipw.begin; i < ipw.end; ++i ) { 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, + 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) + viaNameAttr( impactRowNameOnly( proven, show[i] ) ), imAttr.ofDefXml( show[i] ) ); } // FE-B (D3); attrib-multidef // 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, @@ -3210,9 +3377,9 @@ inline std::string usesSelectorRefusal( const IngestResult& ing, const std::stri { continue; } - if( r.isCompose || r.isDocLink ) + if( r.isCompose || r.isDocLink || r.role == RefRole::Subscript ) { - continue; // type edge / doc mention — not a use-site + continue; // type edge / doc mention / an element access (no use-site: valuerefindex.h UsesValueFilter::skip) — not a use-site } if( r.lang == Lang::Markdown ) { @@ -3461,13 +3628,17 @@ inline std::optional pathText( const std::string& root, const std:: // 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{}; const bool ptHasVia = pathHasNameOnlyHop( g, pth ); // FE-B: exactly when a hop of this path is via="name", as on the CLI + const PathEndpointBinding ptBound = pathEndpointBinding( g, endpointDefinitionCount( ing, srcDefs ), endpointDefinitionCount( ing, dstDefs ), pth ); // attrib-multidef, as on the CLI + const bool ptHasSplit = ptBound.splitHop; rw::emitTo( mem, "{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) ).c_str(), + "graph holds none. {}{}{}{}-->{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) + + splitArmLegend( ptHasSplit ) + pathUnboundLegend( !ptBound.from || !ptBound.to ) ).c_str(), ( std::string( toValueRefsLegend( ptToValueRefs.count > 0 ) ) + valueRefsDepthLegend( ptToValueRefs.depthCut.files > 0 ) ).c_str(), pathGapsLegend( ptGaps.any() ), graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rootRelPathsLegend( ptSingleRoot ) ); - rw::emitTo( mem, " pathText( const std::string& root, const std:: 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, - viaNameAttr( i > 0 && edgeNameOnly( g, pth[ i - 1 ], pth[ i ] ) ) ); } // FE-B: as on the CLI + 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 ] ) ), splitArmAttrXml( pathHopSplitArm( g, pth, i ) ) ); } // FE-B; attrib-multidef: as on the CLI rw::emitRaw( mem, "" ); return mcpAnswerText( stream ); // nullopt = the buffer lost bytes (dispatch answers -32603), "" stays not-found } @@ -4542,6 +4713,7 @@ inline std::string packTaskText( const std::string& root, const std::string& tas PackTaskInputs in; in.budgetTokens = budgetTokens; + in.softTarget = budgetTokens > 0 && !( partitionCount >= packpartition::kMinPartitions && partitionCount <= packpartition::kMaxPartitions ); // forbudget-soft: the CLI twin's rule in.fanIn = &fanIn; in.impure = &impure; in.redact = redact; @@ -4583,6 +4755,9 @@ inline FromTraceResult fromTraceText( const std::string& root, const std::string FromTraceInputs in; in.bundleBudgetBytes = budgetTokens > 0 ? rw::budgetBytesForTokens( budgetTokens ) : rw::kForPayloadBudgetBytes; + // forbudget-soft (sibling of the CLI --from-trace): the caller's budget_tokens is named on the root and read as a SOFT + // target (no ceiling ladder, no over_ceiling for it) — this twin used to shape by it without naming it at all + in.budgetTokens = budgetTokens; in.fanIn = &fanIn; in.impure = &impure; in.redact = redact; diff --git a/src/memguard.h b/src/memguard.h index bae76491e..c98da6faf 100644 --- a/src/memguard.h +++ b/src/memguard.h @@ -23,6 +23,10 @@ // became a hard stop after the ingest instead of a partial answer. // hard at the limit itself, checked between phases (main.cpp; --quality-delta=A..B, which dispatches before main()'s // ingest, between its own: verbs_quality.h refPairOverHardLimit) and before each MCP tool call: no partial answer. +// The range form alone answers PART of the question instead (qd-memory-degrade): a stop in either commit's full +// tree — or a projection that says the target phase will not fit (projectTargetPhase) — releases the full trees +// and re-asks from both commits' CHANGED FILES the kinds those files decide, naming every kind it skipped +// (exit 2 when a row gates, else 6); exit 5 is then left for "even the changed files did not fit". // pressure: the OS signal (os::mem_pressure) at "critical" stops the crawl or the parse too — but only once this // process holds at least max( 256 MiB, limit/8 ), so a small run is never cut because something ELSE is // using the machine. It never causes a hard stop. @@ -32,21 +36,28 @@ // completion, and only the thread that wins the time slot pays the syscall. A CLI run shorter than five seconds reads // the footprint three times — at ingest start, after the ingest and after the graph build (the hard line) — and an MCP // tool call once more. The `--quality-delta=A..B` range form reads it up to 2 + 6 times: two outside its commit -// ingests and the six hard-line readings around them. None of it changes a byte of output unless a line is crossed -// (test/memguardcheck.sh (C)). No thread, no timer, no signal handler. +// ingests and the six hard-line readings around them, plus one projection after the base commit (two footprint readings +// and one PEAK reading, os::mem_footprint_peak) and one peak reading after the answer. None of it changes a byte of +// output unless a line is crossed (test/memguardcheck.sh (C), test/qddegradecheck.sh (L)). No thread, no timer, no +// signal handler. // // THE STOP. A soft stop sets an atomic flag the crawl loop and the parse workers read before the next unit of work, // records WHY through DISCLOSE on IngestResult::memoryStop, and lets ingest() finish with what it has: the crawl keeps // the entries it saw (then sorts them, as always); the parse keeps every file it claimed — a prefix of its parse order // (a worker checks the flag BEFORE claiming a file, so every claimed file completes). Nothing derived from a partial ingest is ever persisted — not the ingest // cache, not a quality snapshot, not a baseline. Whether the partial ingest may answer is the caller's decision — main.cpp answers the -// default map with memory_stop= in its header and refuses every other verb (exit 5); the MCP server answers with -// `_memory_stop` in the envelope. +// default map with memory_stop= in its header and refuses every other verb (exit 5) except --quality-delta=A..B, which +// releases the cut tree unscored and answers from both commits' changed files (see "hard" above); the MCP server +// answers with `_memory_stop` in the envelope. // // THE TEST SEAM. RIPWIRE_TEST_MEMGUARD=crawl:N | pressure:N | parse:N | request:N | hard:N ADDS a trip — at the Nth crawl entry // (over the line, or under critical pressure), after exactly N files of the parse order are parsed, or at the Nth and // later MCP tool calls, or at the Nth and later hard-line readings (hard:N); eager:1 instead drops the five-second time gate, so every guarded unit takes a REAL reading — so test/memguardcheck.sh can drive every stop path deterministically. It never replaces a real -// reading: every real line, the time gate and the hard line still apply, so it can only make a run stricter. +// reading: every real line, the time gate and the hard line still apply, so it can only make a run stricter. crawl:N and +// parse:N count per INGEST (each ingest's own Watch). Three more for the range form (test/qddegradecheck.sh): project:N — +// the Nth target-phase projection reads "does not fit"; ingest:N — the Nth ingest of the process stops its crawl at its +// first entry (a stop in ONE chosen ingest, e.g. the target's after the base fit); peak:N — the Nth post-answer peak +// reading reads as over the limit. // // REPEATABILITY. A crawl stop keeps what the walk had seen, then sorts it. A parse stop keeps the first K slots of the // parse ORDER (cache misses, then cache hits, then grammarless files, each largest first, then fileId — or plain fileId @@ -98,8 +109,9 @@ inline Limits& limitsSlot() noexcept } inline const Limits& limits() noexcept { return limitsSlot(); } -// 65% of min( physical RAM, cgroup memory.max ), or 0 when the machine cannot say how much RAM it has. -inline std::uint64_t defaultLimitBytes() +// the machine's own ceiling: physical RAM, or the cgroup's memory.max when that is lower; 0 when unknown. Also what a +// degraded --quality-delta=A..B compares its replay's need with (a need above it is recoverable only on a larger machine). +inline std::uint64_t machineMemoryBytes() { std::uint64_t machine = os::mem_physical(); const std::uint64_t cgroup = os::mem_cgroup_max(); @@ -107,7 +119,12 @@ inline std::uint64_t defaultLimitBytes() { machine = cgroup; } - return machine / 100 * 65; + return machine; +} +// 65% of min( physical RAM, cgroup memory.max ), or 0 when the machine cannot say how much RAM it has. +inline std::uint64_t defaultLimitBytes() +{ + return machineMemoryBytes() / 100 * 65; } // the footprint every line is measured on: this process's own (os::mem_footprint), or, when it is larger, the @@ -143,6 +160,9 @@ enum class TripAt : std::uint8_t Request, // request:N — the Nth MCP tool call reads as over the hard limit Hard, // hard:N — the Nth hard-line reading (overHardLimit: between CLI phases, per workspace root, between --quality-delta=A..B's phases) reads as over Eager, // eager:1 — no five-second time gate: every guarded unit takes a REAL footprint reading (only stricter) + Project, // project:N — the Nth target-phase projection (projectTargetPhase) reads as "does not fit" + Ingest, // ingest:N — the Nth ingest of the process stops its crawl at its first entry (a crawl stop) + Peak, // peak:N — the Nth post-answer peak reading (peakOverLimit) reads as over the limit }; struct TestTrip { @@ -159,6 +179,7 @@ inline TripAt tripAtFor( std::string_view phase ) noexcept } kPhases[] = { { "crawl", TripAt::Crawl }, { "pressure", TripAt::Pressure }, { "parse", TripAt::Parse }, { "request", TripAt::Request }, { "hard", TripAt::Hard }, { "eager", TripAt::Eager }, + { "project", TripAt::Project }, { "ingest", TripAt::Ingest }, { "peak", TripAt::Peak }, }; for( const auto& p : kPhases ) { @@ -207,6 +228,32 @@ inline const TestTrip& testTrip() noexcept using GateClock = std::chrono::steady_clock; inline constexpr GateClock::rep kCheckIntervalTicks = std::chrono::duration_cast( std::chrono::nanoseconds( kCheckIntervalNs ) ).count(); +// The seams' process-wide call counters, one struct (stopCounts' shape): every ingest() (the ingest:N ordinal, counted +// at its Watch), the MCP tool calls (request:N), the range form's projections (project:N) and its post-answer peak +// readings (peak:N). Relaxed: each is read by the call that bumps it. +struct SeamCounters +{ + std::atomic ingests{ 0 }; + std::atomic requests{ 0 }; + std::atomic projections{ 0 }; + std::atomic peaks{ 0 }; +}; +inline SeamCounters& seamCounters() noexcept +{ + static SeamCounters counters; + return counters; +} +// one call of a call-counted seam: true for the Nth call (`laterToo` false) or the Nth and every later one +inline bool seamCallTrips( TripAt at, std::atomic& seen, bool laterToo ) noexcept +{ + if( testTrip().at != at ) + { + return false; + } + const std::uint64_t n = seen.fetch_add( 1, std::memory_order_relaxed ) + 1; + return laterToo ? n >= testTrip().index : n == testTrip().index; +} + // install: main() calls this once, before any thread, with the resolved limit. A zero `bytes` means "the default". inline void install( std::uint64_t bytes, Source source ) { @@ -225,6 +272,7 @@ class Watch Watch() noexcept : trip_( testTrip() ) , hardBytes_( limits().hardBytes ) + , ingestOrdinal_( seamCounters().ingests.fetch_add( 1, std::memory_order_relaxed ) + 1 ) { baseFootprint_ = measuredFootprint(); nextCheckTick_.store( GateClock::now().time_since_epoch().count() + kCheckIntervalTicks, std::memory_order_relaxed ); @@ -243,6 +291,10 @@ class Watch { return tripSoft( trip_.at == TripAt::Pressure ); } + if( trip_.at == TripAt::Ingest && ingestOrdinal_ == trip_.index ) + { + return tripSoft( false ); // ingest:N — this whole ingest is the one the seam cuts, at its first entry + } if( ( entryCount & ( kCrawlStride - 1 ) ) != 0 || !claimTimeSlot() ) { return false; @@ -329,6 +381,7 @@ class Watch const TestTrip& trip_; const std::uint64_t hardBytes_; + const std::uint64_t ingestOrdinal_; std::uint64_t baseFootprint_ = 0; std::atomic nextCheckTick_{ 0 }; std::atomic stopped_{ false }; @@ -362,14 +415,85 @@ inline bool overHardLimit() // pre-check is `requestSeamTrips() || overHardLimit()` — additive, like every seam. inline bool requestSeamTrips() { - if( testTrip().at != TripAt::Request ) + return seamCallTrips( TripAt::Request, seamCounters().requests, /*laterToo=*/true ); +} + +// ── the range form's projection and peak (qd-memory-degrade) ─────────────────────────────────────────────── +// --quality-delta=A..B scores the base commit and releases it before the target is materialized (#387). The target +// phase then holds the base snapshot, the full target tree and computeDelta's passes; on the measured repositories its +// growth over the starting footprint was 0.9-1.4x the base phase's. What the base released is reused, not added: the +// footprint once it is released (`after`) is anywhere from the start plus the snapshot (the allocator returned the +// pages) to the base phase's peak (it kept them), and the target phase grows into that. So after the base phase: +// need = max( after, before + k * ( basePeak - before ) ) against the hard limit, and +// ingestNeed = max( after, ingestPeak ) against the parse line (half the limit), +// where `before` is this process's own footprint before the base phase and `basePeak` / `ingestPeak` its PEAK at the end +// of the base phase / of the base ingest (os::mem_footprint_peak — the readings between phases miss the clone passes' +// transient peak). k is kProjectionScalePct / 100, calibrated in the qd-memory-degrade report as the max over the measured +// corpora of ( full peak - before ) / ( base-phase peak - before ), rounded up. A run whose true need lies within that +// margin below the limit may be answered degraded; the projection never lets a run past the limit by its own choice (an +// under-projection is caught by the guard's stops, which degrade). An unknown peak (0) projects nothing: the full attempt +// runs, and a stop in it still degrades. +inline constexpr std::uint64_t kProjectionScalePct = 175; // max measured 1.48 over 11 cold runs on 5 corpora (qd-memory-degrade report), + ~18% +struct TargetProjection +{ + bool measured = false; // false: no peak reading — nothing was projected + bool fits = true; + std::uint64_t needBytes = 0; // the projected need (0 when not measured) + std::uint64_t ingestNeedBytes = 0; // max( after, ingestPeak ), judged against half the limit (0 when not measured) +}; +inline TargetProjection projectTargetPhase( std::uint64_t before, std::uint64_t ingestPeak, std::uint64_t basePeak, std::uint64_t after ) +{ + TargetProjection p; + const std::uint64_t limit = limits().hardBytes; + p.measured = basePeak != 0 && ingestPeak != 0 && after != 0 && basePeak >= before; + if( p.measured ) { - return false; + const std::uint64_t growth = basePeak - before; + const std::uint64_t scaled = growth / 100 * kProjectionScalePct + growth % 100 * kProjectionScalePct / 100; // no overflow below 2^57 B + p.needBytes = std::max( after, before + scaled ); + const std::uint64_t ingestNeed = std::max( after, ingestPeak ); + p.ingestNeedBytes = ingestNeed; + p.fits = limit == 0 || ( p.needBytes < limit && ingestNeed < limit / 2 ); } - static std::atomic requestsSeen{ 0 }; - return requestsSeen.fetch_add( 1, std::memory_order_relaxed ) + 1 >= testTrip().index; + if( seamCallTrips( TripAt::Project, seamCounters().projections, /*laterToo=*/false ) ) + { + p.fits = false; // the seam: additive — it can only make a run degrade, never let one run full past a real projection + } + return p; +} +// --max-memory for a replay of what a degraded run skipped: strictly above BOTH lines the projection judged — the projected +// need (against the limit) and twice the ingest need (against the parse line, half the limit) — and never below the limit +// this run used, in whole MiB, at least the parser's floor, and at most what a size_t can hold (item 27: the value the +// continuation names is one the parser accepts). After a stop no need was measured, so the replay names twice the limit: +// a guess, and said to be one. +inline std::uint64_t replayLimitBytes( std::uint64_t needBytes, std::uint64_t ingestNeedBytes, bool needMeasured ) +{ + const std::uint64_t limit = limits().hardBytes; + const std::uint64_t ingestLine = ingestNeedBytes > ( UINT64_MAX / 2 ) ? UINT64_MAX : ingestNeedBytes * 2; + const std::uint64_t need = std::max( { needBytes, ingestLine, limit } ); + std::uint64_t want = needMeasured ? ( need >= UINT64_MAX - kMiB ? UINT64_MAX : ( ( need + kMiB - 1 ) / kMiB + 1 ) * kMiB ) // one MiB above, as spelled + : ( limit > ( UINT64_MAX / 2 ) ? UINT64_MAX : limit * 2 ); + want = std::max( want, kFloorBytes ); + const std::uint64_t ceiling = std::uint64_t( SIZE_MAX ) / kMiB * kMiB; + return std::min( want, ceiling ); +} +// The peak after the answer was computed, when it went over the limit (or the peak:N seam says so), else 0. computeDelta +// runs after the last hard-line reading, so a full answer can finish over --max-memory (rv-pr391 N1: 1.09-1.11x); this +// is the reading that SAYS so instead of answering silently over it. It never changes the exit code. +inline std::uint64_t peakOverLimit() +{ + const std::uint64_t limit = limits().hardBytes; + if( limit == 0 ) + { + return 0; + } + std::uint64_t peak = os::mem_footprint_peak(); + if( seamCallTrips( TripAt::Peak, seamCounters().peaks, /*laterToo=*/false ) ) + { + peak = std::max( peak, limit + kMiB ); // the seam: the reading says over, whatever the real peak was + } + return peak > limit ? peak : 0; } - // ── the backstop: a stop nobody answered for ──────────────────────────────────────────────────────────── // ingest() counts every stop it discloses; the caller that decides what the partial ingest may answer (main.cpp's // memoryStopExit) marks every stop so far as answered for. A CLI run that ends with a stop nobody answered for — an @@ -405,13 +529,20 @@ inline std::string_view phaseName( MemoryStop::Phase phase ) noexcept // every message ends with the override inline constexpr std::string_view kOverride = "raise it with --max-memory=[K|M|G] or RIPWIRE_MAX_MEMORY, or pass a smaller root"; -// the hard stop's one line: nothing was built that could answer -inline std::string hardStopLine( std::string_view where ) +// where the limit came from, as the clause every message spells it with +inline std::string_view limitSourceClause() noexcept { const Limits& l = limits(); - return "memory limit reached during the " + std::string( where ) + " (limit " + limitSpelling( l.hardBytes ) - + ( l.source == Source::Flag ? ", from --max-memory" : l.source == Source::Env ? ", from RIPWIRE_MAX_MEMORY" : ", 65% of this machine's memory" ) - + "); stopped cleanly without an answer — " + std::string( kOverride ); + return l.source == Source::Flag ? "from --max-memory" : l.source == Source::Env ? "from RIPWIRE_MAX_MEMORY" : "65% of this machine's memory"; +} + +// the hard stop's one line: nothing was built that could answer. `also` is said between the limit and the verdict — the +// range form's "the changed files were not tried" / "were cut too" — and is empty everywhere else (the line keeps its bytes). +inline std::string hardStopLine( std::string_view where, std::string_view also = {} ) +{ + std::string line = "memory limit reached during the "; + line.append( where ).append( " (limit " ).append( limitSpelling( limits().hardBytes ) ).append( ", " ).append( limitSourceClause() ).append( "); " ); + return line.append( also ).append( "stopped cleanly without an answer — " ).append( kOverride ); } // which line a soft stop crossed, spelled against the limit that sets it (a soft stop is never the limit itself) diff --git a/src/mergescout.h b/src/mergescout.h index e23478dc8..f2343ed1f 100644 --- a/src/mergescout.h +++ b/src/mergescout.h @@ -637,7 +637,7 @@ inline ScoutResult computeMergeScout( const std::string& root, std::string_view baseShas[i] = resolveMergeBase( root, refShas[i], result.headSha ); } - const bool dirty = !quality::gitOneLine( root, "status --porcelain 2>/dev/null" ).empty(); // dirty ⇒ the implicit extra arm + const bool dirty = !quality::gitOneLine( root, "status --porcelain --ignore-submodules=dirty 2>/dev/null" ).empty(); // dirty ⇒ the implicit extra arm HeadChangedByBase headChangedByBase = planHeadConflictLane( baseShas, result.headSha ); diff --git a/src/model.h b/src/model.h index 4e6bf0dd2..e8ed7d9e4 100644 --- a/src/model.h +++ b/src/model.h @@ -403,16 +403,24 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // 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 // may-call clue); never a use site, never in the CSR. -enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through }; +// Subscript — an element access `recv[ args ]` (Ruby, Python; ingest_binds.h subscriptOperatorRef), named as the +// receiver class's subscript OPERATOR METHOD (kSubscriptOperatorMethods: Ruby `[]` / `[]=`, Python +// `__getitem__` / `__setitem__`); receiver fields as a call's. NOT a call by itself: graph.h's resolve loop +// takes it up as one ONLY where a receiver rule proves the class (subscriptOperatorProven) — `self[ k ]` in a +// class, a receiver the source types — and drops it otherwise, with no count anywhere (a `hash[ k ]` is the +// builtin's own operator, not an unresolved call). Never a use site. +// Inert — an Elixir call/operator/named-capture candidate whose enclosing quote AST remains inert after live-hole re-entry. +// It remains a site for declined/report accounting, but can never bind or mint a call edge. +enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through, Subscript, Inert }; // The number of RefRole enumerators — the bound readRef validates a cached role byte against (see kSymKindCount). -inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Through ) + 1; +inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Inert ) + 1; static_assert( enumCountIsExact(), "kRefRoleCount must name the LAST RefRole enumerator — move it with the append" ); static_assert( sizeof( RefRole ) == 1, "RefRole must be a single byte (SoA-friendly, smallest int that fits)" ); // the terse `role=` attribute string for the use-site index — a declarative table indexed by the enum, in enum // order. The static_assert is the guard a switch's -Werror=switch used to be: a NEW role without a spelling is a // build error, never a silent fallback. -inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through" }; +inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through", "subscript", "inert" }; static_assert( std::size( kRefRoleTagTable ) == kRefRoleCount, "kRefRoleTagTable: one spelling per RefRole, in enum order" ); inline const char* refRoleTag( RefRole r ) noexcept { @@ -1103,9 +1111,15 @@ enum class LocalBindKind : std::uint8_t 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). + RubyMainMixin, // parser version 174 (lane ruby-pair): a Ruby file's TOP-LEVEL `include M` / `extend M` / `self.extend M` — + // self there is the main object, which then answers M's methods (rake's `self.extend Rake::DSL`). One + // record per constant, at the constant's own start: typeName = the constant as written, importedName = + // the directive ("include" / "extend"), spanStart = the constant's 1-based line; var stays EMPTY and the + // record is file-scope, so no local-name table reads it. Read by graph.h rubyMainObjectMixins ONLY + // (--lego's `` row). APPENDED (cache u8). }; // 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::StaticMember ) + 1; +inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::RubyMainMixin ) + 1; // FE-B: the five kinds only graph.h ReceiverEvidence reads (see RecvType above). inline bool isReceiverEvidenceKind( LocalBindKind k ) noexcept { @@ -1122,6 +1136,15 @@ static_assert( enumCountIsExact(), "kLocalBi inline constexpr const char* kFnBindLambdaTarget = "(lambda)"; // parens are illegal in identifiers, so inline constexpr const char* kFnBindClobberTarget = "(unknown)"; // neither sentinel can match a real def +// One main-object implementor row (Graph::rubyMainMixins, ordered by base, then by site): the module the top level mixes in, +// and the directive's site. +struct RubyMainMixinRow +{ + NodeId base = kNoNode; + std::uint32_t fileId = 0; + std::uint32_t line = 0; // the constant's 1-based line +}; + struct Binding { NodeId fromSymbol = kNoNode; // enclosing function/method (the binding's scope); kNoNode if file-scope diff --git a/src/packtask.h b/src/packtask.h index edf6e1757..527ca49e9 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -177,6 +177,7 @@ struct PackTaskHeaderParts // must carry the SAME root as the pre-built rootOpenStr did. std::string_view runClause; // M21(b)/E1: testmap.h's run=/run_unknown=/ clause — ROWS-GATED (empty when // the tests section kept no row), so a bundle that omits tests pays nothing + bool softBudget = false; // forbudget-soft: the caller's own budget — a soft target (kPackTaskSoftBudgetSentence) }; // P10 (L7): the bundle legend's BODY, one constant, spliced by the standalone header (packTaskHeaderText) and @@ -242,6 +243,17 @@ inline constexpr const char* kPackTaskBundleLegendBody = // F2: over_ceiling= is now the property those two numbers state, on every rung — not only the // last-rung case the old wording described. Same length class, one clause, unconditional as before. "budget_tokens= is the token target; over_ceiling= is 1 when est_tokens exceeds it (the bundle is then complete, not trimmed). "; +// forbudget-soft (owner decision 2026-10-14): the reading of budget_tokens= on a bundle whose budget the CALLER passed — a SOFT +// target. The section shares are the default's own cascade (an explicit 6000 serves the default's sections byte for byte), but +// nothing is labelled over_ceiling and the header (task echo, route=) is never dropped for it. Replaces the legend body's last +// sentence on such a bundle (packTaskHeaderText); the default (hard) bundle and the partitioned document keep that sentence. +inline constexpr std::string_view kPackTaskHardBudgetSentence = + "budget_tokens= is the token target; over_ceiling= is 1 when est_tokens exceeds it (the bundle is then complete, not trimmed). "; +inline constexpr std::string_view kPackTaskSoftBudgetSentence = + "budget_tokens= is the soft token target asked: each section stops once its share is reached; est_tokens may exceed it " + "(the header always ships; no over_ceiling). "; +static_assert( std::string_view( kPackTaskBundleLegendBody ).ends_with( kPackTaskHardBudgetSentence ), + "the soft bundle swaps exactly the legend body's last sentence" ); // r4/r6 (2026-09-10 lift-disclosure round): append the two experimental lift notes' JSON keys — same // absent-unless-present convention as the mention/boost/doc_mention keys beside them. Factored out (unlike @@ -293,7 +305,18 @@ inline std::string packTaskHeaderText( const PackTaskHeaderParts& p, bool withRo h.append( p.sibliftNote ); h.append( p.expandNote ); h.append( p.docMentionNote ); - h += kPackTaskBundleLegendBody; // P10 (L7): the body is ONE constant — the partitioned document states it once for all slices + // P10 (L7): the body is ONE constant — the partitioned document states it once for all slices. forbudget-soft: a soft + // target's bundle swaps the body's last sentence (the hard budget's reading) for the soft one. + const std::string_view body( kPackTaskBundleLegendBody ); + if( p.softBudget ) + { + h.append( body.substr( 0, body.size() - kPackTaskHardBudgetSentence.size() ) ); + h.append( kPackTaskSoftBudgetSentence ); + } + else + { + h.append( body ); + } h.append( p.runClause ); // M21(b)/E1: the row's run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated h.append( p.report ); h.append( extraNotes ); @@ -549,6 +572,12 @@ struct PackTaskInputs // its own local; nullptr ⇒ a render failure is still returned as empty text, just not recorded. PackTaskRenderFaults* renderFaults = nullptr; + // forbudget-soft (owner decision 2026-10-14): the caller passed this budget (CLI --pack-task --token-budget, MCP explore + // budget_tokens) — a SOFT target. The section cascade is the default's, unchanged; the ceiling ladder does not run (the + // task echo and route= always ship) and nothing is labelled over_ceiling. false: the default budget (kPackTaskDefaultTokens) + // and the --partition slices, which keep the hard ceiling (packtaskcheck 4b, partitioncheck 5). + bool softTarget = false; + // L1 fix round (rv-r1-L1 MED-4): the run's legend posture is compact, so the ceiling ladder's rungs (drop the task echo, // then route=, then label over_ceiling) are judged on the bytes the compact layer DELIVERS — the task echo lives in the // prose the layer strips, and dropping it for a full-dialect price cut a fact the compact answer had room for. The @@ -1998,7 +2027,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // ceiling (the silence-means-nothing-happened convention route/mention/boost use above), never "not // measured". Measured on the FINISHED document, so the key's own bytes are part of what it reports on. constexpr std::size_t kOverCeilingKeyBytes = 22; // `,"over_ceiling":true` + the closing brace - if( j.size() + kOverCeilingKeyBytes > rw::ceilingAllowanceBytes( budgetTokens ) ) + if( !in.softTarget && j.size() + kOverCeilingKeyBytes > rw::ceilingAllowanceBytes( budgetTokens ) ) // forbudget-soft: never on a soft target { j += ",\"over_ceiling\":true"; } @@ -2082,7 +2111,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, const std::string runClauseStr = rw::runHintClauseIfRows( tests.kept, rw::runsAreRootRelative( ing, in.rootArg ) ); // the ONE gate: the section's own kept count const PackTaskHeaderParts headerParts{ task, rootOpenStr, taskNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, report, droppedPositiveAttr, in.rootArg, - runClauseStr }; + runClauseStr, /*softBudget=*/in.softTarget && !in.innerBundle }; const auto buildHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { if( in.innerBundle ) // P10 (L7): a partition slice — the outer legend speaks once for all of them @@ -2146,7 +2175,8 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, std::size_t estTokens = 0; std::string attrs = rw::pricedRootAttr( markupBytes, rw::kBytesPerTokenDefault, bodiesStr.size(), &estTokens ); attrs += " budget_tokens=\"" + std::to_string( budgetTokens ) + "\""; - if( lastRungFired || ( budgetTokens > 0 && estTokens > budgetTokens ) ) { attrs += " over_ceiling=\"1\""; } + // forbudget-soft: a soft target (the caller's own budget) is never "over"; the default budget keeps the label + if( !in.softTarget && ( lastRungFired || ( budgetTokens > 0 && estTokens > budgetTokens ) ) ) { attrs += " over_ceiling=\"1\""; } if( in.isTrailingUncharged ) { attrs += " est_measured=\"0\""; } // defined by the comment spliced in below return attrs; }; @@ -2168,7 +2198,9 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // judging the as-built header at the exact ceiling would take a rung whose note claims a drop the reader cannot // see. Under the compact posture the as-built answer stands when it fits the allowance; route= (a delivered root // attribute) and the over_ceiling rung keep their meaning (compactlegendcheck (FX4)). - const rw::CeilingLadderChoice chosen = in.compactLegend && !in.innerBundle + // forbudget-soft: a soft target climbs no ladder — the header as built ships (echo, route= and every clause) + const rw::CeilingLadderChoice chosen = in.softTarget ? rw::CeilingLadderChoice{ headerStr, rw::CeilingRung::AsBuilt } + : in.compactLegend && !in.innerBundle ? rw::climbCeilingLadderBy( buildHeader, headerStr, deliveredFits( rw::ceilingAllowanceBytes( budgetTokens ) ), deliveredFits( rw::ceilingAllowanceBytes( budgetTokens ) ), /*hasRouteAttr=*/!lr.routeNote.empty(), kNotes ) : climbCeilingLadder( buildHeader, headerStr, diff --git a/src/pincensus.h b/src/pincensus.h index 5add98daf..5b70acf1a 100644 --- a/src/pincensus.h +++ b/src/pincensus.h @@ -115,10 +115,9 @@ enum class CallDisposition : std::uint8_t OtherRoot = 5, // multi-root only: every compatible definition lives in ANOTHER root and no include/import // reaches it — external to this root, which is what that root's solo run would say QualifiedExternal = 6, // a Rust `Scope::name()` call no in-repo member of `Scope` can answer (the H4 W3 guard) - Declined = 7, // tier 3: two or more same-language candidates, none in the caller's file or directory, none - // pinned by a qualifier or a receiver rule; or the builtin-method name gate (graph.h - // BuiltinMethodGate) admitted none of a builtin-type method name's candidates — header - // declined=, answers' declined_calls= + Declined = 7, // tier 3: ambiguous calls with no safe binding; builtin-method name gate refusals; or + // inert quoted call candidates. No edge; all count in header declined=. An inert site + // contributes to its caller's declinedOut, but names no possible target list. FileScope = 8, // a call outside every symbol with no caller node to hang an edge on. Since issue #60 // ingest_model.h mintModuleScopeOwners gives every such call a module-scope owner over // exactly isResolvableCallReference's population below, so on a code corpus this bucket @@ -147,7 +146,7 @@ static_assert( std::string_view( kCallDispositionNames[ std::size_t( CallDisposi // re-derivation, so the two cannot disagree about what a call is — only about what happened to one. inline bool isResolvableCallReference( const Reference& r ) noexcept { - return !r.isInherit && !r.isDocLink && !r.isCompose && ( r.role == RefRole::Call || r.role == RefRole::Macro ); + return !r.isInherit && !r.isDocLink && !r.isCompose && ( r.role == RefRole::Call || r.role == RefRole::Macro || r.role == RefRole::Inert ); } // Per-row provenance bits — every narrowing stage that FIRED on this site, not just the deciding one. A diff --git a/src/planlint.h b/src/planlint.h index 9f2ff1028..ffe4c097c 100644 --- a/src/planlint.h +++ b/src/planlint.h @@ -119,6 +119,7 @@ #include "gitmine.h" // gitRepoToplevel #include "quality.h" // quality::gitBlameConfigPins / quality::gitOneLine — the shared git-blame plumbing #include "gitstamp.h" // gitstamp::stampAt — the at="[+dirty]" anchor +#include "githarden.h" // githarden::hardenLateRoot — FILE's repository is probed like a request-named root #include "serialize.h" // escapeXml / appendCdataSafe #include @@ -385,7 +386,7 @@ inline std::string gitBlameLineSha( const std::string& repoRoot, const std::stri return {}; } const std::string cmd = gitCmd( " -c core.quotepath=false" ) + quality::gitBlameConfigPins( repoRoot ) - + " -C " + shSingleQuote( repoRoot ) + " blame --porcelain -L " + + " -C " + shSingleQuote( repoRoot ) + " blame --no-ext-diff --no-textconv --porcelain -L " + std::to_string( lineNo1 ) + ",+1 HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) @@ -565,6 +566,9 @@ inline LintResult computePlanLint( const std::string& fileArg ) } if( res.gitAvailable ) { + // FILE's repository need not be a crawl root, so main's startup probe never saw it: probe it here, before the first + // git child that reads its config (status for the stamp, blame below), as a server probes a request-named root. + githarden::hardenLateRoot( repoRoot ); res.atStamp = gitstamp::stampAt( repoRoot ); } diff --git a/src/prcontext.h b/src/prcontext.h index 4b7ad300b..08ff9d619 100644 --- a/src/prcontext.h +++ b/src/prcontext.h @@ -220,7 +220,7 @@ inline NumstatDiff numstatChangedPaths( const std::string& root, const std::stri // revision this file builds is a rev-parse-resolved sha, see resolveDiffAnchor), so nothing downstream // can be re-read as an option or as an ambiguous pathspec. Belt to the resolver's braces; an empty // pathspec list after `--` means "all paths", so the diff itself is unchanged. - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --numstat " + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --no-ext-diff --no-textconv --ignore-submodules=dirty --numstat " + revArgs + " -- 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); diff --git a/src/quality.h b/src/quality.h index 3cc0dd208..4a091fc0e 100644 --- a/src/quality.h +++ b/src/quality.h @@ -287,6 +287,74 @@ struct Snapshot std::vector defectSites; // the defect-shape kind — sorted (anchor, site) records, one per offending site (a multiset), plus each scanned file's presence record }; +// ─── THE TWELVE KINDS AS A SET (qd-memory-degrade) ───────────────────────────────────────────────────────── +// --quality-kinds=K[,K] runs a subset of the kinds (the replay a degraded --quality-delta=A..B hands back), and the +// degraded range form runs the subset its changed files can decide. Each kind stays a separately reported, separately +// skippable unit: the mask decides which passes run and which rows are kept, never how a row is computed — a kind that +// runs computes its rows with the same code and the same bars as in a full answer. Alphabetical, the order every list +// of them (kinds_skipped=, the refusal, the help) is spelled in. +inline constexpr std::string_view kQualityKindNames[] = { + "api-surface", "complexity", "dead-code", "defect-shape", "duplication", "error-masking", "nesting", + "new-clone-of-reused-helper", "params", "placeholder", "short-horizon-churn", "verbosity", +}; +using KindMask = std::uint16_t; +inline constexpr KindMask kAllKinds = KindMask( ( 1u << std::size( kQualityKindNames ) ) - 1u ); +static_assert( std::size( kQualityKindNames ) == 12 && std::size( kQualityKindNames ) <= 16, "one bit per kind" ); +// the kind's bit, or 0 for a name that is not one of the twelve +inline constexpr KindMask kindBit( std::string_view kind ) noexcept +{ + const auto* const at = std::find( std::begin( kQualityKindNames ), std::end( kQualityKindNames ), kind ); + return at == std::end( kQualityKindNames ) ? KindMask( 0 ) : KindMask( 1u << ( at - std::begin( kQualityKindNames ) ) ); +} +// the kinds that need EVERY file of the target tree (dead-code: the whole graph's evidence; the two clone kinds: every +// body) — what a changed-files answer cannot run +inline constexpr KindMask kWholeTreeKinds = KindMask( kindBit( "dead-code" ) | kindBit( "duplication" ) | kindBit( "new-clone-of-reused-helper" ) ); +inline constexpr bool kindSelected( KindMask mask, std::string_view kind ) noexcept { return ( mask & kindBit( kind ) ) != 0; } +// the kinds of `mask`, alphabetical, joined by `sep` +inline std::string kindList( KindMask mask, std::string_view sep ) +{ + std::string out; + for( std::size_t k = 0; k < std::size( kQualityKindNames ); ++k ) + { + if( ( mask & ( 1u << k ) ) != 0 ) + { + out.append( out.empty() ? std::string_view{} : sep ).append( kQualityKindNames[ k ] ); + } + } + return out; +} +// What a snapshot or a delta is computed FOR (qd-memory-degrade): the kinds to run, and — when the index holds only some +// of the tree's files (a changed-files answer) — the tree's C-family macro names to scan its C++ files against +// (defectSitesOf). The default is every kind over a whole tree: the computation these functions always did. +struct KindScope +{ + KindMask kinds = kAllKinds; + const std::vector* macroOverride = nullptr; +}; + +// --quality-kinds' value parsed: the mask, or 0 with `bad` naming the first token that is not a kind (an empty token too) +inline KindMask parseKindList( std::string_view spec, std::string& bad ) +{ + KindMask mask = 0; + while( true ) + { + const std::size_t comma = spec.find( ',' ); + const std::string_view token = spec.substr( 0, comma ); + const KindMask bit = kindBit( token ); + if( bit == 0 ) + { + bad.assign( token ); + return 0; + } + mask |= bit; + if( comma == std::string_view::npos ) + { + return mask; + } + spec.remove_prefix( comma + 1 ); + } +} + // Q1 api-surface — the PUBLIC/exported contract surface. DEFINITION (deterministic, documented): // a symbol is PUBLIC iff it is DECLARED IN A HEADER file (`.h/.hpp/.hh/.hxx`) — the C/C++/ObjC export surface // by convention, exactly the convention `isDeadCandidate` already uses ("header-exported by convention"). @@ -1974,16 +2042,16 @@ inline RenameMap gitRenameMap( const std::string& root, const std::string& span // silently dropped the most common rename an agent ever makes — and the gate would still have passed, // because the content route rescues that same ack by a different mechanism. That near-miss is why the // gate asserts the ROUTE (acked_by_rename) and not merely the outcome. - absorb( popenTrimmed( pinned + "diff -M50% --diff-filter=R --name-status --cached 2>/dev/null" ) ); - absorb( popenTrimmed( pinned + "diff -M50% --diff-filter=R --name-status HEAD 2>/dev/null" ) ); - absorb( popenTrimmed( pinned + "log -M50% --diff-filter=R --name-status --format= -n " + absorb( popenTrimmed( pinned + "diff --no-ext-diff --no-textconv --ignore-submodules=dirty -M50% --diff-filter=R --name-status --cached 2>/dev/null" ) ); + absorb( popenTrimmed( pinned + "diff --no-ext-diff --no-textconv --ignore-submodules=dirty -M50% --diff-filter=R --name-status HEAD 2>/dev/null" ) ); + absorb( popenTrimmed( pinned + "log --no-ext-diff --no-textconv -M50% --diff-filter=R --name-status --format= -n " + std::to_string( kRenameWindowCommits ) + " 2>/dev/null" ) ); } else { rm.truncatedWindow = false; // the range IS the scope; nothing outside it was in question rm.commitsScanned = 0; - absorb( popenTrimmed( pinned + "log -M50% --diff-filter=R --name-status --format= " + shSingleQuote( span ) + " 2>/dev/null" ) ); + absorb( popenTrimmed( pinned + "log --no-ext-diff --no-textconv -M50% --diff-filter=R --name-status --format= " + shSingleQuote( span ) + " 2>/dev/null" ) ); } return rm; } @@ -2005,7 +2073,7 @@ inline std::string gitHeadSha( const std::string& root ) // dashes at 4 and 7) — the caller degrades to a fixed epoch date. Read-only: `git log` never mutates the repo. inline std::string gitCommitterDateIso( const std::string& root ) { - std::string out = gitOneLine( root, "log -1 --format=%cs HEAD 2>/dev/null" ); // not const: a const local cannot be moved out on return + std::string out = gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%cs HEAD 2>/dev/null" ); // not const: a const local cannot be moved out on return if( out.size() != 10 || out[4] != '-' || out[7] != '-' ) { return {}; @@ -2052,7 +2120,7 @@ inline bool gitIsAncestor( const std::string& root, const std::string& ancestor, // (HEAD epoch anchors the window — wall-clock is never consulted). "" on any failure (degrade: churn silent). inline std::string gitWindowRefSha( const std::string& root, std::uint32_t days ) { - const std::string epochStr = gitOneLine( root, "log -1 --format=%ct HEAD 2>/dev/null" ); + const std::string epochStr = gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%ct HEAD 2>/dev/null" ); if( epochStr.empty() ) { return {}; @@ -2267,13 +2335,17 @@ 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 = 31; // MUST equal ingest.cpp's kCacheVersion (gated); 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 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 = 173; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestCacheVersionMirror = 32; // MUST equal ingest.cpp's kCacheVersion (gated); 32 = train 28: RefRole Subscript 9 + Inert 10, 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 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 = 176; // MUST equal ingest.cpp's kParserVer (gated) + // 176 = 2026-10-15 (train 28: above main 173/174, grep-carrier 173/174, + // ruby-pair 174/175, #380 145, the other lanes 171; see the kParserVer note) // 173 = 2026-10-15 (slice 4a, #400): Ruby's mixin directives onto the shared // base-relations path — queries/ruby/tags.scm captures them as @reference.mixin // and captureTagsFacts emits the inherit reference; captureRubyMixinBases // retired. Output byte-identical, extraction path changed. 173, not 172: 172 // is 171's rich file tag. + // 173 = 2026-10-14 (train 28 grep-carrier: value refs through ?? || && / or and) + // 174 = lane ruby-pair (train 28: Ruby/Python subscript references, Ruby main-object mixins) // 171 = 2026-10-13 (PR #349 Clojure lands above main's 167 / rich tag 168 and the // branch's own 169 / rich tag 170; see the kParserVer note) // 167 = 2026-10-11 (train 27 release: lane fix27-cpp-subscript's 166 renumbered above the lane build; @@ -3534,7 +3606,13 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // already keeps this build's blobs apart; bumped 19 -> 20 so the history above stays complete and the scheme sits above // every live value (19 on every branch). The lane's own note said the walk itself is a refactor (same class set and // order for the same g.implementors) — true; what moved is g.implementors. -constexpr std::uint32_t kQSnapCacheScheme = 20; +// v21 (2026-10-15, train 28) — no blob shape change; the dead SET's inputs moved again. Lane ruby-pair makes a proven element +// access a call of the receiver class's subscript operator (Ruby `[]`/`[]=`, Python `__getitem__`/`__setitem__`) and PR #380 +// makes an Elixir `unquote(...)` call under `quote` live, so the calls that keep a definition alive can differ for an +// UNCHANGED sha. kParserVer moved with those extractions (176, its mirror with it), and the producer identity already keeps +// this build's blobs apart; bumped 20 -> 21 so the history above stays complete and the scheme sits above every live value +// (20 on main and every lane). Lane qd-memory-degrade's KindScope builds the default scope's records unchanged (its note). +constexpr std::uint32_t kQSnapCacheScheme = 21; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) @@ -4189,7 +4267,8 @@ inline RefSpec resolveRefSpec( const std::string& root, std::string_view spec ) // a failed archive/extract/ingest → returns {snapshot, false}. rootPath is shell-escaped (shSingleQuote) and // quotepath=false — no injection, deterministic path handling. inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, - const std::vector* fileInBaseline = nullptr ); // fwd — defined below; computeHeadSnapshot reuses it + const std::vector* fileInBaseline = nullptr, // fwd — defined below; computeHeadSnapshot reuses it + const KindScope& scope = KindScope() ); // no braces here: qschemetripcheck's extractor reads them as a body // `excludes` MUST mirror the working-tree side's cfg.excludes (A4-F5): the HEAD snapshot is compared key-for-key // against the working tree, and any in-edge-derived kind (dead-code, api-surface) diverges if one side honors // --exclude and the other does not — e.g. a helper called only from tests/ is dead on a --exclude=tests working @@ -4393,6 +4472,315 @@ inline RefTreeLoad loadRefTree( const std::string& repoRoot, const std::string& return RefTreeLoad::Loaded; } +// ─── qd-memory-degrade: a commit pair's CHANGED FILES, and their trees ───────────────────────────────────────── +// When the memory guard stops either commit's full tree, or projects that the target phase will not fit, the range form +// re-asks the question of each commit's CHANGED files only. That answer is exact for a kind only if every file that +// could yield a row of it is in the set, and is materialized exactly as the full archive materializes it. So the set is +// the paths `git diff --no-renames` names (per side, those that exist there as files), plus the control files the crawl +// reads to decide those paths, and the pair is REFUSED by name when an archive or a crawl would treat a path git does not +// list differently: +// * a symlink anywhere in either tree — the crawl follows in-root links, so an UNCHANGED link to a changed file rows +// the link's path too (and a changed link to an unchanged file dangles in a partial tree); +// * an export-subst attribute in either tree — `git archive` rewrites the bytes of files git calls unchanged; +// * a changed .gitattributes that carries an export attribute on either side — an export-ignore flip adds or removes +// files the diff does not name. +// The one crawl verdict decided by another file is the build-tree prune: a directory holding CMakeCache.txt is skipped +// whole (ingest_crawl.h), so a changed path under such a directory brings that CMakeCache.txt along. Nothing else in +// the crawl of a materialized tree reads another file: .gitignore is consulted through `git ls-files`, which a temp +// tree does not answer, and .ripwire_config feeds only dead-code (a skipped kind). What it does not cover, stated: +// attributes from $GIT_DIR/info/attributes or core.attributesFile (outside the tree, so the same on both sides). +struct ChangedFiles +{ + std::vector base; // root-relative paths to materialize at the base commit (sorted, unique) + std::vector target; // ... at the target commit + std::vector targetTree; // every file of the target commit, sorted — filled only when asked (--scope's test) + std::string refusal; // non-empty: why the changed files cannot stand in for the trees (the line names it) + bool ok = false; // git answered every question this needed + enum class DisclosureWhy : std::uint8_t + { + GitFailed, // a diff, ls-tree or export-attribute grep over the pair did not complete: the set is unknown, and the caller refuses + }; + void disclose( DisclosureWhy ) noexcept { ok = false; } +}; + +// Stream one git command's NUL-separated records into `onRecord`; true when git exited 0, or with the one other exit code +// `alsoClean` names (git grep's 1 = no hit). Never holds the whole listing: on a large tree `ls-tree -r` is tens of +// megabytes, and this runs exactly when memory is short. +template +inline bool forEachGitNulRecord( const std::string& root, const std::string& args, OnRecord&& onRecord, int alsoClean = 0 ) +{ + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " " + args + " 2>/dev/null"; + std::FILE* pipe = os::popen( cmd.c_str(), "r" ); + if( pipe == nullptr ) + { + return false; + } + std::string record; + char buf[ 16384 ]; + std::size_t got = 0; + while( ( got = std::fread( buf, 1, sizeof( buf ), pipe ) ) > 0 ) + { + for( std::size_t at = 0; at < got; ) + { + const char* nul = static_cast( std::memchr( buf + at, '\0', got - at ) ); + const std::size_t end = nul == nullptr ? got : std::size_t( nul - buf ); + record.append( buf + at, end - at ); + if( nul == nullptr ) + { + break; + } + onRecord( std::string_view( record ) ); + record.clear(); + at = end + 1; + } + } + if( !record.empty() ) + { + onRecord( std::string_view( record ) ); + } + const int status = os::pclose( pipe ); + return status == 0 || ( alsoClean != 0 && decodedExitCode( status ) == alsoClean ); +} + +// The changed files of `baseSha`..`targetSha` (two resolved commit names), with the refusals above decided. +inline ChangedFiles changedFilesBetween( const std::string& root, const std::string& baseSha, const std::string& targetSha, bool wantTargetTree ) +{ + ChangedFiles out; + // 1. what git says changed: "\0\0" pairs; --no-renames reports a move as its delete and its add + std::vector changed; + std::vector statusOf; + { + bool wantPath = false; + char status = 0; + const bool ran = forEachGitNulRecord( root, "diff --no-renames --no-ext-diff --no-textconv --name-status -z " + shSingleQuote( baseSha ) + " " + shSingleQuote( targetSha ), + [ & ]( std::string_view rec ) + { + if( !wantPath ) + { + status = rec.empty() ? 0 : rec.front(); + wantPath = true; + return; + } + changed.emplace_back( rec ); + statusOf.push_back( status ); + wantPath = false; + } ); + if( !ran ) + { + DISCLOSE( out, ChangedFiles::DisclosureWhy::GitFailed, "quality: git diff over the commit pair failed — the changed files are unknown" ); + return out; + } + } + // 2. each side's tree: which changed paths exist there as FILES (a submodule's gitlink is no file), the build-tree + // sentinels, the attribute files, and any symlink at all. One streamed `ls-tree -r` per side. + std::vector order( changed.size() ); + for( std::size_t k = 0; k < order.size(); ++k ) { order[k] = k; } + // rw::sortutil::svLess, not operator<: libstdc++'s string_view compare trips -fsanitize=integer (splitNulPaths' note) + std::sort( order.begin(), order.end(), [ & ]( std::size_t a, std::size_t b ) { return rw::sortutil::svLess( changed[a], changed[b] ); } ); + const auto changedIndex = [ & ]( std::string_view path ) -> std::size_t + { + const auto it = std::lower_bound( order.begin(), order.end(), path, [ & ]( std::size_t k, std::string_view p ) { return rw::sortutil::svLess( changed[k], p ); } ); + return ( it != order.end() && changed[ *it ] == path ) ? *it : SIZE_MAX; + }; + const auto readSide = [ & ]( const std::string& sha, std::vector& existsHere, std::vector& cmakeDirs, + std::vector* tree ) -> bool + { + existsHere.assign( changed.size(), 0 ); + return forEachGitNulRecord( root, "ls-tree -r -z --full-tree " + shSingleQuote( sha ), [ & ]( std::string_view rec ) + { + const std::size_t tab = rec.find( '\t' ); + if( tab == std::string_view::npos || tab < 7 ) + { + return; + } + const std::string_view mode = rec.substr( 0, 6 ); + const std::string_view path = rec.substr( tab + 1 ); + if( mode == "120000" && out.refusal.empty() ) + { + out.refusal = "the commit " + sha.substr( 0, 12 ) + " holds a symlink (" + std::string( path ) + "), which the crawl follows into files git diff does not name"; + } + const bool isFile = mode == "100644" || mode == "100755"; + if( isFile && ( path == "CMakeCache.txt" || ( path.size() > 15 && path.substr( path.size() - 15 ) == "/CMakeCache.txt" ) ) ) + { + cmakeDirs.emplace_back( path.substr( 0, path.size() - 14 ) ); // the directory with its trailing '/', "" for the root + } + if( const std::size_t k = changedIndex( path ); k != SIZE_MAX && isFile ) + { + existsHere[k] = 1; + } + if( tree != nullptr && isFile ) + { + tree->emplace_back( path ); + } + } ); + }; + std::vector atBase, atTarget; + std::vector cmakeBase, cmakeTarget; + if( !readSide( baseSha, atBase, cmakeBase, nullptr ) || !readSide( targetSha, atTarget, cmakeTarget, wantTargetTree ? &out.targetTree : nullptr ) ) + { + DISCLOSE( out, ChangedFiles::DisclosureWhy::GitFailed, "quality: git ls-tree over a commit of the pair failed — the changed files are unknown" ); + return out; + } + // 3. export attributes: ":\0\n" per hit (exit 1 = no hit, a clean answer; any other exit — a bad + // object, git missing — is a failed read, never "no attribute") + const auto exportAttrs = [ & ]( const std::string& sha, std::vector& files ) -> bool + { + std::string text; + bool first = true; + const bool ran = forEachGitNulRecord( root, "grep -I -z -o -e export-subst -e export-ignore " + shSingleQuote( sha ) + " -- " + shSingleQuote( "*.gitattributes" ), + [ & ]( std::string_view rec ) + { + if( !first ) + { + text += '\0'; // the records are rejoined: the parse below reads the line shape + } + first = false; + text.append( rec ); + }, + /*alsoClean=*/1 ); + if( !ran ) + { + return false; + } + for( std::size_t at = 0; at < text.size(); ) + { + const std::size_t nl = std::min( text.find( '\n', at ), text.size() ); + const std::string_view line( text.data() + at, nl - at ); + const std::size_t nul = line.find( '\0' ); + if( nul != std::string_view::npos && line.size() > sha.size() + 1 ) + { + const std::string_view path = line.substr( sha.size() + 1, nul - sha.size() - 1 ); + const std::string_view match = line.substr( nul + 1 ); + if( match == "export-subst" && out.refusal.empty() ) + { + out.refusal = "the commit " + sha.substr( 0, 12 ) + " carries an export-subst attribute (" + std::string( path ) + "), which rewrites files git diff calls unchanged"; + } + files.emplace_back( path ); + } + at = nl + 1; + } + return true; + }; + std::vector attrFiles; + if( !exportAttrs( baseSha, attrFiles ) || !exportAttrs( targetSha, attrFiles ) ) + { + DISCLOSE( out, ChangedFiles::DisclosureWhy::GitFailed, "quality: git grep for export attributes over a commit of the pair failed — the changed files are unknown" ); + return out; + } + for( const std::string& p : changed ) + { + if( out.refusal.empty() && std::find( attrFiles.begin(), attrFiles.end(), p ) != attrFiles.end() ) + { + out.refusal = "the pair changes " + p + ", which carries an export attribute (an export-ignore flip adds or removes files git diff does not name)"; + } + } + // 4. the per-side lists, each changed path that is a file there, plus the CMakeCache.txt of any ancestor directory + std::sort( cmakeBase.begin(), cmakeBase.end() ); + std::sort( cmakeTarget.begin(), cmakeTarget.end() ); + const auto addWithSentinels = [ & ]( const std::string& path, const std::vector& cmakeDirs, std::vector& list ) + { + list.push_back( path ); + for( std::size_t slash = path.find( '/' ); slash != std::string::npos; slash = path.find( '/', slash + 1 ) ) + { + const std::string dir = path.substr( 0, slash + 1 ); + if( std::binary_search( cmakeDirs.begin(), cmakeDirs.end(), dir ) ) + { + list.push_back( dir + "CMakeCache.txt" ); + } + } + }; + for( std::size_t k = 0; k < changed.size(); ++k ) + { + if( atBase[k] ) + { + addWithSentinels( changed[k], cmakeBase, out.base ); + } + if( atTarget[k] ) + { + addWithSentinels( changed[k], cmakeTarget, out.target ); + } + } + for( std::vector* list : { &out.base, &out.target } ) + { + std::sort( list->begin(), list->end() ); + list->erase( std::unique( list->begin(), list->end() ), list->end() ); + } + std::sort( out.targetTree.begin(), out.targetTree.end() ); + out.ok = true; + return out; +} + +// Materialize `paths` of `sha` (already-resolved, each a file there) into a fresh temp tree: the same archive-then-extract +// discipline as materializeCommitTree, in chunks so no command line grows with the change. Each path is a :(literal) +// pathspec, so a name with glob characters is itself. An empty list is an empty tree (an empty diff is a legal question). +// Returns the temp root, or "" on any failure (the guard the caller passes removes a partial tree). +inline std::string materializeCommitSubset( const std::string& root, const std::string& sha, const std::vector& paths, + const char* tag, TmpTreeGuard& guard ) +{ + namespace fs = std::filesystem; + std::error_code ec; + const std::string tmpRoot = cacheDirLadder() + "/ripwire-" + tag + "-" + std::to_string( os::getpid() ); + fs::remove_all( fs::path( tmpRoot ), ec ); + if( !fs::create_directories( fs::path( tmpRoot ), ec ) && ec ) + { + DISCLOSE( Diagnostics::answerRefused, "the range form refuses with the sha named; nothing is scored", "quality: cannot create the changed-files temp dir" ); + return {}; + } + guard.p = tmpRoot; + const std::string archiveFile = tmpRoot + ".tar"; + constexpr std::size_t kChunkBytes = 64 * 1024; // well under every ARG_MAX this builds on + for( std::size_t at = 0; at < paths.size(); ) + { + std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " archive --format=tar --output=" + shSingleQuote( archiveFile ) + + " " + shSingleQuote( sha ) + " --"; + const std::size_t first = at; + while( at < paths.size() && ( at == first || cmd.size() + paths[at].size() < kChunkBytes ) ) + { + cmd += " " + shSingleQuote( ":(literal)" + paths[at] ); + ++at; + } + const bool archived = os::system( ( cmd + " 2>/dev/null" ).c_str() ) == 0; + const bool extracted = archived && os::system( ( "tar -x -f " + shSingleQuote( archiveFile ) + " -C " + shSingleQuote( tmpRoot ) + " 2>/dev/null" ).c_str() ) == 0; + fs::remove( fs::path( archiveFile ), ec ); + if( !extracted ) + { + DISCLOSE( Diagnostics::answerRefused, "the range form refuses with the sha named; nothing is scored", "quality: archiving a commit's changed files failed" ); + return {}; + } + } + return tmpRoot; +} + +// The changed files of one commit, ingested and graphed — loadRefTree's shape with three differences: never through an +// ingest cache (a subset must not read a full-tree blob or write one under the commit's key — checklist 21), an empty +// subset is a legal answer, and `tag` names it apart from the full trees. +inline RefTreeLoad loadRefSubset( const std::string& repoRoot, const std::string& sha, const std::vector& paths, + const std::vector& excludes, std::size_t maxFileBytes, const char* tag, + TmpTreeGuard& guard, RefTree& out ) +{ + ASSUME( tag != nullptr && *tag != '\0' ); + const std::string tmpRoot = materializeCommitSubset( repoRoot, sha, paths, tag, guard ); + if( tmpRoot.empty() ) + { + return RefTreeLoad::Unavailable; + } + { + std::lock_guard ingestLk( headSnapshotIngestMutex() ); + out.ing = ingest( tmpRoot.c_str(), excludes, std::string_view {}, maxFileBytes ); + } + if( out.ing.memoryStop.isSet() ) + { + return RefTreeLoad::MemoryStopped; + } + if( memguard::overHardLimit() ) + { + return RefTreeLoad::OverHardLimit; + } + out.g = buildGraph( out.ing, nullptr ); + out.root = tmpRoot; + return RefTreeLoad::Loaded; +} + // Signal-to-noise round — the committed-thrash evidence for short-horizon-churn: the per-canonId RAW-body // hashes of the tree at the churn-window reference commit (gitWindowRefSha). A symbol whose baseline (HEAD) // body differs from this ref's body — or that is absent here but present at HEAD — was already written/ @@ -4798,22 +5186,34 @@ inline std::vector baselineCloneGroupHashes( const IngestResult& // 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 ) +// format-arity's F1 skip input: every macro NAME the index holds in a C-family file, tree-wide (a call in one file may +// use a macro another file defines). Sorted, unique: the scanner binary-searches it. Also what a degraded +// --quality-delta=A..B keeps of the base commit's full tree (names only) so the changed files are scanned against the +// tree's set rather than their own (qd-memory-degrade). +inline std::vector cFamilyMacroNames( const IngestResult& ing ) { - std::vector out; - const SymbolsByFile byFile = symbolsByFileInIdOrder( ing, []( const Symbol& s ) { return s.endByte > s.sigStartByte && s.kind != SymKind::ModuleScope; } ); - // format-arity's F1 skip: every macro the index holds in a C-family file, tree-wide (a call in one file may use - // a macro another file defines). Sorted, unique: the scanner binary-searches it. - std::vector treeMacros; + std::vector names; for( const Symbol& s : ing.symbols ) { if( s.kind == SymKind::Macro && ( s.lang == Lang::Cpp || s.lang == Lang::C || s.lang == Lang::ObjC ) ) { - treeMacros.push_back( s.name ); + names.push_back( s.name ); } } - std::sort( treeMacros.begin(), treeMacros.end() ); - treeMacros.erase( std::unique( treeMacros.begin(), treeMacros.end() ), treeMacros.end() ); + std::sort( names.begin(), names.end() ); + names.erase( std::unique( names.begin(), names.end() ), names.end() ); + return names; +} + +// `macroOverride`, when given, is the macro-name set to scan C++ files against instead of this index's own — the whole +// tree's, when this index holds only some of its files (a degraded --quality-delta=A..B's changed files). +inline std::vector defectSitesOf( const IngestResult& ing, std::string_view root, const std::vector* fileInBaseline, + const std::vector* macroOverride = nullptr ) +{ + std::vector out; + const SymbolsByFile byFile = symbolsByFileInIdOrder( ing, []( const Symbol& s ) { return s.endByte > s.sigStartByte && s.kind != SymKind::ModuleScope; } ); + const std::vector ownMacros = macroOverride == nullptr ? cFamilyMacroNames( ing ) : std::vector{}; + const std::vector& treeMacros = macroOverride == nullptr ? ownMacros : *macroOverride; for( std::uint32_t f = 0; f < ing.files.size(); ++f ) { if( !fileIsInBaseline( fileInBaseline, f ) ) @@ -4873,17 +5273,72 @@ inline std::vector defectSitesOf( const IngestResult& ing, std::st return out; } -inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, - const std::vector* fileInBaseline ) +// The dead set of a snapshot: every dead-code candidate the --dead-code verb's rule leaves dead, keyed like every per-symbol +// record. Its own pass (and its four evidence structures) so a snapshot computed for kinds without dead-code skips it. +inline void addDeadSet( Snapshot& snap, const IngestResult& ing, const Graph& g, std::string_view root, const std::vector* fileInBaseline ) { - Snapshot snap; - const std::vector codeLoc = codeLocByNode( ing ); // Q-DIAL-3: the verbosity kind's metric is CODE lines const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); const std::vector pythonDispatch = pythonDispatchedMethodIds( ing, g ); const ValueRefIndex valueRefs( ing ); // a value-held function is not dead for( NodeId i = 0; i < ing.symbols.size(); ++i ) + { + if( i >= g.canonId.size() || g.canonId[i].empty() || !fileIsInBaseline( fileInBaseline, ing.symbols[i].fileId ) ) + { + continue; + } + if( isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch ) && !declinedCallMayReach( g, i ) + && !valueRefs.isValueReferenced( i ) ) + { + snap.dead.push_back( qualityKey( ing, i, root ) ); + } + } +} + +// The snapshot's whole-file passes, each only for the kinds it feeds (see computeSnapshot): clone groups, construct +// counts, defect sites, body hashes. +inline void addKindPasses( Snapshot& snap, const IngestResult& ing, std::string_view root, const std::vector* fileInBaseline, const KindScope& scope ) +{ + if( ( scope.kinds & ( kindBit( "duplication" ) | kindBit( "new-clone-of-reused-helper" ) ) ) != 0 ) + { + snap.cloneGroups = baselineCloneGroupHashes( ing, root, fileInBaseline ); + } + // §D#4 error-masking + placeholder baseline: per-canonId counts (the SUMs the delta compares), from one pass. + if( ( scope.kinds & ( kindBit( "error-masking" ) | kindBit( "placeholder" ) ) ) != 0 ) + { + ConstructCounts counts = constructCountsBySym( ing, root ); + snap.maskBySym = std::move( counts.mask ); + snap.maskReportBySym = std::move( counts.maskReport ); + 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). + if( kindSelected( scope.kinds, "defect-shape" ) ) + { + const std::vector sites = defectSitesOf( ing, root, fileInBaseline, scope.macroOverride ); + 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. + if( kindSelected( scope.kinds, "short-horizon-churn" ) ) + { + snap.bodyHashBySym = bodyHashesBySym( ing, root ); + } +} + +// `scope` (qd-memory-degrade): the kinds this snapshot will be compared for. A pass that feeds only unselected kinds is +// not run and its records stay empty — the dead set without dead-code, the clone groups without either clone kind, the +// construct counts without error-masking or placeholder, the defect sites without defect-shape, the body hashes without +// short-horizon-churn. The per-symbol metric maps always run (locBySym is every kind's existence oracle). The default +// scope is every pass, the snapshot this function has always returned; its macroOverride: see defectSitesOf. +inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, + const std::vector* fileInBaseline, const KindScope& scope ) +{ + Snapshot snap; + const std::vector codeLoc = codeLocByNode( ing ); // Q-DIAL-3: the verbosity kind's metric is CODE lines + for( NodeId i = 0; i < ing.symbols.size(); ++i ) { if( i >= g.canonId.size() || g.canonId[i].empty() ) { @@ -4909,37 +5364,16 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s // (editcheck.h). A COUNT is overload-collision-proof for the opposite reason a MAX is: it is the one // number a collision cannot hide. (maskBySym is the other non-MAX kind; it sums for its own reason.) { std::uint32_t& slot = snap.defsBySym[ key ]; slot += 1; } - if( isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch ) && !declinedCallMayReach( g, i ) - && !valueRefs.isValueReferenced( i ) ) - { - snap.dead.push_back( key ); - } if( isPublicApi( ing, i ) ) { snap.publicApi.push_back( key ); } } - snap.cloneGroups = baselineCloneGroupHashes( ing, root, fileInBaseline ); - - // §D#4 error-masking + placeholder baseline: per-canonId counts (the SUMs the delta compares), from one pass. - { - ConstructCounts counts = constructCountsBySym( ing, root ); - snap.maskBySym = std::move( counts.mask ); - snap.maskReportBySym = std::move( counts.maskReport ); - 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). + if( kindSelected( scope.kinds, "dead-code" ) ) { - const std::vector sites = defectSitesOf( ing, root, fileInBaseline ); - snap.defectSites.resize( sites.size() ); - std::ranges::transform( sites, snap.defectSites.begin(), &DefectSiteAt::key ); + addDeadSet( snap, ing, g, root, fileInBaseline ); // sorted below with the rest; its order of insertion never reaches a reader } - - // §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 ); + addKindPasses( snap, ing, root, fileInBaseline, scope ); dropNonBaselineKeys( snap, ing, root, fileInBaseline ); // IDENTITY BASIS membership, on the two key-built maps @@ -5018,7 +5452,7 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s // (not a repo, unborn HEAD, git absent) reads as "not identical" and falls back to the archived baseline. inline bool workingTreeMatchesHead( const std::string& root ) { - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --quiet HEAD -- . >/dev/null 2>&1"; + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --no-ext-diff --no-textconv --ignore-submodules=dirty --quiet HEAD -- . >/dev/null 2>&1"; return os::system( cmd.c_str() ) == 0; } @@ -5932,7 +6366,7 @@ inline void gitBlameRangeWindowCommits( const std::string& root, const std::stri return; } const std::string cmd = gitCmd( " -c core.quotepath=false" ) + gitBlameConfigPins( root ) + " -C " + shSingleQuote( root ) - + " blame --porcelain -L " + std::to_string( startLine ) + ",+" + std::to_string( lineCount ) + + " blame --no-ext-diff --no-textconv --porcelain -L " + std::to_string( startLine ) + ",+" + std::to_string( lineCount ) + " HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) @@ -6006,7 +6440,7 @@ inline std::vector gitDiffHunksVsHead( const std::string& root, const { std::vector hunks; const std::string cmd = gitCmd( " -c core.quotepath=false -c diff.algorithm=myers -C " ) + shSingleQuote( root ) - + " diff --no-ext-diff --unified=0 --no-color HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; + + " diff --no-ext-diff --no-textconv --ignore-submodules=dirty --unified=0 --no-color HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) { DISCLOSE( "quality: churn hunk diff could not be spawned" ); return hunks; } @@ -7805,9 +8239,14 @@ struct IdentityHealing // where the judged tree is a materialized temp dir with no git history at all: passing one root there would // silently lose every rename in the very wave being measured, which is the failure mode this round exists to // end. Collapsing them into one parameter is therefore a bug waiting for the next ref-pair caller. +// `treeWideRescues` (qd-memory-degrade): false when `ing` holds only the changed files. The scheme re-key and the content-id +// rescue both refuse an identity that is ambiguous ACROSS THE TREE (two files defining one scope-less name, two equal +// bodies), and a partial index cannot see that ambiguity — it would rescue what the full run refuses. So both are skipped +// there (and said: also_skipped= names ack-healing); the git rename route is per-path and runs as always. inline IdentityHealing healIdentity( Snapshot& base, gtl::btree_map& acks, const IngestResult& ing, const Graph& g, const std::string& corpusRoot, - const std::string& gitRoot, bool wantContentIds, const std::string& span = {} ) + const std::string& gitRoot, bool wantContentIds, const std::string& span = {}, + bool treeWideRescues = true ) { IdentityHealing h; if( acks.empty() && !wantContentIds ) @@ -7819,7 +8258,10 @@ inline IdentityHealing healIdentity( Snapshot& base, gtl::btree_map scopeDisclosure( const Scope& scope, // 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 ) + std::string_view root, ExistedFn&& existedAtBaseline, StampFn&& stampLoc, + const std::vector* macroOverride = nullptr ) { - const std::vector nowSites = defectSitesOf( ing, root, nullptr ); + const std::vector nowSites = defectSitesOf( ing, root, nullptr, macroOverride ); const auto isRealSite = []( std::uint64_t v ) { return v != kDefectFilePresent && v != kDefectFileUnreadable; }; std::vector baseVals, nowVals, baseUnreadable; for( const DefectSiteKey& d : base.defectSites ) @@ -8541,6 +8984,64 @@ inline void appendDefectShapeRows( std::vector& regs, const IngestRe } } +// The dead-code kind's delta pass: a dead candidate the --dead-code verb's rule leaves dead, not dead at the baseline. Its +// own function (qd-memory-degrade) so computeDelta runs it, and builds its four evidence structures, only when the kind is +// asked for. The three exclusion counts are the disclosed floors computeDelta's caller prints (any may be null). +struct DeadExclusionCounts +{ + std::size_t* registerMacro = nullptr; // P2.2: would be dead-code but for the register-macro exemption + std::size_t* declinedCall = nullptr; // ... but for a declined call that may reach it + std::size_t* valueRef = nullptr; // ... but for a table/field/argument holding it as a value +}; +template +inline void appendDeadCodeRows( std::vector& regs, const IngestResult& ing, const Graph& g, const Snapshot& base, std::string_view root, + const std::vector& keyByNode, ExistedFn&& existedAtBaseline, StampFn&& stampLoc, DeadExclusionCounts excluded ) +{ + const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once + const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config + const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); + const std::vector pythonDispatch = pythonDispatchedMethodIds( ing, g ); + const ValueRefIndex valueRefs( ing ); // reference-as-value round: the --dead-code verb's rule + for( NodeId i = 0; i < ing.symbols.size(); ++i ) + { + if( i >= g.canonId.size() || g.canonId[i].empty() ) + { + continue; + } + bool macroExempt = false; + if( !isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch, ¯oExempt ) ) + { + if( macroExempt && excluded.registerMacro ) + { + ++( *excluded.registerMacro ); // P2.2: would be dead-code but for the macro exemption — disclosed count + } + continue; + } + if( declinedCallMayReach( g, i ) ) + { + if( excluded.declinedCall ) + { + ++( *excluded.declinedCall ); // would be dead-code but for a declined call that may reach it — disclosed count + } + continue; + } + if( valueRefs.isValueReferenced( i ) ) // checked LAST: one entity, one reason (the --dead-code verb's order) + { + if( excluded.valueRef ) + { + ++( *excluded.valueRef ); // a table, field or argument holds it as a value — disclosed count + } + continue; + } + if( !std::binary_search( base.dead.begin(), base.dead.end(), keyByNode[i] ) ) + { + regs.push_back( { "dead-code", g.canonId[i], 0, 0, keyByNode[i], false, {}, + !existedAtBaseline( keyByNode[i] ) } ); // origin: born uncalled (new) vs lost its last caller (preexisting) + stampLoc( i ); + } + } +} + // 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 @@ -8565,7 +9066,8 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap std::size_t* apiNewSurfaceOut = nullptr, // Q-DIAL-4: the api-surface new-symbol COUNT that replaced N never-gating rows std::vector* cloneIdiomsOut = nullptr, // every CURRENT-tree clone group's (hash, idiom), for the legacy-ack backfill std::size_t* declinedCallExcludedOut = nullptr, // symbols kept out of dead-code ONLY by a declined call (declinedCallMayReach) - std::size_t* valueRefExcludedOut = nullptr ) // ... ONLY because a table/field/argument holds them as a VALUE + std::size_t* valueRefExcludedOut = nullptr, // ... ONLY because a table/field/argument holds them as a VALUE + const KindScope& scope = {} ) // qd-memory-degrade: the kinds to report (see computeSnapshot) { if( declinedCallExcludedOut ) { @@ -8837,8 +9339,11 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // ran TWICE per call (~46 % of the whole verb was redundant recompute). Compute each pass ONCE here and let // both consumers read the same vectors. Byte-identical BY CONSTRUCTION: both functions are deterministic // pure functions, so their single result is field-for-field the value both call sites received before. - const std::vector exactClones = findClones( ing, int( kMinCloneTokens ) ); - const std::vector type3Clones = findClonesType3( ing, int( kMinCloneTokens ) ); + // qd-memory-degrade: the clone passes feed only the two clone kinds (and the --quality-ack backfill export, which is + // never asked with a kind mask) — not run when neither is selected, the largest saving a kind selection buys. + const bool wantClones = ( scope.kinds & ( kindBit( "duplication" ) | kindBit( "new-clone-of-reused-helper" ) ) ) != 0 || cloneIdiomsOut != nullptr; + const std::vector exactClones = wantClones ? findClones( ing, int( kMinCloneTokens ) ) : std::vector{}; + const std::vector type3Clones = wantClones ? findClonesType3( ing, int( kMinCloneTokens ) ) : std::vector{}; // duplication: a clone group (exact Type-1/2 OR gapped Type-3) whose member-set is not in the // baseline set. Both passes hash by the same sorted-member-canonId identity, so a change that @@ -8999,48 +9504,10 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap reportNewClones( exactClones, exactIdioms ); reportNewClones( type3Clones, type3Idioms ); - const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once - const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config - const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); - const std::vector pythonDispatch = pythonDispatchedMethodIds( ing, g ); - const ValueRefIndex valueRefs( ing ); // reference-as-value round: the --dead-code verb's rule - for( NodeId i = 0; i < ing.symbols.size(); ++i ) + if( kindSelected( scope.kinds, "dead-code" ) ) // qd-memory-degrade: the pass and its four evidence structures, only for its kind { - if( i >= g.canonId.size() || g.canonId[i].empty() ) - { - continue; - } - bool macroExempt = false; - if( !isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch, ¯oExempt ) ) - { - if( macroExempt && registerMacroExcludedOut ) - { - ++( *registerMacroExcludedOut ); // P2.2: would be dead-code but for the macro exemption — disclosed count - } - continue; - } - if( declinedCallMayReach( g, i ) ) - { - if( declinedCallExcludedOut ) - { - ++( *declinedCallExcludedOut ); // would be dead-code but for a declined call that may reach it — disclosed count - } - continue; - } - if( valueRefs.isValueReferenced( i ) ) // checked LAST: one entity, one reason (the --dead-code verb's order) - { - if( valueRefExcludedOut ) - { - ++( *valueRefExcludedOut ); // a table, field or argument holds it as a value — disclosed count - } - continue; - } - if( !std::binary_search( base.dead.begin(), base.dead.end(), keyByNode[i] ) ) - { - regs.push_back( { "dead-code", g.canonId[i], 0, 0, keyByNode[i], false, {}, - !existedAtBaseline( keyByNode[i] ) } ); // origin: born uncalled (new) vs lost its last caller (preexisting) - stampLoc( i ); - } + appendDeadCodeRows( regs, ing, g, base, root, keyByNode, existedAtBaseline, stampLoc, + DeadExclusionCounts{ registerMacroExcludedOut, declinedCallExcludedOut, valueRefExcludedOut } ); } // api-surface (contract drift) — B10.2e TIERED into two shapes on the PUBLIC/exported surface: @@ -9178,7 +9645,9 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // code that did not exist at the baseline — so it is marked origin="new-symbol" on every row and never // gates, whatever symbol it lands in (the same way short-horizon-churn is preexisting by construction). { - const ConstructCounts nowCounts = constructCountsBySym( ing, root ); + // qd-memory-degrade: the construct pass only for its two kinds — without them every count reads 0 and no row is made + const ConstructCounts nowCounts = ( scope.kinds & ( kindBit( "error-masking" ) | kindBit( "placeholder" ) ) ) != 0 + ? constructCountsBySym( ing, root ) : ConstructCounts{}; const auto countIn = []( const gtl::btree_map& m, std::uint64_t key ) { const auto it = m.find( key ); @@ -9216,7 +9685,10 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap } } - appendDefectShapeRows( regs, ing, g, base, root, existedAtBaseline, stampLoc ); // the twelfth kind (see its doc) + if( kindSelected( scope.kinds, "defect-shape" ) ) + { + appendDefectShapeRows( regs, ing, g, base, root, existedAtBaseline, stampLoc, scope.macroOverride ); // 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): @@ -9236,7 +9708,9 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // Git access uses computeDelta's own `root` (= cfg.rootPath on the CLI, the real repo root over MCP); // no git / no HEAD / no obtainable window ref → this kind simply reports nothing (degrade, precision-first). { - const std::vector commitCounts = gitFileCommitCountsInDayWindow( std::string( root ), ing, kShortHorizonDays ); + // qd-memory-degrade: no window mining when the kind is not asked for (no count reaches the threshold, so no row) + const std::vector commitCounts = kindSelected( scope.kinds, "short-horizon-churn" ) + ? gitFileCommitCountsInDayWindow( std::string( root ), ing, kShortHorizonDays ) : std::vector{}; const bool anyChurn = std::any_of( commitCounts.begin(), commitCounts.end(), []( std::uint32_t c ){ return c >= kShortHorizonMinCommits; } ); if( anyChurn ) @@ -9259,7 +9733,7 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // degrades every symbol below to AMBIENT (churnEditWindowCommitCount is gated on `> 0`). std::int64_t churnCutoffEpoch = 0; { - const std::string epochStr = gitOneLine( std::string( root ), "log -1 --format=%ct HEAD 2>/dev/null" ); + const std::string epochStr = gitOneLine( std::string( root ), "log --no-ext-diff --no-textconv -1 --format=%ct HEAD 2>/dev/null" ); if( !epochStr.empty() ) { const std::int64_t headEpoch = std::strtoll( epochStr.c_str(), nullptr, 10 ); @@ -9451,6 +9925,13 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap reportReusedClones( type3Clones, type3Idioms ); } + // qd-memory-degrade: only the selected kinds' rows. A pass shared by two kinds (error-masking and placeholder, the + // two clone kinds) or read by another kind (params, which api-surface's one-fact-one-row rule reads) ran for the + // selected one; its other rows are dropped here, so a selected kind's rows are exactly its rows in a full answer. + if( scope.kinds != kAllKinds ) + { + std::erase_if( regs, [ &scope ]( const Regression& r ) { return !kindSelected( scope.kinds, r.kind ); } ); + } std::sort( regs.begin(), regs.end(), []( const Regression& a, const Regression& b ) { return a.kind != b.kind ? a.kind < b.kind : a.sym < b.sym; } ); return regs; diff --git a/src/resolve.h b/src/resolve.h index 5cd3edaf6..689e012c7 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -1805,7 +1805,7 @@ inline std::pair resolveRubyConstant( const RubyCo // byName rule — the pre-scoping answer, never a guess in either direction — and is COUNTED by `disclosure` below // (RubyBaseScopeDisclosure), which buildGraph copies to Graph::rubyBasesUnscoped: the gauge attribute // ruby_bases_unscoped=N, absent at zero (graphlegend.h graphGaugeAttrXml/Json, test/rubyinheritcheck.sh). -// Built only when the corpus holds a Ruby inherit reference; empty otherwise. +// Built only when the corpus holds a Ruby inherit reference or a main-object mixin (LocalBindKind::RubyMainMixin); empty otherwise. // // The sink models Diagnostics::DisclosureSink: one reason, one count, read by the emitter as the gauge attribute. struct RubyBaseScopeDisclosure @@ -2078,7 +2078,8 @@ inline RubyBaseScope buildRubyBaseScope( const IngestResult& ing ) { RubyBaseScope sc; const auto isRubyBase = []( const Reference& r ) noexcept { return r.isInherit && r.lang == Lang::Ruby; }; - if( std::none_of( ing.references.begin(), ing.references.end(), isRubyBase ) ) + const auto isMainMixin = []( const Binding& b ) noexcept { return b.kind == LocalBindKind::RubyMainMixin; }; // graph.h rubyMainObjectMixins reads classesByFqn too + if( std::none_of( ing.references.begin(), ing.references.end(), isRubyBase ) && std::none_of( ing.bindings.begin(), ing.bindings.end(), isMainMixin ) ) { return sc; } diff --git a/src/serialize.h b/src/serialize.h index 830ed426e..eabf28b98 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -36,7 +36,6 @@ #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 // back_inserter (reorderDocsAfterCode) @@ -889,18 +888,6 @@ inline constexpr std::string_view kForSigsNextBudgetNote = // (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; 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. @@ -923,13 +910,119 @@ inline constexpr std::string_view kForSigsUnpaidOverCeilingNote = // deterministic, query-independent, and self-announcing (capped="1" on ). An EXPLICIT // --token-budget=N overrides the default (N tokens × the conservative byte rate), so a caller who asks // for a bigger (or smaller) bundle beats the default — but in AUTO-BUNDLE mode the sig side's claim on -// that wider ceiling is capped at THIS default budget (main.cpp sigSideCeiling; classb-bytes-memo §2's +// that wider ceiling is capped at THIS default budget (main.cpp sigSideCeiling, retired by forbudget-soft; classb-bytes-memo §2's // N=8000 trap: an uncapped claim re-inflated the trimmed tail and crowded the auto bodies out), so the // excess buys bodies, not sig tail. An explicit --pack-top-n keeps the whole-ceiling sig claim, and // --signatures-only / --detail=N / --json (no auto bodies to reserve for) are likewise uncapped. +// +// STOP WHEN COMPLETE (forbudget-soft, owner decisions 2026-10-14; PROCESS rule 5). The default bundle's ranked head (: the relevance floor intersected with kForLensDefaultTopN or --pack-top-n) IS the answer, so this budget is a +// STAIR-STEP TARGET, not a ceiling: steps A..E above still shrink row TEXT against it (an answer that fit before is +// byte-identical), but step F — the only step that removes a row — runs against kForRunawayGuardBytes below. The rows served +// past the target are disclosed on the block (: the last K rows are the ones F would have dropped +// here) and the budget_bytes= clause calls this number a target on such an answer. An EXPLICIT --token-budget is a SOFT +// target over the rows alone (forSigsBudgets below); the text above about its frozen sig share describes the +// legacy hard-ceiling regime it replaced. inline constexpr std::size_t kForPayloadBudgetBytes = 7500; inline constexpr std::size_t kForCapTailSigBytes = 96; +// ── THE RUNAWAY GUARD (forbudget-soft; PROCESS rule 5's role (a)) ──────────────────────────────────────────────── +// The one default-regime row cut. Measured on the same quantity the 7.5 KB target measures (the charged header + the charged +// sections + ), far above a typical answer: hitting it is a BUG SIGNAL (a recursion, a dump, a head far wider than any +// question needs), never a budget. A default answer that hits it discloses over_ceiling="1" on the root, (the continuation serves every row: an explicit target is never cut by this guard, see max(guard, T)) and +// forRunawayGuardNote(). The value is set by the lane's pre-registered guard rule (forbudget-soft, frozen before the code): roundup_1000( max( 4 x the +// target, 2 x the largest complete default payload on the round-2 --for argv ) ) = max( 30,000, 2 x 12,519 ) = 30,000 B (44 +// default-regime round-2 answers, measured on the provisional 32,000 build); verified with zero hits on the round-4-only (max +// 13,005 B) and capsweep (max 12,094 B) argv. Gate: test/forsoftbudgetcheck.sh (D4, D4b, NX2). +inline constexpr std::size_t kForRunawayGuardBytes = 30000; +static_assert( kForRunawayGuardBytes >= 4 * kForPayloadBudgetBytes, "the runaway guard sits far above the default target" ); +// The reading spliced on an answer that hit the guard (and only there). It names the value so a reader (and the gate) reads the +// guard from the answer itself. "A bug signal" is true of a default-size head; an explicitly wide --pack-top-n head is named +// as the other way to reach it. No "--" (it rides inside an XML comment, G4). +inline std::string forRunawayGuardNote() +{ + return " [over_ceiling=1: the ranked head passed the runaway guard of " + std::to_string( kForRunawayGuardBytes ) + + " B (header plus ranked payload), so the rows past it are cut and sigs next= serves them all; on a default-size head" + " this is a bug signal, not a budget (a wide pack-top-n head can reach it too)]"; +} + +// ── THE TWO BUDGETS (forbudget-soft, owner decisions 2026-10-14; gate: forsoftbudgetcheck) ────────────────────── +// `target`: what the ladder's text-shrink steps A..E aim at (serialize.h trimSigLadder's effectiveBudget). `rowDrop`: where +// its row-drop step F aims (SigsCutReport::rowDropBudgetBytes). Both in the block's own bytes. +// • DEFAULT (no --token-budget, softRowBytes == 0): the target is kForPayloadBudgetBytes' sig share (the bundle's charged +// header + sections subtracted), exactly as before, so an answer that fit is byte-identical; rows drop only past the +// runaway guard's share (kForRunawayGuardBytes, the same quantity) — the whole ranked head is the answer. +// • SOFT TARGET (an explicit --token-budget / MCP budget_tokens: T = softRowBytes): T is charged against the ROWS inside +// only — never the header or a disclosure — so the compact and full dialects serve the same rows. Rows drop, +// tail-first, until they fit the row budget (+ the "" wrapper); rank 1..4 always ship. Rows are therefore a +// rank prefix, monotone in T. RESERVE SECTIONS FIRST (round C, owner decision 2026-10-14): the text tier and the TARGET +// CUT (what A..E aim at, and where the default regime's past_target= would start) is the retired hard ceiling's sig +// claim — T less the charged header and sections as the full dialect prices them (`fullFixedBytes`), and on the default +// auto bundle (`tierAtDefault`: bodies or hops ride after the rows and no --pack-top-n names a sig posture) no more than +// the DEFAULT target's share (classb-bytes-memo §2's N=8000 trap: a wider target must not re-inflate row text at the +// bodies' expense). The trailing sections (lego/compose scope, hops, bodies, the tail) are funded as if the rows stopped +// at that cut, so each keeps at least the share the hard ceiling gave it; the rows past the cut then fill what the +// sections leave of T (the caller's second rowDrop, runForLens) — and never fall below the cut. +struct ForSigsBudgets +{ + std::size_t target = 1; // >= 1: 0 means "no budget" to packSignatures + std::size_t rowDrop = 0; // > target whenever a soft target or the guard is in force +}; +inline ForSigsBudgets forSigsBudgets( std::size_t softRowBytes, std::size_t fixedBytes, std::size_t fullFixedBytes, bool tierAtDefault ) noexcept +{ + constexpr std::size_t kSigsWrapperBytes = 13; // "" + "": the block's own frame, not a row + const auto share = []( std::size_t budget, std::size_t fixed ) { return budget > fixed ? budget - fixed : std::size_t( 1 ); }; + ForSigsBudgets b; + if( softRowBytes > 0 ) + { + // round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the target is the retired hard ceiling's sig claim at this + // N — T less the bundle's charged header and sections as the FULL dialect prices them (one cut for both dialects), and + // on the default auto bundle no more than the default target's share — so the rows at the target are the rows that + // ceiling served, the trailing sections are funded as if the rows stopped there (the default regime's own rule), and + // the rows past it fill what the sections leave of T (the caller narrows rowDrop to that once they are rendered). + const std::size_t cut = share( softRowBytes, fullFixedBytes ); + b.target = tierAtDefault ? std::min( cut, share( kForPayloadBudgetBytes, fullFixedBytes ) ) : cut; + b.rowDrop = softRowBytes + kSigsWrapperBytes; + } + else + { + b.target = share( kForPayloadBudgetBytes, fixedBytes ); + b.rowDrop = std::max( share( kForRunawayGuardBytes, fixedBytes ), b.target + 1 ); + } + ENSURES( b.target >= 1 && b.rowDrop > b.target, "forSigsBudgets: the row-drop budget sits above the text target" ); + return b; +} + +// ── ROUND C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the shared arithmetic of the soft target's row refill ────── +// The rows past a soft target's cut fill what the trailing sections served before them left of T: this is the row-drop budget +// a refill render passes (SigsCutReport::rowDropBudgetBytes, the "" frame added as forSigsBudgets adds it). At or +// under the ladder's target it IS the cut (sigsRowDropEffective), so the rows never fall below it. CLI XML, JSON and MCP `for`. +inline std::size_t forSoftRowDropFill( std::size_t targetBytes, std::size_t sectionsServedBytes ) noexcept +{ + constexpr std::size_t kSigsWrapperBytes = 13; // "" + "", as forSigsBudgets + return ( targetBytes > sectionsServedBytes ? targetBytes - sectionsServedBytes : 0u ) + kSigsWrapperBytes; +} +// What a soft target's bundle budget leaves the trailing sections once `spentBytes` (the rows at the cut and the fixed +// sections) are paid — the share forSoftSectionsOptIn and the tail fill read. +inline std::size_t forSoftSectionsShare( std::size_t budgetBytes, std::size_t spentBytes ) noexcept +{ + return budgetBytes > spentBytes ? budgetBytes - spentBytes : 0u; +} +// A refill render replaces the first render's redaction count rather than adding to it: take the first render's own +// contribution (`after` less `before`, snapshotted around it) back out of the run's tally before rendering again. +inline void redactTakeBack( RedactCounts* tally, const std::optional& before, const std::optional& after ) noexcept +{ + if( tally == nullptr || !before || !after ) + { + return; + } + for( std::size_t k = 0; k < tally->byKind.size(); ++k ) + { + tally->byKind[k] -= after->byKind[k] - before->byKind[k]; + } +} + // ── THE --for LENS'S RANKED-HEAD CAP, OWNED ONCE (round-4 finding F-03) ────────────────────────────── // How many ranked symbols the --for lens considers before the relevance floor narrows it and the ladder // above trims it. The CLI lens (`--pack-top-n`, else this) and the MCP `for` verb MUST read the same @@ -950,12 +1043,11 @@ inline constexpr int kForLensDefaultTopN = 40; // the DEFAULT gets richer rather than a richer verb staying unreachable. This allowance is the body // budget the DEFAULT bundle gains ON TOP of kForPayloadBudgetBytes (~1.6K tokens at kBytesPerTokenBody): // the signature budget above is untouched, so the ranked map is byte-identical to the signatures-only -// run and the bodies ride only on this disclosed extra. An EXPLICIT --token-budget=N is a hard ceiling — -// no allowance; bodies take the budget the signature bundle left over, where the signature side's claim -// is capped at kForPayloadBudgetBytes (main.cpp sigSideCeiling — the forbudgetmonotoncheck invariant: a -// wider ceiling never serves less decisive content; at any ceiling >= kForPayloadBudgetBytes + -// kForAutoBodyBudgetBytes the sig render equals the default regime's, so the bodies' leftover is -// provably >= the default's and every body the default serves still fits). Disclosure is on +// run and the bodies ride only on this disclosed extra. An EXPLICIT --token-budget=N is a soft target over +// the rows (forbudget-soft) — no allowance; bodies take what the rows left of N, the rows shrunk at +// the default's tier on this bundle (forSigsBudgets tierAtDefault), so a wide N never re-inflates row text at +// the bodies' expense (the forbudgetmonotoncheck invariant: a wider target never serves less decisive +// content). Disclosure is on // the root (bundle="auto" bodies="N", reason="budget" when none fit) plus a legend sentence; // --signatures-only opts out byte-identically; --detail=N (the explicit knob) supersedes the automatic // selection. Candidate cap: kPackTaskBodyCandidates — the default --for converges on the pack-task shape. @@ -1001,135 +1093,35 @@ inline constexpr std::size_t kForAnchorBodyBudgetBytes = 22800; // The lens serves ranked SYMBOL heads concentrated in few files; a consumer that needs file-grain recall // 20+ deep (external re-measure: the pre-registered retrieval lanes score exactly that) runs out of files // long before that depth — measured externally as the ENTIRE recall gap on the one task family where the -// head's MRR already wins. The tail is the cheapest honest widening: the REMAINING candidate files (a -// positive lens score, not already in the served head's file set) as PATHS ONLY, in the deterministic -// best-symbol projection of the same (score desc, id asc) order the head selected with — a projection of -// the existing ranking, never a re-rank. Zero-score files are NOT served (the LB-A relevance-floor -// reasoning at file grain: a file whose symbols matched nothing has no evidence to stand on). total= -// counts every candidate file; shown= what this bundle printed; capped="1" when they differ. The legend -// labels it file-grain, weaker evidence than the ranked rows above it. -// • DEFAULT regime: rows cap at kForFileTailShownCap; the bytes ride ON TOP of the sig/body budgets -// (the kForAutoBodyBudgetBytes precedent) and are measured into est_tokens — the ranked head and the -// bodies stay byte-identical to a tail-less bundle by construction. -// • EXPLICIT --token-budget: the ceiling stays hard — the tail is funded LAST (weakest evidence pays -// first): rows fit into the residual the rendered bundle actually left, trimming down to the honest -// empty shell. kForFileTailShellReserve rides inside the committed bytes handed to the body walk so -// that shell always fits — the kAutoAttrReserve pattern, sized for ``. -inline constexpr std::size_t kForFileTailShownCap = 24; +// head's MRR already wins. Zero-score files are NOT served (the LB-A relevance-floor reasoning at file grain). +// The REMAINING candidate files (a positive lens score, not among the shown head's files) as PATHS ONLY, in the +// deterministic best-symbol projection of the same (score desc, id asc) order the head selected with — the rank rows. +// FOLD C (forbudget-soft round C, owner decision 2026-10-14): page 0 of the --for file page (forpage.h computeForFilePage: +// one row per positive-score file, file-first by the IDF share its top symbols cover) follows them, each row with sym= +// naming its top symbols, so the answer carries its file grain and a capped tail's next= is the page itself. Measured +// before the fold (arm M4, the page appended to the answer): 6 of 6 predicted rows reached an R0-missing item, none lost +// one. The model and its renderers live in forpage.h (they need the page's ranking and its next= composer); the constants +// and the legend stay here, where the --for header reads them. +// • DEFAULT regime: kForFileTailShownCap rank rows + kForFileTailPageRows page rows; the bytes ride ON TOP of the +// sig/body budgets and are measured into est_tokens. +// • EXPLICIT --token-budget (a soft target): the tail is a trailing section funded first at its share — the +// bytes left once the rows at the target cut and the sections before it are paid (runForLens) — and fills +// that share (its last row may cross it); every cut carries next=. kForFileTailShellReserve sizes the +// empty shell ``. +inline constexpr std::size_t kForFileTailShownCap = 24; // the rank rows (deep-tail d2): files outside the shown head +inline constexpr std::size_t kForFileTailPageRows = 40; // FOLD C: page 0 of the file page (forpage.h kForPageRowsDefault) inline constexpr std::size_t kForFileTailShellReserve = 48; -// The candidate walk, shared by the XML and JSON dialects (and the MCP `for` twin) so the three surfaces -// cannot select differently. `headIds` is THE BUNDLE'S RESOLVED SURFACE (the exact set selects); -// paths are root-relativized exactly as every emitted p= is (R-R). paths.size() <= kForFileTailShownCap; -// `total` keeps counting past the cap so the disclosure pair is honest. -struct FileTail -{ - std::vector paths; // first kForFileTailShownCap tail files, ranker order, display form - std::size_t total = 0; // every remaining candidate file with a positive lens score -}; - -inline FileTail computeFileTail( const IngestResult& ing, const std::vector& rank, - const std::vector& headIds, std::string_view rootArg ) -{ - FileTail out; - std::vector inHead( ing.files.size(), 0 ); - for( NodeId sid : headIds ) - { - if( sid < ing.symbols.size() && ing.symbols[ sid ].fileId < inHead.size() ) - { - inHead[ ing.symbols[ sid ].fileId ] = 1; - } - } - - const std::size_t S = ing.symbols.size(); - std::vector order( S ); - for( NodeId i = 0; i < S; ++i ) - { - order[i] = i; - } - sortutil::radixSortByScoreDescId( order, rank ); - - const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); - std::vector seen( ing.files.size(), 0 ); - for( std::size_t k = 0; k < S; ++k ) - { - const NodeId id = order[k]; - if( id >= rank.size() || !( rank[id] > 0.0f ) ) - { - break; // (score desc) order: the zero-score tail is contiguous - } - const std::uint32_t f = ing.symbols[id].fileId; - if( f >= seen.size() || seen[f] || inHead[f] ) - { - continue; - } - seen[f] = 1; - ++out.total; - if( out.paths.size() < kForFileTailShownCap ) - { - out.paths.emplace_back( rootArg.empty() ? std::string( ing.files[f] ) - : std::string( rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) ) ); - } - } - return out; -} - -// The XML shape: `…` — §P8 vocabulary, always -// emitted (a total of 0 means genuinely none remain, never not-computed — the B1.4 rule for elements). -// `shownCap` lets the explicit-budget caller trim rows below the collected count; the JSON twin below -// renders the SAME decision so the dialects cannot diverge on what was served. -// `cutNextAttr` (a ready ` next="…"` from nextAttrXml, or empty) rides the open tag ONLY when the tail is cut -// (shown < total): the call that serves the files left out. Empty (every caller but the --for explicit-budget -// regime) ⇒ byte-identical to before. -inline std::string renderFileTailXml( const FileTail& t, std::size_t shownCap, std::vector& esc, std::string_view cutNextAttr = {} ) -{ - const std::size_t shown = std::min( shownCap, t.paths.size() ); - std::string x = ""; - } - x += ""; - return x; -} - -// The JSON twin: `"tail":{"total":T,"shown":K,"files":[…]}` (the caller supplies the leading comma). -inline std::string renderFileTailJson( const FileTail& t, std::size_t shownCap ) -{ - const std::size_t shown = std::min( shownCap, t.paths.size() ); - std::string j = "\"tail\":{\"total\":" + std::to_string( t.total ) - + ",\"shown\":" + std::to_string( shown ) + ",\"files\":["; - for( std::size_t i = 0; i < shown; ++i ) - { - if( i > 0 ) - { - j += ","; - } - j += '"'; - jsonesc::escapeInto( t.paths[i], j, false, true, false ); // same core appendJsonStrField uses - j += '"'; - } - j += "]}"; - return j; -} - // The legend clause defining BOTH deep-tail surfaces (the r= rank fact and the element) — a named // constant, shared by the CLI --for header and the MCP `for` twin, so the sigs-budget exemption in each // dialect subtracts EXACTLY the bytes the clause adds (the kForAutoBundleLegend pattern). No "--" anywhere: // it rides inside an XML comment where "--" is ill-formed (G4). inline constexpr std::string_view kForFileTailLegend = - "; tail: file-grain tail, WEAKER evidence than the ranked rows (paths only): every positive-score file " - "NOT among the shown sigs rows — the files of trimmed rows first, best-symbol rank order; rows are t p=file; total=such files, " - "shown=printed, capped=1 when they differ. r= on a ranked row is its 1-based rank in this lens ranking, " - "rows in r= order, p= the file (a gap = a budget-trimmed row)"; + "; tail: file-grain rows, WEAKER evidence than the ranked rows: first t p= alone, the positive-score files NOT among the " + "shown sigs rows, the files of trimmed rows first, best-symbol rank order; then t p= sym=, the file page's first 40 rows " + "not listed above, file-first (the IDF share of the query's subtokens its top 8 symbols cover), sym= its top symbols; " + "total=every positive-score file, shown=printed, capped=1 when they differ, next= the page from its first unprinted row. " + "r= on a ranked row is its 1-based rank in this lens ranking, rows in r= order, p= the file (a gap = a budget-trimmed row)"; // R2-AF (round 2, S4): the legend clause defining `` — verbatim, round-2 amendment §R7. A // named constant, shared by the CLI --for header and the MCP `for` twin (rw::forNamedHeaderRows, @@ -1139,20 +1131,6 @@ inline constexpr std::string_view kForHdrLegend = "; hdr p= of=: the file the task names (of=) has exactly one same-directory, same-stem declaration/" "implementation partner (p=), listed first by name alone: a lookup, not a ranked or graph-derived row"; -// Explicit-budget row fit: the largest shown count whose rendered XML fits `budgetBytes` (0 rows always -// "fits" — the shell is reserved by the caller). Walks down from the collected count; deterministic. -inline std::size_t fileTailShownForBudget( const FileTail& t, std::size_t budgetBytes, std::vector& esc, std::string_view cutNextAttr = {} ) -{ - for( std::size_t shown = t.paths.size(); shown > 0; --shown ) - { - if( renderFileTailXml( t, shown, esc, cutNextAttr ).size() <= budgetBytes ) - { - return shown; - } - } - return 0; -} - // ── COMPACT CONCEPTUAL SERVING (pre-registered: docs/EVALS.md, the T3 route-narrowing round) ────────── // On the CONCEPTUAL route — the subtoken+body ranker, the route that names no anchor — the allowance // above is where this tool's bytes actually go and where they cost it the standing head-to-head: class B @@ -2335,6 +2313,8 @@ inline constexpr const char* kDeclinedGateMapLegend = inline constexpr const char* kDeclinedStdMapLegend = ""; +inline constexpr const char* kInertDeclinedMapLegend = + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged @@ -2745,6 +2725,26 @@ inline bool namesOnlyOutAny( const std::vector& outOff, const std return false; } +// attrib-multidef Q1: may a row under one of `nodes` carry prov="split"? True when any of their out-edges is one arm of a +// resolver split and not by name alone (a by-name row carries via="name" instead) — namesOnlyOutAny's twin question, over +// the same nodes, for the same first-screen legend. +inline bool splitArmsOutAny( const std::vector& outOff, const std::vector& outNameOnly, + const std::vector& outProv, std::span nodes ) noexcept +{ + for( const NodeId n : nodes ) + { + for( std::uint32_t e = ( std::size_t( n ) + 1 < outOff.size() ) ? outOff[ n ] : 0u; + std::size_t( n ) + 1 < outOff.size() && e < outOff[ n + 1 ] && e < outProv.size(); ++e ) + { + if( outProv[ e ] == 3u && !( e < outNameOnly.size() && outNameOnly[ e ] != 0 ) ) + { + return true; + } + } + } + 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 @@ -2890,7 +2890,10 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::vector* outNameOnly = nullptr, // of declined=, the calls the C++ standard-member gate declined (graph.h g.stdMemberDeclinedCalls) → // the kDeclinedStdMapLegend clause, absent when zero. - std::size_t stdMemberDeclinedCalls = 0 ) + std::size_t stdMemberDeclinedCalls = 0, + // of declined=, the Elixir call candidates inside inert quote AST (g.inertDeclinedCalls, #380) → + // the kInertDeclinedMapLegend clause, absent when zero. + std::size_t inertDeclinedCalls = 0 ) { const std::size_t* changedCount = ann.changedCount; const std::string* mapAtStamp = ann.atStamp; @@ -3135,6 +3138,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto legend += declinedTotal > 0 ? kDeclinedMapLegend : ""; // charged to the map that carries declined= legend += gateDeclinedCalls > 0 ? kDeclinedGateMapLegend : ""; // only where the builtin-method gate declined a call legend += stdMemberDeclinedCalls > 0 ? kDeclinedStdMapLegend : ""; // only where the C++ standard-member gate declined a call + legend += inertDeclinedCalls > 0 ? kInertDeclinedMapLegend : ""; // only where an Elixir quote declined a call candidate if( outNameOnly && !stubbed && !ann.viaLegendStripped && namesOnlyOutAny( outOff, *outNameOnly, std::span( order.data(), std::min( keep, order.size() ) ) ) ) { @@ -4912,10 +4916,80 @@ inline std::size_t droppedPositiveCount( std::size_t candidatePositives, std::si // so subtract the old cost first and add the new one — never form `before - after` (it can be negative, // i.e. unsigned-overflow UB under G1's -fsanitize=integer). `total >= before` always holds (before is a // summand of total). +// Ladder step F's walk, LOWEST RANK first (P7, terminality round A, lane R, 2026-09-05: `entries` is in rank order — both callers +// sort it before the ladder runs — so the tail walk IS the rank walk; the old file-major form could drop a rank-5 row in the +// last file while a rank-39 row in the first file survived). A file's wrapBytes (its notes) go with the file's last live row. +// Rank 1..4 always survive (the floor). Returns the rows it dropped. +template +inline std::size_t sigLadderDropRows( std::vector& entries, std::vector& files, std::size_t& total, + std::size_t budget, CostFn entryCost ) +{ + std::size_t dropped = 0; + for( std::size_t k = entries.size(); k > 0 && total > budget; ) + { + EntryT& e = entries[ --k ]; + if( e.dropped || e.globalRank <= 4 ) + { + continue; + } + total -= entryCost( e ); + e.dropped = true; + ++dropped; + FileT& sf = files[ e.fileSlot ]; + if( --sf.liveCount == 0 ) + { + total -= sf.wrapBytes; + } + } + return dropped; +} +// forbudget-soft: the same walk as a DRY RUN on a copy of the live counts — how many rows step F would drop at `budget` and the +// block total it would leave, mutating nothing. +template +inline std::pair sigLadderDryDrop( const std::vector& entries, const std::vector& files, + std::size_t total, std::size_t budget, CostFn entryCost ) +{ + std::vector live( files.size() ); + for( std::size_t i = 0; i < files.size(); ++i ) + { + live[ i ] = files[ i ].liveCount; + } + std::size_t wouldDrop = 0; + for( std::size_t k = entries.size(); k > 0 && total > budget; ) + { + const EntryT& e = entries[ --k ]; + if( e.dropped || e.globalRank <= 4 ) + { + continue; + } + total -= entryCost( e ); + ++wouldDrop; + if( --live[ e.fileSlot ] == 0 ) + { + total -= files[ e.fileSlot ].wrapBytes; + } + } + return { wouldDrop, total }; +} + +// +// THE TARGET AND THE ROW-DROP BUDGET (forbudget-soft). Steps A..E aim at `effectiveBudget` (the TARGET: they shorten row text, +// no row goes). Step F — the only step that removes a row — runs against `dropBudget` (>= the target; equal to it in the +// classic hard-ceiling call below). When it is wider, the rows F WOULD have dropped at the target but keeps are counted +// (a dry run over the same tail walk, so they are exactly the last rows kept: pastTargetRows) and `targetCutTotal` is the +// block total that cut would have left — the caller's trailing sections are funded as if the block stopped there. +struct SigLadderOutcome +{ + std::size_t pastTargetRows = 0; // rows F would have dropped at the target and kept (the --for default's past_target=) + std::size_t droppedRows = 0; // rows F dropped against dropBudget + std::size_t targetCutTotal = 0; // the block total after F at the target (== total when nothing ran past it) +}; template -inline void trimSigLadder( std::vector& entries, std::vector& files, - std::size_t& total, std::size_t effectiveBudget, CostFn entryCost ) +inline SigLadderOutcome trimSigLadder( std::vector& entries, std::vector& files, + std::size_t& total, std::size_t effectiveBudget, CostFn entryCost, std::size_t dropBudget ) { + EXPECTS( dropBudget >= effectiveBudget ); // the row-drop budget is never tighter than the target the shrink steps aim at + SigLadderOutcome out; const auto fits = [ & ] { return total <= effectiveBudget; }; const auto shrinkSig = [ & ]( EntryT& e, std::size_t cap ) { total -= entryCost( e ); truncateUtf8WithEllipsis( e.sig, cap ); total += entryCost( e ); }; @@ -4958,26 +5032,25 @@ inline void trimSigLadder( std::vector& entries, std::vector& fil shrinkSig( entries[k], kForTailSigBytes ); } } - // F: drop whole entries, LOWEST RANK first. P7 (terminality round A, lane R, 2026-09-05): `entries` is in - // rank order (both callers sort it before the ladder runs), so the tail walk IS the rank walk — the old - // file-major form ("tail file first, source order inside it") could drop a rank-5 row in the last file - // while a rank-39 row in the first file survived. A file's wrapBytes (its file notes now — the - // wrapper is gone) go with the file's last live row, exactly as before. - for( std::size_t k = entries.size(); k > 0 && !fits(); ) + // F: drop whole entries, lowest rank first (sigLadderDropRows), against the row-drop budget. + // F's dry run at the TARGET (only when the row-drop budget is wider) names exactly the tail rows the real walk keeps past it + std::size_t wouldDrop = 0; + out.targetCutTotal = total; + if( dropBudget > effectiveBudget && !fits() ) { - EntryT& e = entries[ --k ]; - if( e.dropped || e.globalRank <= 4 ) - { - continue; // rank 1..4 always survive (the floor) - } - total -= entryCost( e ); - e.dropped = true; - FileT& sf = files[ e.fileSlot ]; - if( --sf.liveCount == 0 ) - { - total -= sf.wrapBytes; // the file's notes go with its last live row - } + const auto [ rows, cutTotal ] = sigLadderDryDrop( entries, files, total, effectiveBudget, entryCost ); + wouldDrop = rows; + out.targetCutTotal = cutTotal; } + out.droppedRows = sigLadderDropRows( entries, files, total, dropBudget, entryCost ); + if( dropBudget == effectiveBudget ) + { + out.targetCutTotal = total; + } + // the guard's cut is a tail of the target's (same walk, wider stop), so it never drops a row the target kept + ASSUME( out.droppedRows <= wouldDrop || dropBudget == effectiveBudget ); + out.pastTargetRows = wouldDrop > out.droppedRows ? wouldDrop - out.droppedRows : 0u; + return out; } // ── THE ROW GATE (`budgetBytes`, --pack-budget-bytes), RANK FIRST (cut-fix lane A, 2026-09-23) ───────────── @@ -5050,6 +5123,19 @@ struct SigsCutReport // 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; + // IN (forbudget-soft): where the ladder's row-drop step F aims. 0 = at the payload budget, the steps A..E target (the hard + // ceiling every caller had before). Nonzero = F runs against THIS budget (>= the payload budget; same units, the block's + // own bytes): the payload budget stays the TARGET the text-shrink steps aim at, and rows past it are kept until this one. + // The --for default passes the runaway guard's share here; an explicit soft target passes its row bytes T. + std::size_t rowDropBudgetBytes = 0; + bool pastTargetDisclosed = false; // IN: print past_target= (the default regime's target); the soft regime does not + std::size_t pastTarget = 0; // OUT: rows step F would have dropped at the payload budget and kept (past_target=) + std::size_t pastTargetBytes = 0; // OUT: the block bytes those rows add over the target cut (rows, their file notes, + // the tag's attribute bytes) — what the caller's trailing sections must not pay for + bool rowDropCut = false; // OUT: step F dropped rows against rowDropBudgetBytes (the guard / the soft target) + std::vector pastTargetRanks; // OUT: the r= values the past_target rows are SHOWN with, ascending — the K + // lowest-ranked shown rows by their global rank, read after the docs reorder + // renumbered them (sigsRowsAtTarget scopes the default's sections by it) std::size_t shown = 0; std::size_t total = 0; std::size_t docsDropped = 0; @@ -5062,13 +5148,6 @@ 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 - std::size_t continuationPaidSurplus = 0; // train 27: what the rows gave up past what they owed (a whole row dropped for a - // smaller handle) — the caller adds it to a measured overrun (verbs_for.h runForLens) - 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=) ────────── @@ -5076,30 +5155,15 @@ struct SigsCutReport // 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). -// 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 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). -// …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). +// --signatures-only serves the rows uncut at a wide soft target (forSigsBudgets: the text tier is the target itself off the +// default auto bundle), 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 soft target, 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 target that cut them. +// WHO PAYS FOR ITS BYTES: nobody — the continuation is EXEMPT (the e= / docs_after_code convention: a disclosure never costs +// a row). forbudget-soft retired the CHARGED regime (knob-honesty-068: a hard explicit ceiling paid the handle from rows, +// terse, and re-rendered up to three times under ruling C3): an explicit budget is a soft target over the rows now, with no +// byte promise for a handle to be paid into. // 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 @@ -5116,22 +5180,10 @@ struct SigsCutContinuation 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 charged = false; // reserve it (and its legend clause) INSIDE the block's budget — see the regime rule above + std::size_t fixedBytes = 0; // what the re-run's target must carry besides the block: the charged header + sibling blocks 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 a PAID 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) - std::size_t overrunBytes = 0; // train 27 (compactlegendcheck P1-B): a CHARGED one paying from rows also pays these bytes - // from the rows, byte for byte beside the handle — the overrun past the exact budget the - // caller MEASURED on its paid render (verbs_for.h runForLens, the compact dialect only); - // 0 everywhere else, so every other caller's rows are untouched }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -5191,6 +5243,7 @@ struct SigsTrimPlan bool capped = false; std::size_t effectiveBudget = 0; // the ladder's target: the budget minus the tag's reserved bytes std::size_t docsDroppable = 0; // the docs_dropped= upper bound + std::size_t reserveBytes = 0; // the tag's reserved bytes (forbudget-soft: the row-drop budget subtracts them too) }; template inline SigsTrimPlan planSigsTrim( const std::vector& entries, std::size_t totalRows, std::size_t gateCut, @@ -5207,9 +5260,71 @@ inline SigsTrimPlan planSigsTrim( const std::vector& entries, std::size_ && blockBytes + docsReserve + ( gateCut > 0 ? markerBytes : 0u ) > payloadBudgetBytes; plan.capped = plan.ladderFires || gateCut > 0; plan.effectiveBudget = payloadBudgetBytes > reserve ? payloadBudgetBytes - reserve : 0u; + plan.reserveBytes = reserve; return plan; } +// forbudget-soft: step F's effective budget for a caller that asked for a wider row-drop budget (SigsCutReport:: +// rowDropBudgetBytes): that budget less the tag's reserve and the past_target= attribute the block may then carry — never +// below the target's own effective budget (0 asked, or one at or under the payload budget = the classic hard ceiling). +inline std::size_t sigsRowDropEffective( const SigsTrimPlan& plan, std::size_t payloadBudgetBytes, std::size_t rowDropBudgetBytes, + std::size_t totalRows ) noexcept +{ + if( rowDropBudgetBytes <= payloadBudgetBytes ) + { + return plan.effectiveBudget; + } + const std::size_t reserve = plan.reserveBytes + sizeof( " past_target=\"\"" ) - 1 + sigsDecimalDigits( totalRows ); + return std::max( plan.effectiveBudget, rowDropBudgetBytes > reserve ? rowDropBudgetBytes - reserve : 0u ); +} + +// forbudget-soft: what the ladder's two budgets left on the report — the rows past the target, the bytes they add over the +// target cut (rows, file notes and, when it is printed, the attribute), and whether the wider budget itself cut rows. +inline void sigsCutRecordLadder( SigsCutReport& cut, const SigLadderOutcome& ladder, std::size_t totalAfterLadder, + std::size_t rowDropBudgetBytes, bool pastTargetDisclosed ) +{ + cut.rowDropBudgetBytes = rowDropBudgetBytes; + cut.pastTargetDisclosed = pastTargetDisclosed; + cut.pastTarget = ladder.pastTargetRows; + cut.rowDropCut = rowDropBudgetBytes > 0 && ladder.droppedRows > 0; + if( ladder.pastTargetRows > 0 ) + { + ASSUME( totalAfterLadder >= ladder.targetCutTotal ); // the target cut is a superset of the guard's: fewer bytes + cut.pastTargetBytes = totalAfterLadder - ladder.targetCutTotal + + ( pastTargetDisclosed ? sizeof( " past_target=\"\"" ) - 1 + sigsDecimalDigits( ladder.pastTargetRows ) : 0u ); + } + ENSURES( cut.pastTarget <= cut.shown, "sigsCutRecordLadder: more rows past the target than rows shown" ); // they are shown rows +} + +// forbudget-soft: which shown rows are the past_target= ones, by the r= they are shown with. The ladder's past-target rows are +// the rank TAIL of the shown set (the K highest global ranks); the docs reorder (reorderDocsAfterCode) then renumbers the shown +// rows, so the tail cannot be read back off the rendered r= (a doc row of global rank 28 is shown as r="35"). Recorded from the +// entries after the reorder, before the block renders. EntryT: the XML SigEntry or the JSON sibling (dropped, globalRank, displayRank). +template< class EntryT > +inline void sigsCutRecordPastTargetRanks( SigsCutReport& cut, const std::vector& entries ) +{ + cut.pastTargetRanks.clear(); + if( cut.pastTarget == 0 ) + { + return; + } + std::vector> live; // (globalRank, displayRank) of the shown rows + for( const EntryT& e : entries ) + { + if( !e.dropped ) + { + live.emplace_back( e.globalRank, e.displayRank ); + } + } + std::sort( live.begin(), live.end(), []( const auto& a, const auto& b ) { return a.first > b.first; } ); + for( std::size_t k = 0; k < live.size() && k < cut.pastTarget; ++k ) + { + cut.pastTargetRanks.push_back( live[ k ].second ); + } + std::sort( cut.pastTargetRanks.begin(), cut.pastTargetRanks.end() ); + ENSURES( cut.pastTargetRanks.size() == cut.pastTarget, "sigsCutRecordPastTargetRanks: a past_target row is not a shown row" ); +} + // What the finished block shows, from its rows after the gate and the ladder: shown rows, and the shown rows whose doc // comment was removed (hadDoc, and no doc at emission) — the docs_dropped= value. template @@ -5237,8 +5352,7 @@ inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinua { return; } - cut.hasContinuation = true; - cut.terseContinuation = req->charged && req->pasteHandle; + cut.hasContinuation = true; if( req->pasteHandle ) { cut.next = invocation; @@ -5250,7 +5364,7 @@ inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinua } inline bool sigsCutHasNextOffset( const SigsCutReport& cut ) noexcept { - return cut.hasContinuation && !cut.terseContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; + return cut.hasContinuation && 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, // docs_after_code (train 26b: lean's reorder reading joins the knob's report legend) @@ -5261,7 +5375,11 @@ inline std::string sigsCutReportLegend( const SigsCutReport& cut ) { notes += kForSigsShrunkNote; } - if( cut.isCapped && !cut.next.empty() && !cut.terseContinuation ) + if( cut.pastTargetDisclosed && cut.pastTarget > 0 ) + { + notes += kForPastTargetNote; + } + if( cut.isCapped && !cut.next.empty() ) { notes += kForSigsNextNote; } @@ -5293,67 +5411,6 @@ 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 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 ) -{ - if( req == nullptr || !req->charged ) - { - return 0; - } - if( req->pasteHandle ) - { - return valueAttrBytes; // terse: next= alone (sigsCutAttachContinuation) - } - 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 bool sigsCutPaysFromRows( const SigsCutContinuation* req ) noexcept -{ - return req != nullptr && req->charged && req->payFromRows; -} -inline std::size_t sigsCutPlanReserve( const SigsCutContinuation* req ) noexcept -{ - return sigsCutPaysFromRows( req ) ? req->ledgerGapBytes : 0u; -} -// train 27: what a payment from the rows gave up PAST what it owed — the ladder drops whole rows, so a 90 B handle can cost a -// 150 B row. A caller that measures its paid document still over its budget owes that overrun ON TOP of what was given up. -inline std::size_t sigsCutPaidSurplus( std::size_t totalBefore, std::size_t totalAfter, std::size_t owed, std::size_t unpaid ) noexcept -{ - const std::size_t given = totalBefore > totalAfter ? totalBefore - totalAfter : 0u; - const std::size_t paid = owed > unpaid ? owed - unpaid : 0u; - const std::size_t surplus = given > paid ? given - paid : 0u; - ENSURES( surplus <= given, "sigsCutPaidSurplus: the surplus exceeds what the rows gave up" ); - return surplus; -} -// train 27: the row-side share beside the handle — the caller's measured overrun (SigsCutContinuation::overrunBytes), owed only by -// a capped block whose caller lets rows pay; what the rank 1..4 floor cannot give up is reported with the handle's unpaid bytes -inline std::size_t sigsCutRowOverrun( const SigsCutContinuation* req ) noexcept -{ - return sigsCutPaysFromRows( req ) ? req->overrunBytes : 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): 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 ) -{ - 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. inline std::string sigsOpenTag( const SigsCutReport& cut ) @@ -5365,6 +5422,11 @@ inline std::string sigsOpenTag( const SigsCutReport& cut ) rw::formatTo( nb, sizeof( nb ), " shown=\"{}\" total=\"{}\" capped=\"1\"", cut.shown, cut.total ); tag += nb; } + if( cut.pastTargetDisclosed && cut.pastTarget > 0 ) // forbudget-soft: the rows served past the default target + { + rw::formatTo( nb, sizeof( nb ), " past_target=\"{}\"", cut.pastTarget ); + tag += nb; + } if( cut.docsDropped > 0 ) { rw::formatTo( nb, sizeof( nb ), " docs_dropped=\"{}\"", cut.docsDropped ); @@ -5490,10 +5552,16 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // on GLOBAL rank), so pages merge without duplicate r= { const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; + // forbudget-soft: the two other INPUTS the report carries (the row-drop budget and whether past_target= is printed), read + // before the reset like the continuation request + const std::size_t rowDropBudgetIn = cutOut != nullptr ? cutOut->rowDropBudgetBytes : 0u; + const bool pastTargetDisclosed = cutOut != nullptr && cutOut->pastTargetDisclosed; if( cutOut ) { *cutOut = SigsCutReport {}; cutOut->continuationRequest = cutNext; + cutOut->rowDropBudgetBytes = rowDropBudgetIn; + cutOut->pastTargetDisclosed = pastTargetDisclosed; } if( droppedPositiveOut ) { @@ -5794,34 +5862,25 @@ 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, 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 + // knob-honesty-068: the continuation a capped tag carries — sized from the UNTRIMMED block; exempt (a disclosure never + // costs a row), so the plan reserves nothing for it 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 + sigsCutPlanReserve( cutNext ), + sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1, sizeof( " docs_dropped=\"\"" ) - 1 ); + SigLadderOutcome ladder; if( plan.ladderFires ) { // one ladder ACTION on one entry, tail-first; every action re-checks the budget so the ladder // stops at the first fitting state. Pure function of (global rank, the kFor* constants) — the // 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 (paySigsContinuationFromRows); what the floor - // cannot pay rides anyway and is reported for the caller's over_ceiling label - // …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 + sigsCutRowOverrun( cutNext ) : 0u; - const std::size_t totalBeforePay = total; - const std::size_t cutNextUnpaid = sigsCutPaysFromRows( cutNext ) - ? paySigsContinuationFromRows( entries, sigFiles, total, cutNextOwed, entryCost ) - : cutNextOwed; - const std::size_t cutNextSurplus = sigsCutPaysFromRows( cutNext ) ? sigsCutPaidSurplus( totalBeforePay, total, cutNextOwed, cutNextUnpaid ) : 0u; + // forbudget-soft: step F aims at the caller's row-drop budget when it asked for a wider one (the --for default's + // runaway guard, an explicit soft target); the shrink steps keep aiming at payloadBudgetBytes + ladder = trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, entryCost, + sigsRowDropEffective( plan, payloadBudgetBytes, rowDropBudgetIn, totalRows ) ); + } + const std::size_t totalAfterLadder = total; // 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. @@ -5869,9 +5928,9 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext, spelling.elideZeroMetrics }, esc, rootArg ); } ); } - cut.continuationRequest = cutNext; - cut.continuationUnpaidBytes = cutNextUnpaid; - cut.continuationPaidSurplus = cutNextSurplus; + cut.continuationRequest = cutNext; + sigsCutRecordLadder( cut, ladder, totalAfterLadder, rowDropBudgetIn, pastTargetDisclosed ); // forbudget-soft + sigsCutRecordPastTargetRanks( cut, entries ); // forbudget-soft: after the docs reorder renumbered them sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { @@ -6481,6 +6540,10 @@ struct CalleeCallsSink // every pre-existing caller is byte-identical. `cappedNext` is already XML-escaped and written only when cut. std::uint32_t rowCap = 16; std::string_view cappedNext; + // attrib-multidef Q1: Graph::outProv (parallel to outTargets), so a callee row whose edge is a split arm carries prov="split"; + // nullptr / empty ⇒ no row is marked. `splitLegendCharged`: as viaLegendCharged, for its comment (kForSplitClause). + const std::vector* prov = nullptr; + bool* splitLegendCharged = nullptr; }; // The wrapper, written in front of the rows it describes (they have to be walked before `shown` @@ -6625,17 +6688,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 + bool split = false; // attrib-multidef Q1: these edges are split arms — likewise }; inline void collectCalleeNameRow( std::vector& rows, const Symbol& cs, - std::size_t& used, const CalleeCallsSink& sink, bool via = false ) + std::size_t& used, const CalleeCallsSink& sink, bool via = false, bool split = false ) { char lb[ 16 ]; rw::formatTo( lb, sizeof( lb ), "{}", cs.line ); bool merged = false; for( MergedCalleeNameRow& r : rows ) { - if( r.name == cs.name && r.via == via ) + if( r.name == cs.name && r.via == via && r.split == split ) { r.lines.push_back( cs.line ); merged = true; break; @@ -6643,10 +6707,11 @@ inline void collectCalleeNameRow( std::vector& rows, const } if( !merged ) { - rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via } ); + rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via, split } ); } - // 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 ); + // the comma and the digits a merge appends, or the whole row it opens (FE-B: plus its 11-byte via="name"; Q1: its 13-byte + // prov="split") + used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16 + ( via ? 11u : 0u ) + ( split ? 13u : 0u ); if( sink.recorded ) { sink.recorded->push_back( EmittedBodyCall { cs.name, cs.line, std::string() } ); // §H5: no sig to record @@ -6674,6 +6739,7 @@ inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector& outOff, const std::v return via; } +// attrib-multidef Q1: the per-edge bytes a section's rows read — Graph::outNameOnly (via="name") and, where the caller +// asks for it, Graph::outProv (prov="split"), with the flag its prov comment's one-time charge sets (packHops passes its own to +// the owner rows). Built implicitly from outNameOnly alone, so every caller that marks no split arm passes what it always did. +struct CalleeEdgeBytes +{ + const std::vector& nameOnly; + const std::vector* prov = nullptr; + bool* splitLegendCharged = nullptr; + CalleeEdgeBytes( const std::vector& nameOnlyBytes, const std::vector* provBytes = nullptr, // NOLINT: implicit + bool* charged = nullptr ) noexcept + : nameOnly( nameOnlyBytes ), prov( provBytes ), splitLegendCharged( charged ) {} +}; +// …and which of their readings the caller's own first-screen legend already carries (FE-B's via="name", Q1's prov="split"), so +// the section neither repeats nor charges them. Built implicitly from the via bit alone, as every pre-Q1 caller passes it. +struct CalleeLegendsInHead +{ + bool via = false; + bool split = false; + CalleeLegendsInHead( bool viaInHead = false, bool splitInHead = false ) noexcept : via( viaInHead ), split( splitInHead ) {} // NOLINT: implicit +}; + +// attrib-multidef Q1: the sink a section passes, with the split-arm bytes it reads and the flag its comment charge sets +inline CalleeCallsSink withSplitArms( CalleeCallsSink sink, const std::vector* prov, bool* splitLegendCharged ) noexcept +{ + sink.prov = prov; + sink.splitLegendCharged = splitLegendCharged; + return sink; +} + +// attrib-multidef Q1: is the row id → cid a split arm (and not by name alone: that row says via="name")? The first +// such row of an answer charges the comment that defines it to `used`, before the row, when the head did not define it. +inline std::string_view calleeRowSplitAttr( const std::vector& outOff, const std::vector& outTargets, NodeId id, NodeId cid, + bool via, std::size_t& used, const CalleeCallsSink& sink ) +{ + if( via || sink.prov == nullptr || sink.prov->empty() || std::size_t( id ) + 1 >= outOff.size() ) + { + return {}; + } + const auto b = outTargets.begin() + outOff[ id ]; + const auto e = outTargets.begin() + outOff[ std::size_t( id ) + 1 ]; + const auto it = std::lower_bound( b, e, cid ); // ascending within a source (buildGraph sorts by (from, to)) + const bool split = it != e && *it == cid && ( *sink.prov )[ std::size_t( it - outTargets.begin() ) ] == 3u; + if( split && sink.splitLegendCharged != nullptr && !*sink.splitLegendCharged ) + { + used += splitArmCallsLegendComment().size(); + *sink.splitLegendCharged = true; + } + return split ? std::string_view( " prov=\"split\"" ) : std::string_view(); +} + // §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- @@ -6730,11 +6846,12 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector } const Symbol& cs = ing.symbols[cid]; const std::string_view viaAttr = viaNameAttr( calleeRowVia( outOff, outTargets, id, cid, used, sink ) ); // FE-B: via="name" or "" + const std::string_view splitAttr = calleeRowSplitAttr( outOff, outTargets, id, cid, !viaAttr.empty(), used, sink ); // Q1: prov="split" 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, !viaAttr.empty() ); + collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty(), !splitAttr.empty() ); ++shown; continue; } @@ -6749,10 +6866,10 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector { continue; } - char hb[ 48 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}>", cs.line, viaAttr ); + char hb[ 64 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}{}>", cs.line, viaAttr, splitAttr ); 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" + const CalleeEdgeBytes& outEdges, // FE-B: Graph::outNameOnly — the rows' via="name" (+ Q1's outProv) bool compress = false, RedactCounts* redact = nullptr, const HashMap* ranges = nullptr, const notes::NoteIndex* noteIndex = nullptr, // L3: field notes — surfaces children on each @@ -7130,10 +7247,14 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // on --for/--pack-task/--from-trace, calleeNameSpecificity on // --expand; nullptr (--around/--exemplar) ⇒ node-id order. // See CalleeCallsSink::rank. - bool viaLegendInHead = false ) // FE-B: the caller's own first-screen legend already defines via="name" - // (namesOnlyOutAny above), so this section neither repeats nor charges it + CalleeLegendsInHead legendsInHead = {} ) // FE-B: the caller's own first-screen legend already defines via="name" + // (namesOnlyOutAny above), so this section neither repeats nor charges + // it; Q1: likewise prov="split" (splitArmsOutAny) { + const std::vector& outNameOnly = outEdges.nameOnly; + const bool viaLegendInHead = legendsInHead.via; bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) + bool splitLegendCharged = legendsInHead.split; // attrib-multidef Q1: likewise for the prov="split" comment // 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). @@ -7430,7 +7551,8 @@ 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, &viaLegendCharged } ); + withSplitArms( CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, + &viaLegendCharged }, outEdges.prov, &splitLegendCharged ) ); 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 @@ -7506,6 +7628,10 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect { w.write( viaNameLegendComment() ); // FE-B: the same rule, for a row's via="name" } + if( !legendsInHead.split && children.find( " prov=\"split\"" ) != std::string::npos ) + { + w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: the same rule, for a row's prov="split" + } w.write( children ); w.write( "" ); w.flush(); @@ -7558,10 +7684,11 @@ struct OwnerHopRows template inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector& nodes, const HopOwners* owners, const std::vector& outOff, const std::vector& outTargets, - const std::vector& outNameOnly, RedactCounts* redact, const std::vector* rank, + const CalleeEdgeBytes& outEdges, RedactCounts* redact, const std::vector* rank, std::string_view rootArg, std::size_t rankedBudget, bool rankedVia, PathRelFn&& pathRel, ContentOfFn&& contentOf, std::vector& esc ) { + const std::vector& outNameOnly = outEdges.nameOnly; OwnerHopRows out; out.rankedBudget = rankedBudget; out.rankedVia = rankedVia; @@ -7599,8 +7726,8 @@ inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector } used += rowFixed; emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budget, - CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, - kForOwnerHopNameGuard, next } ); + withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, + kForOwnerHopNameGuard, next }, outEdges.prov, outEdges.splitLegendCharged ) ); out.xml += row; out.xml += ""; ++out.shown; @@ -7670,14 +7797,17 @@ inline std::string hopsOpenTag( std::size_t shown, std::size_t requested, std::s 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" + const CalleeEdgeBytes& outEdges, // FE-B: Graph::outNameOnly — the rows' via="name" (+ Q1's outProv) RedactCounts* redact = nullptr, 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 viaLegendInHead = false, // FE-B: packBodies' parameter of the same name + CalleeLegendsInHead legendsInHead = {}, // FE-B (+ Q1): packBodies' parameter of the same name const HopOwners* owners = nullptr ) // owner-hop: the leading owner rows; nullptr ⇒ none (byte-identical) { + const std::vector& outNameOnly = outEdges.nameOnly; + const bool viaLegendInHead = legendsInHead.via; bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) + bool splitLegendCharged = legendsInHead.split; // attrib-multidef Q1: likewise for the prov="split" comment if( budgetBytes == 0 ) // 0 ⇒ UNLIMITED, packBodies' own convention { budgetBytes = SIZE_MAX; @@ -7721,8 +7851,8 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // owner-hop: the leading owner rows first (THE QUESTION'S OWNER ROWS above; nothing when `owners` is null), then the // ranked rows exactly as before, from the node the owners end at, on the budget and via charge the owners leave them - const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, outNameOnly, redact, rank, rootArg, - budgetBytes, viaLegendCharged, pathRel, contentOf, esc ); + const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, CalleeEdgeBytes( outNameOnly, outEdges.prov, &splitLegendCharged ), + redact, rank, rootArg, budgetBytes, viaLegendCharged, pathRel, contentOf, esc ); std::string children = own.xml; std::size_t requestedCount = own.shown; std::size_t shownCount = own.shown; @@ -7773,7 +7903,8 @@ 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, &viaLegendCharged } ); + withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged }, + outEdges.prov, &splitLegendCharged ) ); row += ""; children += row; ++shownCount; @@ -7789,6 +7920,10 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector { w.write( viaNameLegendComment() ); // FE-B: exactly when a hop's row carries via="name" } + if( !legendsInHead.split && children.find( " prov=\"split\"" ) != std::string::npos ) + { + w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: exactly when a hop's row carries prov="split" + } w.write( openTag ); w.write( children ); w.write( "" ); @@ -8520,6 +8655,18 @@ inline ForSectionStubPlan planForSectionStubs( std::string_view sections, std::s return p; } +// round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST): under a soft target a stubbable section is +// served whole when the two sections' measured renders fit what the rows at the target cut left of the bundle budget +// (`shareBytes`). The collapse exists to save bytes a hard ceiling could not spare; a section that fits its share keeps it, +// because a stub there serves less than the retired ceiling did (forbudget-soft §B5: s078/s083, a 1-interface became a +// stub once the soft target's wider rows widened its scope). Otherwise the caller's --sections= stands and the priced +// collapse (planForSectionStubs) decides. The default regime never calls this: its stub posture is unchanged. +inline std::string_view forSoftSectionsOptIn( std::string_view sections, std::size_t legoBytes, std::size_t composeBytes, + std::size_t shareBytes ) noexcept +{ + return legoBytes + composeBytes <= shareBytes ? std::string_view( "lego,compose" ) : sections; +} + // lego-charge (K18 root cause): what / cost the budget, computed BEFORE the sigs render. It used to be // the full pre-rendered bytes, while the default answer serves a collapsing section as a ~120 B stub: textual's 6.5 KB lego // left 71 B for (4 rows shipped in a 4.6 KB answer under a 7.5 KB ceiling). Now a section is charged as it is SERVED. @@ -8771,6 +8918,42 @@ inline std::vector> legoImplementorsOnSurface( const IngestR // every --for run whose rendered sigs happened to hit this path at all. Caught by legobundlecheck.sh going // red for the wrong reason (an empty lego, not a mis-narrowed one) rather than by a dedicated assertion — // worth a gate of its own if this class recurs. +// forbudget-soft: the rows a DEFAULT answer's sections are scoped by — the rows at the target cut, without the +// past_target= rows (the K highest ranks: the rows are a rank prefix), so / narrow as they did when the target +// was a ceiling and keep the shape they had (§1.3's section invariant: no section loses rows because the head was kept whole). +// Selected by the r= values the ladder recorded for the past-target rows (cut.pastTargetRanks), not by emission position nor +// by an r= threshold: the docs reorder (docs_after_code=) moves doc rows below the code rows AND renumbers them. +// A soft target scopes them the same way at ITS target cut (round C, reserve first: the rows the retired ceiling served). +// The whole block on an answer with nothing past its target. Only … rows are kept (a file's +// notes ride its rows and are scanned for p= the same way). +inline std::string sigsRowsAtTarget( std::string_view sigsRendered, const SigsCutReport& cut ) +{ + if( cut.pastTargetRanks.empty() || cut.pastTarget >= cut.shown ) + { + return std::string( sigsRendered ); + } + std::string kept; + for( std::size_t at = sigsRendered.find( "", at ); + if( end == std::string_view::npos ) + { + break; + } + const std::string_view text = sigsRendered.substr( at, end + 4 - at ); + const std::size_t rAt = text.find( " r=\"" ); + std::uint32_t rank = 0; + for( std::size_t i = rAt == std::string_view::npos ? text.size() : rAt + 4; i < text.size() && text[ i ] >= '0' && text[ i ] <= '9'; ++i ) + { + rank = rank * 10 + std::uint32_t( text[ i ] - '0' ); + } + if( !std::binary_search( cut.pastTargetRanks.begin(), cut.pastTargetRanks.end(), rank ) ) + { + kept.append( text ); + } + } + return kept; +} inline bool narrowLegoToRenderedSigs( const IngestResult& ing, std::vector>& legoScoped, std::string_view sigsRendered, std::string_view rootPrefix = {} ) { @@ -8921,10 +9104,27 @@ struct LegoTargetedArgs int pageLimit = 0; // --limit / MCP limit over the deeper implementor rows (0 = the default page) int pageOffset = 0; // --offset / MCP offset const std::vector>* implementorBound = nullptr; // fix 1: Graph::implementorBound (null: nothing proven) + std::span mainMixins; // lane ruby-pair: Graph::rubyMainMixins (sorted by base) — the main-object rows (withMainRows) LegoTargetedArgs() = default; LegoTargetedArgs( std::string_view floorAttr, int limit = 0, int offset = 0, const std::vector>* bound = nullptr ) : graphCountFloorAttr( floorAttr ), pageLimit( limit ), pageOffset( offset ), implementorBound( bound ) {} + LegoTargetedArgs&& withMainRows( std::span rows ) && noexcept + { + mainMixins = rows; + return std::move( *this ); + } }; + +// lane ruby-pair: the main-object implementor rows of base `id` (LegoTargetedArgs::mainMixins), in (file, line) order. +inline std::span legoMainRowsOf( std::span rows, NodeId id ) noexcept +{ + const auto [ first, last ] = std::ranges::equal_range( rows, id, {}, &RubyMainMixinRow::base ); + return { first, last }; +} + +// The present-only legend clause of a main-object row (no `--` inside: it rides in an XML comment). +inline constexpr std::string_view kLegoMainRowClause = + ""; inline const std::vector> kNoImplementorBound{}; // the empty bound map: every edge reads ByName // One row. `line` (S2): the TARGETED answer puts the definition's line on p= (p="file:LINE", the --uses and @@ -9082,10 +9282,11 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector : ( impls.size() < 16 ? impls.size() : 16 ); const std::size_t implementorTotal = id < countMap.size() ? std::max( countMap[id].size(), impls.size() ) : impls.size(); std::string hdr; + const std::span mainRows = focusId != kNoNode ? legoMainRowsOf( targeted.mainMixins, id ) : std::span {}; const LegoClosurePage deep = focusId != kNoNode ? legoClosurePage( implementors, targeted.implementorBound != nullptr ? *targeted.implementorBound : kNoImplementorBound, id, targeted.pageLimit, targeted.pageOffset ) : LegoClosurePage{}; if( focusId != kNoNode ) { - hdr = "\" defs=\"" + std::to_string( definitionCountOfName( ing, id ) ) + "\" implementors=\"" + std::to_string( impls.size() ) + "\"" + hdr = "\" defs=\"" + std::to_string( definitionCountOfName( ing, id ) ) + "\" implementors=\"" + std::to_string( impls.size() + ( mainRows.empty() ? 0u : 1u ) ) + "\"" + legoClosureAttrs( deep, std::string( pathRel( isym.fileId ) ) + ":" + isym.name, deep.unwalked.empty() ? std::string() : std::string( pathRel( ing.symbols[ deep.unwalked.front() ].fileId ) ) + ":" + ing.symbols[ deep.unwalked.front() ].name, targeted.pageLimit ); @@ -9179,6 +9380,17 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector { w.write( "" ); } + // train 28 (ruby-pair review): one row per directive (each p= a site), but ONE implementor — every top-level directive + // mixes into the same main object, so implementors= above counts it once however many rows list it + for( const RubyMainMixinRow& m : mainRows ) // targeted only (empty otherwise): after the class rows, before the deeper ones + { + w.write( "" ); + } + if( !mainRows.empty() ) + { + w.write( kLegoMainRowClause ); + } for( std::size_t j = deep.window.begin; j < deep.window.end; ++j ) // empty unless TARGETED with a deeper row { writeLegoImplRow( w, ing, deep.deeper[j].id, pathRel( ing.symbols[ deep.deeper[j].id ].fileId ), withPaths, true, &deep.deeper[j], false, esc ); @@ -9659,8 +9871,8 @@ inline void emitImpactFileRollupXml( std::FILE* out, const IngestResult& ing, co { const std::string_view raw = ing.files[ r.rows[i].fileId ]; const std::string_view rel = rootPrefix.empty() ? raw : rw::sarif::rootRelativeUri( raw, rootPrefix ); - rw::emitTo( out, "", std::string( escapeXml( rel, esc ) ).c_str(), r.rows[i].syms, r.rows[i].minDepth, - impactRollupHedgeXml( r.rows[i] ) ); // fix27: the symbol rows' via="name", kept through the fold + rw::emitTo( out, "", std::string( escapeXml( rel, esc ) ).c_str(), r.rows[i].syms, r.rows[i].minDepth, + impactRollupHedgeXml( r.rows[i] ), r.rows[i].ofDefXml ); // fix27: the symbol rows' via="name", kept through the fold; attrib-multidef } rw::emitRaw( out, "" ); } @@ -9680,8 +9892,8 @@ inline void emitImpactFileRollupJson( std::FILE* out, const IngestResult& ing, c { const std::string_view raw = ing.files[ r.rows[i].fileId ]; const std::string_view rel = rootPrefix.empty() ? raw : rw::sarif::rootRelativeUri( raw, rootPrefix ); - rw::emitTo( out, "{}{{\"p\":\"{}\",\"syms\":{},\"d\":{}{}}}", i ? "," : "", jsonStr( rel ).c_str(), r.rows[i].syms, r.rows[i].minDepth, - impactRollupHedgeJson( r.rows[i] ) ); // fix27: as the XML row + rw::emitTo( out, "{}{{\"p\":\"{}\",\"syms\":{},\"d\":{}{}{}}}", i ? "," : "", jsonStr( rel ).c_str(), r.rows[i].syms, r.rows[i].minDepth, + impactRollupHedgeJson( r.rows[i] ), r.rows[i].ofDefJson ); // fix27: as the XML row; attrib-multidef } rw::emitRaw( out, "]}" ); } @@ -10820,6 +11032,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s // knob-honesty-068: continuationRequest is an INPUT { const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; + const std::size_t rowDropBudgetIn = cutOut != nullptr ? cutOut->rowDropBudgetBytes : 0u; // forbudget-soft: the XML twin's inputs + const bool pastTargetDisclosed = cutOut != nullptr && cutOut->pastTargetDisclosed; const bool rankAdaptivePayload = lens.rankAdaptivePayload; if( outCapped ) { @@ -10829,6 +11043,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s { *cutOut = SigsCutReport {}; cutOut->continuationRequest = cutNext; // knob-honesty-068: the request survives the reset + cutOut->rowDropBudgetBytes = rowDropBudgetIn; + cutOut->pastTargetDisclosed = pastTargetDisclosed; } if( droppedPositiveOut ) { @@ -10911,28 +11127,19 @@ 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, charged or exempt by the XML twin's rule (SigsCutContinuation; no legend here) + // knob-honesty-068: the "sigs_next" key, 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 + sigsCutPlanReserve( cutNext ), + sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1, sizeof( ",\"docs_dropped\":" ) - 1 ); + SigLadderOutcome ladder; if( plan.ladderFires ) { - trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, 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 - // …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 + sigsCutRowOverrun( cutNext ) : 0u; - const std::size_t totalBeforePay = total; - const std::size_t cutNextUnpaid = sigsCutPaysFromRows( cutNext ) - ? paySigsContinuationFromRows( entries, sigFiles, total, cutNextOwed, jsonSigEntryCost ) - : cutNextOwed; - const std::size_t cutNextSurplus = sigsCutPaysFromRows( cutNext ) ? sigsCutPaidSurplus( totalBeforePay, total, cutNextOwed, cutNextUnpaid ) : 0u; + ladder = trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, jsonSigEntryCost, + sigsRowDropEffective( plan, payloadBudgetBytes, rowDropBudgetIn, totalRows ) ); // forbudget-soft: the XML twin's split + } + const std::size_t totalAfterLadder = total; if( outCapped ) { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped @@ -10946,9 +11153,9 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s [ & ]( JsonSigEntry& e, std::uint32_t newRank ) { e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); } ); } - cut.continuationRequest = cutNext; - cut.continuationUnpaidBytes = cutNextUnpaid; - cut.continuationPaidSurplus = cutNextSurplus; + cut.continuationRequest = cutNext; + sigsCutRecordLadder( cut, ladder, totalAfterLadder, rowDropBudgetIn, pastTargetDisclosed ); // forbudget-soft + sigsCutRecordPastTargetRanks( cut, entries ); // forbudget-soft: after the docs reorder renumbered them sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for if( cutOut ) { diff --git a/src/slicediff.h b/src/slicediff.h index 996c16d86..195bb510f 100644 --- a/src/slicediff.h +++ b/src/slicediff.h @@ -329,7 +329,7 @@ inline std::string blobAtRev( const std::string& root, const std::string& sha, c { return {}; } - return quality::gitOneLine( root, "show " + shSingleQuote( sha + ":" + p ) + " 2>/dev/null" ); + return quality::gitOneLine( root, "show --no-ext-diff --no-textconv " + shSingleQuote( sha + ":" + p ) + " 2>/dev/null" ); }; std::string blob = read( rel ); if( !blob.empty() ) diff --git a/src/taskroute.h b/src/taskroute.h index 820df9003..b8d701009 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -1248,6 +1248,16 @@ inline constexpr std::string_view kCalendarWords[] = { inline constexpr std::string_view kExplanatoryCues[] = { "how does", "how do", "how is", "how are", "what does", "implementation of", }; +// Does the (lower-cased) task carry an explanatory cue, word-bounded? The history router's veto below and the --for body slot +// (forowner.h forBodySlotAsked) read the one predicate. +inline bool hasExplanatoryCue( std::string_view lower ) noexcept +{ + // the house cue scorer (cueScore), word-bounded, over the six cues — any hit is a cue + static_assert( std::size( kExplanatoryCues ) == 6, "hasExplanatoryCue lists every explanatory cue" ); + return cueScore( lower, CueMatch::WordBounded, + { { kExplanatoryCues[0], 1 }, { kExplanatoryCues[1], 1 }, { kExplanatoryCues[2], 1 }, + { kExplanatoryCues[3], 1 }, { kExplanatoryCues[4], 1 }, { kExplanatoryCues[5], 1 } } ) > 0; +} // `since Monday`, `since 2026-09-01` — structural, because a date has no paraphrase (the same reasoning // firstFileLineToken is built on). Word-bounded, so `sincerely` is not a window. @@ -1309,8 +1319,7 @@ inline std::optional recencyTaskChoice( std::string_view task, std: // read as history. Recognised as the phrase it is, and only in the question forms that mean it. const bool newInPhrase = has( lower, "what is new in" ) || has( lower, "what's new in" ) || has( lower, "whats new in" ) || has( lower, "anything new in" ); - const bool explanatory = std::any_of( std::begin( kExplanatoryCues ), std::end( kExplanatoryCues ), - [lower]( const std::string_view cue ) { return boundedFind( lower, cue ) != std::string_view::npos; } ); + const bool explanatory = hasExplanatoryCue( lower ); if( ( ( timeScore < 5 || motionScore < 6 ) && !newInPhrase ) || explanatory ) { return std::nullopt; diff --git a/src/tracelocus.h b/src/tracelocus.h index 74d04c899..fe8d66702 100644 --- a/src/tracelocus.h +++ b/src/tracelocus.h @@ -1169,9 +1169,21 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph // M11: the root's machine-readable price, defined beside the ledger prose. // F5: "it" used to have three possible referents on this root. over_ceiling= names the ONE comparison // it is: est_tokens against the smallest ceiling this root prints. - h += "On the root: est_tokens= prices the delivered bundle in tokens, budget_tokens= is the token target you passed (absent " - "when none), max_tokens= is the body ceiling you passed via the max_tokens flag (absent when none); over_ceiling= is 1 when " - "est_tokens exceeds the smallest ceiling named here (the bundle is then complete, not trimmed)."; + // forbudget-soft (decision D-5): the caller's budget_tokens= is a SOFT target — sections are shaped toward it, nothing is + // labelled over it and the header is never dropped for it; max_tokens= stays the hard body ceiling. A bundle without a + // budget of the caller's keeps its reading byte for byte. + if( in.budgetTokens > 0 ) + { + h += "On the root: est_tokens= prices the delivered bundle in tokens, budget_tokens= is the soft token target you passed " + "(est_tokens may exceed it, the header always ships), max_tokens= is the body ceiling you passed via the max_tokens " + "flag (absent when none); over_ceiling= is 1 when est_tokens exceeds max_tokens (the bundle is then complete, not trimmed)."; + } + else + { + h += "On the root: est_tokens= prices the delivered bundle in tokens, budget_tokens= is the token target you passed (absent " + "when none), max_tokens= is the body ceiling you passed via the max_tokens flag (absent when none); over_ceiling= is 1 when " + "est_tokens exceeds the smallest ceiling named here (the bundle is then complete, not trimmed)."; + } h += extraNotes; if( in.notesDegraded ) { @@ -1292,9 +1304,8 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph // nothing else, so the attribute and the numbers beside it can always be reconciled by subtraction. // 0 = the caller named none (the working budget is the lens's own default, stated in the ledger prose // in bytes) and the label is then withheld rather than pointed at a number the root does not carry. - std::size_t namedCeiling = 0; - if( in.budgetTokens > 0 ) { namedCeiling = in.budgetTokens; } - if( in.maxTokens > 0 ) { namedCeiling = namedCeiling > 0 ? std::min( namedCeiling, in.maxTokens ) : in.maxTokens; } + // forbudget-soft (D-5): budget_tokens= is a soft target, never a ceiling this root labels against — max_tokens= is + const std::size_t namedCeiling = in.maxTokens; const auto rootAttrsFor = [ & ]( const std::string& doc, bool widestSpelling ) -> std::string { std::size_t est = 0; @@ -1322,7 +1333,9 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph spliceRootAttrs( candidate, rootAttrsFor( candidate, /*widestSpelling=*/true ) ); return pricedBytesOf( traceDeliveredBytes( candidate ) ); }; - const CeilingLadderChoice chosen = in.compactLegend + // forbudget-soft (D-5): under the caller's soft budget no ladder runs — the src echo always ships + const CeilingLadderChoice chosen = in.budgetTokens > 0 ? CeilingLadderChoice{ headerStr, CeilingRung::AsBuilt } + : in.compactLegend ? climbDeliveredLadder( buildRung, headerStr, whole.substr( headerStr.size() ), deliveredOf, ceilingAllowanceFromBudgetBytes( bundleBudget ), kNotes ) : climbCeilingLadder( buildRung, diff --git a/src/tsprobe.cpp b/src/tsprobe.cpp index 395a62418..688338a3d 100644 --- a/src/tsprobe.cpp +++ b/src/tsprobe.cpp @@ -150,7 +150,7 @@ int main( int argc, char** argv ) { // A value use (a function stored or passed) and a call THROUGH a value are not call references: this dump is // the pre-resolution CALL list test/callformcheck.sh reads, so they stay out of it (src/valuerefs.h serves them). - if( r.fromSymbol != rw::kNoNode && r.role != rw::RefRole::Value && r.role != rw::RefRole::Through ) + if( r.fromSymbol != rw::kNoNode && r.role != rw::RefRole::Value && r.role != rw::RefRole::Through && r.role != rw::RefRole::Subscript ) { bySym[ r.fromSymbol ].push_back( &r ); } diff --git a/src/valuerefindex.h b/src/valuerefindex.h index cdf1c79e4..0d9887bcc 100644 --- a/src/valuerefindex.h +++ b/src/valuerefindex.h @@ -841,7 +841,7 @@ class UsesValueFilter // True when reference `refIndex` is NOT a use-site row of this answer. bool skip( std::uint32_t refIndex, const Reference& r ) const { - if( r.role == RefRole::Through ) + if( r.role == RefRole::Through || r.role == RefRole::Subscript ) // a subscript is a use only as the call it may resolve to { return true; } diff --git a/src/verbs_doctor.h b/src/verbs_doctor.h index f3c2b432f..e9929efdf 100644 --- a/src/verbs_doctor.h +++ b/src/verbs_doctor.h @@ -429,7 +429,11 @@ inline const char* doctorLegendComment() "appended to git's environment override for this run (stderr said so as git_harden=fsmonitor-hook); " "builtin, off and unset need no override and read neutralised=\"0\". Independently of this row, every git command " "ripwire runs carries no-optional-locks and core.fsmonitor=false, so no monitor of either form runs for its " - "read-only calls. " + "read-only calls. filter_drivers counts the filter drivers the repository's own config defines; when it is not 0, " + "filter_scope is local (the repository's config) or all (a git older than 2.26 cannot report scope, so the " + "user's own drivers are pinned too) and filter_pinned is 1 (every git child runs them with an empty command, " + "so the files are read as stored; stderr said git_harden=filter-driver) or refused (a name git's -c option " + "cannot spell: every git child is refused instead). " "layout's state=\"agree\" means the layout records match; agree compares only the types= registered in src/model.h; " "a same-size layout change or a stale constant is invisible, so agree does not rule out a mixed binary; " "checked=\"1\" means the comparison ran; " @@ -800,6 +804,14 @@ inline std::string doctorGitConfigTrustAttrs( const rw::Config& cfg ) const bool neutralised = form == githarden::FsmonitorForm::Hook && githarden::g_startup.applied; std::string attrs = "fsmonitor=\"" + std::string( githarden::fsmonitorFormName( form ) ) + "\""; attrs += " neutralised=\"" + std::string( neutralised ? "1" : "0" ) + "\""; + // The repository's own filter drivers, as the startup probe found and pinned them (githarden.h FILTER DRIVERS). + const githarden::FilterDriverOutcome filters = githarden::startupFiltersFor( cfg.rootPath ); + attrs += " filter_drivers=\"" + std::to_string( filters.drivers ) + "\""; + if( filters.drivers > 0 ) + { + attrs += std::string( " filter_scope=\"" ) + ( filters.allScopes ? "all" : "local" ) + "\""; + attrs += std::string( " filter_pinned=\"" ) + ( filters.refused ? "refused" : "1" ) + "\""; + } return attrs; } diff --git a/src/verbs_for.h b/src/verbs_for.h index a14ab77a9..5957b815d 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -5,6 +5,7 @@ #include "infra/emit.h" // rw::emitTo / emitRaw / formatTo — THE emitter and its siblings #include "forpage.h" // L-W: the --for --limit=N file page, coverage=, the thin rule and the widening next= +#include "forowner.h" // the question's owner (owner-hop's found set) and the one owner body of an explanatory question #include // the redaction-tally snapshots a degraded pre-render restores #include // %.*s (precision, pointer) collapses to one view @@ -414,10 +415,11 @@ struct ForLensJsonInputs // The R-E harvest reached the XML --for bundle but left this JSON sibling's packSignaturesJson call // spelling `/*rootArg=*/{}` — the two dialects of ONE question disagreed about the spelling of a path. std::string_view rootArg; - // DEEP-TAIL d2: the file-grain tail candidates (computed once, above the dialect split, from the same - // resolved surface selects). REQUIRED, not defaulted — a defaulted pointer is how a dialect + // DEEP-TAIL d2: the file-grain tail (FOLD C: page 0 of the --for file page, computed once above the dialect split, so + // both dialects serve the same rows). REQUIRED, not defaulted — a defaulted pointer is how a dialect // silently loses a surface its XML twin serves. const rw::FileTail* fileTail; + const rw::LexTermEvidence* evidence; // FOLD C: the term evidence the tail's page rows rank by (the lens run's own) // L3 follow-up (CodeRabbit 4053600616): read BEFORE the caller nulls noteIndex for emptiness (d.notesDegraded, // 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. @@ -512,19 +514,11 @@ struct ForLensHeaderParts bool autoBundle = false; // T3: auto mode is on (cfg.detail==0, no --signatures-only) — appends the bundle=auto legend bool compactBundle = false; // COMPACT conceptual serving: appends the bundle=compact legend INSTEAD of the auto one — never both, because only one of the two sections can be emitted bool compactLegend = false; // --legend=compact: versioned, shorter explanatory dialect - bool tailLegend = true; // deep-tail: the r=/ legend clause. Cleared ONLY by the - // ceiling ladder's rung zero (with the confidence clause): - // the facts (r= attrs, the element) survive, only the - // explainer goes — the L1 "first rung that costs no unique - // information" ordering. - bool idRouteLegend = true; // row 6 (2026-09-12): the sc=/route= readings (graphlegend.h - // kForIdRouteLegend) — ceiling-droppable exactly like the two - // above; rung zero clears all three and the note names them - bool legendDropped = false; // …and rung zero SAYS SO: set with the two clears above, it splices - // kForLegendDroppedNote. A field rather than a note the rung - // appends once, because the ladder rebuilds this header up to - // three more times and a disclosure that survives only the - // as-built shape is a disclosure the tight budgets never see. + bool tailLegend = true; // deep-tail: the r=/ legend clause. forbudget-soft: never dropped any more + // (the ceiling ladder's rung zero went with the hard ceiling); kept as the + // one switch the header writers read + bool idRouteLegend = true; // row 6 (2026-09-12): the sc=/route= readings (graphlegend.h kForIdRouteLegend); + // never dropped either, for the same reason std::string_view rootArg; // R-E (2026-08-17): the single-root run's own root= — the ladder's // route-dropped rebuild below calls ctxRootOpen a second time and // must carry the SAME root as the pre-built rootOpenStr did. @@ -561,35 +555,11 @@ struct ForLensHeaderParts bool viaByOwnerOnly = false; // owner-hop: viaPresent holds ONLY because the owner rows joined the via question // (the six ranked hop ids alone answer no) — the via clause is then the owner // rows' disclosure, exempt from the sig trim exactly as the qword clause is - - // ── THE DROPPABLE LEGEND, as ONE bit ────────────────────────────────────────────────────────────── - // confidenceNote / tailLegend / idRouteLegend moved in lock step at every read and every write, and the - // guard that used to spell all three was true in the only state rung zero can be reached in. One question, - // asked once; one clear, spelled once. legendDropped is the disclosure and is set by the same call, so a - // future clear cannot forget it (which is exactly how a silent drop ships). - bool legendDroppable() const noexcept - { - return !confidenceNote.empty() || tailLegend || idRouteLegend; - } - void dropDroppableLegend() noexcept - { - confidenceNote = {}; - tailLegend = false; // deep-tail: the explainer falls with the confidence clause — the r= attrs - // and the element (the facts) survive - idRouteLegend = false; // row 6: the sc=/route= readings fall with them — the attributes stay - legendDropped = true; // …and this is the sentence that says so, naming exactly what fell - } - // train 27 (compactlegendcheck P1-B): the compact (compose) / (lego) readings are decided before the - // payload, on an over-approximation; a section served as its counted stub, or not at all, prints none of those rows. - // Clears the reading of each section the caller says is rowless; true when a clause the header carried went (the header - // must be rebuilt). No note: no printed attribute loses its reading. - bool dropRowlessSectionReadings( bool composeRowless, bool legoRowless ) noexcept - { - const bool changed = ( composePresent && composeRowless ) || ( legoPresent && legoRowless ); - composePresent = composePresent && !composeRowless; - legoPresent = legoPresent && !legoRowless; - return changed; - } + bool bodySlotPresent = false; // for-body-slot: an explanatory question on the compact route named an owner + // (forowner.h forBodySlotOwner), so a element rides — served, or skipped + // under a target with its next= — and the slot clause defines it (present-only) + bool splitPresent = false; // attrib-multidef Q1: may a row carry prov="split"? viaPresent's twin + // question over the same ids (serialize.h splitArmsOutAny); set after construction }; // Splice a pre-formatted fragment in front of a structural boundary, or leave the document exactly as it @@ -649,106 +619,6 @@ inline std::string rootOpenWithSchema( std::string rootOpen, std::string_view sc using rw::ForConfidence; using rw::deriveForConfidence; -// L1 — RUNG ZERO'S OWN DISCLOSURE. The ceiling ladder's rung zero (runForLens, below) buys header bytes back by -// dropping the two clauses whose loss costs no UNIQUE information: the confidence=/margin_pct= + -// budget_tokens=/max_tokens= sentence and the r=/tail sentence. Every fact they describe stays on the document; -// only the explanation goes. THE TRADE IS RIGHT and stays — METHODOLOGY §9 takes the smaller document inside the -// budget over the larger one past it, and on src/ at --token-budget=1300 that is 2 890 B against 3 654 B, the -// two clauses being ~750 B of it. What was wrong is that it was SILENT: rungs (b) and (c) each splice a "[… dropped (ceiling)]" -// sentence of their own, and rung zero spliced nothing, so `--for --token-budget=1300` shipped confidence= -// margin_pct= budget_tokens= r= and the counts with no legend defining any of them and no word that a -// definition had been removed — the reader cannot tell a budget cut from a missing feature. This is that word. -// It names the attributes VERBATIM, the way the cap clause and dropped_positive= do, so a reader who meets one -// on the root meets its name here and is told where its definition went. No "--" anywhere: it rides inside an -// XML comment, where a double hyphen is ill-formed (G4). Gate: test/legendcoveragecheck.sh, the for-budgeted row. -// -// ITS LENGTH IS THE SIBLING RUNGS', not a paragraph. Rungs (b) and (c) say "[task_echo: dropped (ceiling) - the -// verbatim copy is the task= attribute above]" in 31-80 B: the thing dropped, the cause, and where the survivor -// is. This says the same four things in 181 B, and the extra bytes are ENTIRELY the attribute names, which are -// the disclosure — a shorter sentence that said "some clauses were dropped" would leave the reader exactly where -// the silence did. The bytes are not free and must not be: this note rides ONLY on a run whose header did not -// fit, so every one of them competes with the sig row the rung just bought back. Gates that pin a budget against -// this floor (fornotesbudgetcheck's tight rung, estcalib's for-budgeted pin) are re-anchored with arithmetic when -// it moves — the note is never re-worded to fit a pin. -// -// WHERE IT SENDS THE READER, and why not to the help text. The first spelling of this note ended "the ripwire -// help text defines them", which was FALSE of half of what it names, measured on the binary that shipped it: -// `budget_tokens` appears ZERO times in --help (19 376 B), in --help=--for (5 027 B) and in --help=all -// (202 636 B); `max_tokens=` appears only in --help=all, never in the --for entry a reader of THIS document -// would open. Only confidence= and margin_pct= are genuinely defined there. A disclosure whose whole job is to -// stop a reader mistaking a budget cut for a missing feature must not then send them somewhere the definition -// is missing — they would conclude the feature is. So it points at the one place that provably has all of them: -// the SAME query at a wider ceiling. Every clause rung zero drops is unconditional at a budget where no rung -// fires, so re-running wider both recovers the definitions AND demonstrates that the absence was a cut. Gate: -// test/legendcoveragecheck.sh arm (E) re-asks every name this note spells against that wider run. -// -// THE CORRECTION IS ONE BYTE SHORTER THAN THE SENTENCE IT REPLACES, and that is a measurement, not a coincidence -// worth ignoring. A first draft of it ran to 191 B, and forrootlegendcheck's arm 2 went red: its --token-budget=800 -// fixture sat at est_tokens=799 with ONE token of headroom, the nine bytes took it to 802, and crossing the budget -// bought the 70 B of over_ceiling="1" plus its legend clause — so nine bytes of prose landed as 31 tokens and put -// a bundle that had fitted 3.9% past the ceiling it names. METHODOLOGY §9 is the tie-breaker: the smaller document -// inside the budget beats the larger one past it. Two honest spellings were available and the shorter was taken; -// that is not the same as trimming a disclosure until a pin goes green, and the day it is, the pin moves instead. -// RUNG ZERO'S OWN DISCLOSURE, ASSEMBLED FROM WHAT ACTUALLY FELL (PR #215 review item 2). -// -// WHAT WAS WRONG. This was four constants — full/thin x default/compact — chosen by one `coverage=` lookup, and -// three of the four were wrong about the document they described: -// * the THIN spellings named neither sc= nor route=, although rung zero clears idRouteLegend on a thin answer -// exactly as it does on any other. MEASURED on the pre-fix binary: `test/cppqualfix --for='mutex and pick -// overloads' --token-budget=300` served two rows, mentioned sc= nowhere in its legend, and its note -// named neither — an attribute with no reading and no disclosure that it had lost one. -// * the non-thin default spelling named route= unconditionally, so a --no-route run was told a definition had -// been "dropped (ceiling)" for an attribute that never rode. That is the error this note exists to prevent, -// pointed the other way: a feature that is absent, reported as a cut. -// * the compact spellings named sc=, which that dialect defines in its always-on rows clause and rung zero -// does not take down. -// A note about a cut is worth nothing if it is not about THIS document, so it is built from this document: the -// four booleans are the four facts that decide what the clauses defined. -// -// ASSEMBLED AT RUNTIME, deliberately. The older comment here argued for constants because "the byte ledgers that -// exempt and charge these strings read their sizes"; no ledger does — this note is spliced into the header and -// priced by finishForLensHeader's own fixpoint, like every other late splice. Nothing reads its size at compile -// time, and the four constants bought only the drift above. -// -// No "--" anywhere: it rides inside an XML comment, where a double hyphen is ill-formed (G4). -// -// compact the compact dialect, whose confidence clause defines confidence=/margin_pct= and NOTHING else -// (budget_tokens= has never been defined there at any budget, so naming it as dropped would be -// the same lie in miniature), and whose sc= reading rides the un-droppable rows clause. So do its -// total= shown= capped= readings ("total= shown= capped=1 if cut"), so the compact note does not -// name them as dropped either (train 27: it did, the same lie, and 24 B a spent ceiling paid for). -// thin the root carries coverage= (L-W present-only), whose clause rode the confidence sentence. -// routeWasOn route= rode this answer, so its reading was there to lose. -// scWasOn a served row carries sc=, so the sc= reading was there to lose (default dialect only). -inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, bool scWasOn ) -{ - std::string note = " [legend clauses: confidence=/margin_pct="; - if( thin ) - { - note += "/coverage="; - } - if( !compact ) - { - note += ", budget_tokens=/max_tokens="; - } - std::string identity; // the sc= / route= readings, named only where they actually rode - if( !compact && scWasOn ) - { - identity = "sc="; - } - if( routeWasOn ) - { - identity += identity.empty() ? "route=" : "/route="; - } - if( !identity.empty() ) - { - note += ", " + identity; - } - note += compact ? std::string_view( " and r=/tail dropped (ceiling) - the attributes stay; a wider token-budget defines them]" ) - : std::string_view( " and r=/tail (total= shown= capped=) dropped (ceiling) - the attributes stay; a wider token-budget defines them]" ); - return note; -} - // A1′ (owner decision 2026-09-12): --for's COMPACT legend, pinned like every other verb's (test/compactlegendcheck.sh, the // ripwire.for/v1 row) and PRESENT-ONLY — every clause below is emitted only when the document carries the // attribute it defines, and every reading is the shortest honest one. It used to spend 1,177–1,216 B here @@ -762,8 +632,7 @@ inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, // clause; the lens columns one clause (cx/ccx/in/churn/amp/clone/tested are always computed on this // surface, clone=/tested= appear only where 1); sc= and next= ride the clause; route= only when the // root carries it (the ladder's rung (c) drops the attribute, and then its reading goes with it); the -// confidence, sc=/route= and tail clauses are the ceiling-droppable trio (rung zero, kForLegendDroppedNoteCompact -// names them); the compact bundle's hops/calls/noedge clause and the auto bundle's bodies clause are +// confidence, sc=/route= and tail clauses always ride (forbudget-soft: no ceiling ladder sheds them); the compact bundle's hops/calls/noedge clause and the auto bundle's bodies clause are // mutually exclusive by construction (planForEnrichment). inline constexpr std::string_view kForCompactLegendRoot = "bundle=/bodies=/reason= the body posture; "; @@ -841,8 +710,26 @@ inline std::size_t ownerLegendExemptBytes( const ForLensHeaderParts& p, bool com } inline constexpr std::string_view kForCompactLegendBodies = "; b t= n= p= l= full bodies, c n= l= callee signatures"; +// for-body-slot: the compact dialect's reading of the ONE owner body (the full dialect's is forowner.h kForBodySlotLegend + the +// root clause below), present-only (bodySlotPresent), right after the owner clause. No "--": it rides inside an XML comment. +// FIX1 R3: it states what the full one states — what each pick= means and what reason=owner does NOT mean (the subject, the +// whole answer): the default posture is this dialect, so this is the only reading most readers get. +inline constexpr std::string_view kForCompactLegendBodySlot = + "; b t= n= p= l= full bodies, c n= l= callee signatures; bodies=1 reason=owner: ONE body, the question's owner by the rule " + "(pick=name: its name is a question word; pick=member: a method so named of a ranked class), chosen by name not resolved; " + "not necessarily the subject, not a claim it is the whole answer; shown=0 capped=1 next= when a budget skipped it"; +static_assert( kForCompactLegendBodySlot.starts_with( kForCompactLegendBodies ), "the slot's compact clause defines as the bodies clause does" ); +inline constexpr std::string_view kForBodySlotRootClause = "; when it rides this root reads bodies=1 reason=owner"; +inline std::string bodySlotLegend( const ForLensHeaderParts& p, bool compact ) +{ + if( !p.bodySlotPresent ) + { + return {}; + } + return compact ? std::string( kForCompactLegendBodySlot ) : std::string( rw::kForBodySlotLegend ) + std::string( kForBodySlotRootClause ); +} inline constexpr std::string_view kForCompactLegendTail = - "; t p= file outside sigs (weaker), r= rank (gap = trimmed)"; + "; t p= file outside sigs (weaker), t sym= a file-page row: its top symbols, r= rank (gap = trimmed)"; // R2-AF (round 2, S4): the COMPACT dialect's `` clause — verbatim, round-2 amendment §R7. // 96 B, present-only like every clause in this dialect (appendCompactForLegend below). inline constexpr std::string_view kForCompactLegendHdr = @@ -949,6 +836,7 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, h += kForCompactLegendBodies; } h += ownerHopLegend( p, /*compact=*/true ); // owner-hop: present-only, right after the hops clause + h += bodySlotLegend( p, /*compact=*/true ); // for-body-slot: present-only, after the owner clause // The unconditionally-droppable-free tail of this dialect: five clauses that ride exactly when the bit beside // them is set, in THIS order, which is the contract a reader and every byte pin depend on. A table rather than // five identical ifs — the list has grown once per lane (hdr, compose, then train 9's lego and layer) and the @@ -960,6 +848,7 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, { p.modScopePresent, rw::kForCompactModScopeClause }, // #60 { p.endLinePresent, rw::kForCompactEndLineLegend }, { p.viaPresent, rw::kForCompactViaNameClause }, // FE-B + { p.splitPresent, rw::kForCompactSplitClause }, // attrib-multidef Q1 }; for( const auto& [ on, clause ] : kPresentOnly ) { @@ -968,14 +857,6 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, h += clause; } } - if( p.legendDropped ) - { - // L1: the droppable clauses went to the ceiling — the attributes stay, and the note names exactly the - // ones THIS document had readings for. scWasOn is false here on purpose: this dialect defines sc= in its - // rows clause, which rung zero does not take down. - h += legendDroppedNote( /*compact=*/true, forCoverageAttrPresent( p ), - forRouteAttrPresent( p, withRouteAttr ), /*scWasOn=*/false ); - } // the data notes, numbers kept — see compactForNote h += compactForNote( p.adaptiveNote ); h += compactForNote( p.mentionNote ); @@ -1098,6 +979,7 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout h.append( kForAutoBundleLegend ); // T3: present whenever auto mode is on, whatever the fit outcome — it explains bodies="0" too } h.append( ownerHopLegend( p, /*compact=*/false ) ); // owner-hop: present-only, right after the bundle clause + h.append( bodySlotLegend( p, /*compact=*/false ) ); // for-body-slot: present-only, after the owner clause if( p.tailLegend ) { h.append( rw::kForFileTailLegend ); // deep-tail: defines r= and the element (sigs-charge-exempt, serialize.h) @@ -1118,13 +1000,9 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout { h.append( rw::forViaNameClause() ); // FE-B: present-only, on the same bit the compact strip reads } - if( p.legendDropped ) + if( p.splitPresent ) { - // L1: the droppable clauses went to the ceiling — say so, and name exactly the ones THIS document had - // readings for (PR #215 review item 2: a thin answer used to be told about neither sc= nor route=, and a - // no-route answer used to be told route= had been dropped when it never rode). - h.append( legendDroppedNote( /*compact=*/false, forCoverageAttrPresent( p ), - forRouteAttrPresent( p, withRouteAttr ), p.scPresent ) ); + h.append( rw::kForSplitClause ); // attrib-multidef Q1: likewise } h.append( extraNotes ); h += " -->"; @@ -1272,27 +1150,6 @@ inline std::string forLensJsonBudgetStanza( std::size_t tokenBudget ) return ",\"budget_tokens\":" + std::to_string( tokenBudget ); } -// R1 — the OVER-CEILING PREDICATE for the JSON dialect: ONE unit, and the same one the XML twin uses. -// This side used to compare BYTES against ceilingAllowanceBytes (N x kMinBytesPerToken(2.36) x -// kCeilingFirstEntryTolerance(1.15) = 2.714 N) while the number it prints, est_tokens, prices those same -// bytes at kBytesPerTokenDefault (2.50). So a document in the band 2.50 N < bytes <= 2.714 N printed -// est_tokens > budget_tokens and said nothing — an 8.6%-wide silent band that any bundle saturating the -// ladder passes straight through (MEASURED on 4b722433: six rungs of fornotesbudgetcheck's own fixture). -// The XML twin (verbs_for.h F2) labels on estTokens > cfg.tokenBudget, and the legend both dialects answer -// to says "over_ceiling=1 says est_tokens exceeds budget_tokens": one sentence, so one predicate. -// The byte clause is KEPT rather than replaced — it is the header-floor case emitForLensJson's W3FIX H2 -// note describes, where a user-length task echo cannot be trimmed under the ceiling at all. Bytes past the -// allowance always price past the budget (2.714 / 2.50 = 1.086), so keeping it costs nothing, and dropping -// it would quietly narrow what this key means. -// A FREE FUNCTION rather than a lambda so the emitter's fixpoint reservation and its emitted key are -// literally the same expression — the pre-2026-09-05 shape wrote those as two separate comparisons, which -// is exactly how both halves of §C2's own bug got in. -inline bool forLensJsonOverCeiling( std::size_t tokenBudget, std::size_t ceilingAllowance, - std::size_t bundleBytes, std::size_t estTokens ) -{ - return tokenBudget > 0 && ( bundleBytes > ceilingAllowance || estTokens > tokenBudget ); -} - // #61 (redhat-et/ripwire#61, YogevKr, 2026-09-08) — THE OVER-CEILING PREDICATE for the XML dialect, and the // SECOND ceiling a `--for` root can name. Written as a free function for the reason its JSON sibling above // is: runForLens is one of the largest bodies in this file, and the rule this expresses is a contract of its @@ -1317,16 +1174,12 @@ inline bool forLensJsonOverCeiling( std::size_t tokenBudget, std::size_t ceiling // // CONVERGENCE is unchanged and for the unchanged reason: the attribute and its legend clause only ADD bytes, // so a document over EITHER ceiling stays over it and the caller's est_tokens fixpoint never oscillates. -// ladderFired is the W3FIX ceiling ladder's last rung (a --token-budget state); it is kept as its own input -// rather than folded into the budget comparison because the rung fires on BYTES against the allowance, which -// is a strictly wider condition than the token comparison beside it — dropping it would narrow what the -// attribute means. It arrives as the VALUE the ladder returned (serialize.h CeilingRung), never as a search -// of the emitted text — see ForLensRootFinish::lastRungFired. Gate: test/formaxtokenscheck.sh. -inline bool forLensOverCeiling( bool ladderFired, std::size_t tokenBudget, int maxTokens, bool hasBodyCeiling, - std::size_t estTokens ) noexcept +// forbudget-soft: the W3FIX ceiling ladder's last rung (ladderFired) went with the hard --token-budget ceiling, and the +// --for callers pass tokenBudget 0 (budget_tokens= names a soft target) — max_tokens= is the ceiling this predicate labels. +// Gate: test/formaxtokenscheck.sh. +inline bool forLensOverCeiling( std::size_t tokenBudget, int maxTokens, bool hasBodyCeiling, std::size_t estTokens ) noexcept { - return ladderFired - || ( tokenBudget > 0 && estTokens > tokenBudget ) + return ( tokenBudget > 0 && estTokens > tokenBudget ) || ( hasBodyCeiling && maxTokens > 0 && estTokens > std::size_t( maxTokens ) ); } @@ -1369,24 +1222,15 @@ struct ForLensRootFinish std::string_view notesDegradedAttr, notesDegradedNote; bool measured = false; std::string_view estTokensLegend, overCeilingLegend; - // M3: the ceiling ladder's VERDICT, carried as a value. False ⇒ the ladder did not run, or stopped above its - // last rung. It used to be the rung's note text, which finishForLensHeader searched the finished header for, - // and the header contains the caller's task echoed verbatim — so a task quoting the note forged the verdict - // and put over_ceiling="1" on a 9.8 KB bundle under a 100 000-token budget. climbCeilingLadderBy returns the - // rung it took (serialize.h CeilingRung); the fit predicate below sets this per candidate, the call site sets - // it from the chosen rung, and no code path derives it from text. Gate: test/ceilingverdictcheck.sh. - bool lastRungFired = false; std::size_t nonHeaderMarkupBytes = 0; // every markup byte outside the header, "" included std::size_t bodyTokens = 0; // the body-rate sections, already priced std::size_t tokenBudget = 0; int maxTokens = 0; bool bodyCeiling = false; - // 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 kForSigsUnpaidOverCeilingNote, which is true where kOverCeilingLegend's est_tokens reading may not be. - bool sigsUnpaidOver = false; + // forbudget-soft: the default answer's ranked head passed the runaway guard (SigsCutReport::rowDropCut): over_ceiling="1". + // Its reading (forRunawayGuardNote) rides the clauses (sigsCeilingNote) already — never kOverCeilingLegend, whose + // est_tokens-versus-a-named-ceiling reading is not what happened. + bool guardOver = false; }; // PR #215 review: the finished header AND the number it prints, handed back together. finishForLensHeader @@ -1470,13 +1314,12 @@ 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. - const bool namedCeilingOver = forLensOverCeiling( f.lastRungFired, f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ); - if( namedCeilingOver || f.sigsUnpaidOver ) + const bool namedCeilingOver = forLensOverCeiling( f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ); + if( namedCeilingOver || f.guardOver ) { - // each clause rides only where its reading is true: the est_tokens/last-rung one when that predicate holds, the - // unpaid one when the recovery handle rode unpaid on an answer past its ceiling (knob-honesty-068) — both when both hold + // each clause rides only where its reading is true: the est_tokens one when that predicate holds; the runaway guard's + // reading rode the clauses already (sigsCeilingNote) — both when both hold spliceBefore( header, " -->", /*fromEnd=*/true, namedCeilingOver ? f.overCeilingLegend : 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() ); } @@ -1492,62 +1335,103 @@ inline std::string finishForLensHeader( std::string header, const ForLensRootFin return finishForLensHeaderPriced( std::move( header ), f ).header; } -// train 27 (compactlegendcheck P1-B): how far a paid render's document runs past the exact budget, in the bytes its rows must -// give up to buy it back. Priced on the header AS BUILT (the ladder's input: no rung note, no over_ceiling label — the shape a -// fitting answer would carry) with every late splice, markup at the rate est_tokens prices it, plus one token for est_tokens' -// rounding, plus what the rows already gave up past the handle (`paidSurplus`: the next render's payment starts from the same -// cut). 0 when that unlabelled document is inside the budget. -inline std::size_t forUnlabelledOverrunBytes( const std::string& asBuiltHeader, const ForLensRootFinish& f, std::size_t tokenBudget, - std::size_t paidSurplus ) +// DEEP-TAIL d2, JSON dialect — the tail stanza and its explicit-regime fill, as a free function over +// emitForLensJson's locals (the ForLensHeaderParts precedent: that emitter is already carrying the whole envelope +// fixpoint). Default regime: the full row cap (page 0's 40 rows, FOLD C). Explicit regime (forbudget-soft round C, +// reserve first): the tail keeps at least its share — `shareBytes`, what the rows at the target cut left of the bundle +// budget — so rows are added until the stanza reaches it (rw::fileTailShownToFill); a cut tail carries "next". +inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t tokenBudget, std::size_t shareBytes ) { - ForLensRootFinish unlabelled = f; - unlabelled.lastRungFired = false; - unlabelled.sigsUnpaidOver = false; - unlabelled.tokenBudget = 0; // no named budget ⇒ no over_ceiling label in the price - unlabelled.bodyCeiling = false; - const std::size_t est = finishForLensHeaderPriced( asBuiltHeader, unlabelled ).estTokens; - return est > tokenBudget ? std::size_t( double( est - tokenBudget + 1 ) * rw::kBytesPerTokenDefault + 0.999 ) + paidSurplus : 0u; + if( tokenBudget == 0 ) + { + return "," + rw::renderFileTailJson( tail, tail.paths.size() ); // every row computeFileTail collected + } + const auto render = [ & ]( std::size_t shown ) { return "," + rw::renderFileTailJson( tail, shown ); }; + return render( rw::fileTailShownToFill( tail, shareBytes, render ) ); } -// train 27: the compact dialect never echoes the task in its comment (appendCompactForLegend), so its rung (b) drops nothing -// and claims nothing (the --pack-task FX4 rule) and its rung (c) names only the route attribute. The full dialect's notes -// are `base`'s. `lastRung` is the last rung's reason, chosen by the caller on what is true. -inline constexpr std::string_view kCompactRouteDroppedNote = " [route_attr: dropped (ceiling)]"; -inline rw::CeilingLadderNotes forLadderNotes( bool compact, const rw::CeilingLadderNotes& base, std::string_view lastRung ) noexcept +// One RENDER of the --json bundle (emitForLensJson below). +// ── round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the JSON twin's row refill, a free function over +// emitForLensJsonPass' locals (the forLensJsonTailStanza precedent). The tail is funded from the rows at the soft target's cut; +// the rows past the cut then fill what it left of T — never below the cut (rw::forSoftRowDropFill). Re-rendered only when that +// narrows the rows; the first render's redaction count and note tallies are replaced, and a failed re-render keeps the first +// render (and every reading taken from it) whole. +struct ForJsonRefillState // the first render's outputs, by reference: the refill replaces them together or not at all { - return { compact ? std::string_view() : base.echoDropped, compact ? kCompactRouteDroppedNote : base.echoAndRouteDropped, lastRung }; + std::string& json; + rw::SigsCutReport& cut; + bool& capped; + std::size_t& droppedPositive; + rw::JsonSigNoteCounts& notes; + std::vector& shown; +}; +// FIX1 R2 (review rv-forbudget-soft): the JSON tail's SHARE under a soft target — what the rows at the cut leave of the bundle +// budget, and never less than what the retired hard ceiling left it (its allowance, N x kMinBytesPerToken x the first-entry +// tolerance, less the envelope it charged and the rows at the cut): owner decision 2 (a trailing section keeps at least the share +// it had at the same N). One function for the refill's reserve and the final fill, so the two read the same number. +inline std::size_t forLensJsonTailShare( std::size_t tokenBudget, std::size_t bundleBytes, std::size_t fixedBytes, std::size_t cutRowsBytes ) noexcept +{ + const std::size_t softShare = rw::forSoftSectionsShare( bundleBytes, cutRowsBytes ); // T less the rows at the cut + const std::size_t retiredShare = rw::forSoftSectionsShare( rw::ceilingAllowanceBytes( tokenBudget ), fixedBytes + cutRowsBytes ); // the hard ceiling's + return softShare > retiredShare ? softShare : retiredShare; } -// DEEP-TAIL d2, JSON dialect — the tail stanza and its explicit-regime fit, as a free function over -// emitForLensJson's locals (the forSigSideCeiling/ForLensHeaderParts precedent: that emitter is already -// carrying the whole envelope fixpoint). Default regime: the full row cap. Explicit regime: the hard -// ceiling stays hard — rows trim one by one into whatever the rendered bundle left, down to the honest -// empty shell (the always-present key is the disclosure; an absent key would read as "no tail exists"). -inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t tokenBudget, - std::size_t ceilingAllowance, std::size_t baseNoTailBytes ) +struct ForJsonRefillBudgets { - std::string stanza = "," + rw::renderFileTailJson( tail, rw::kForFileTailShownCap ); - if( tokenBudget == 0 ) + std::size_t fixedBytes; // the envelope the sigs budget charged (header, reserves, counts, notes) + std::size_t bundleBytes; // budgetBytesForTokens( N ) + std::size_t sigsTarget; // the ladder's target (the cut) + std::size_t firstRowDrop; // the first render's row budget (T plus the frame) +}; +template< class PackSigs > +inline void forLensJsonSoftRefill( const PackSigs& packSigs, const ForLensJsonInputs& in, ForJsonRefillState st, ForJsonRefillBudgets b, + const rw::SigsCutContinuation& cutNext, const std::optional& redactBeforeSigs ) +{ + const std::size_t cutRows = st.json.size() - std::min( st.json.size(), st.cut.pastTargetBytes ); + // the tail at its RESERVE: its share, capped at the head-scoped tail's whole stanza (the same at every N; the XML twin's + // rule) — not its filled bytes nor the cut-scoped tail's size, both of which step with N: rows stay monotone in N + const std::size_t tailBytes = std::min( forLensJsonTailShare( in.tokenBudget, b.bundleBytes, b.fixedBytes, cutRows ), + forLensJsonTailStanza( *in.fileTail, 0, 0 ).size() ); + const std::size_t rowDrop = rw::forSoftRowDropFill( b.bundleBytes, tailBytes ); + if( rowDrop >= b.firstRowDrop ) { - return stanza; + return; } - const std::size_t tailAllowed = ceilingAllowance > baseNoTailBytes ? ceilingAllowance - baseNoTailBytes : 0; - for( std::size_t shown = tail.paths.size(); ; --shown ) + const std::string firstJson = st.json; + const rw::SigsCutReport firstCut = st.cut; + const bool firstCap = st.capped; + const std::size_t firstDrop = st.droppedPositive; + const rw::JsonSigNoteCounts firstNote = st.notes; + const std::vector firstIds = st.shown; + const std::optional redactFirst = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; + if( redactBeforeSigs ) { - stanza = "," + rw::renderFileTailJson( tail, shown ); - if( stanza.size() <= tailAllowed || shown == 0 ) - { - break; - } + *in.redact = *redactBeforeSigs; + } + st.notes = rw::JsonSigNoteCounts {}; + st.cut = rw::SigsCutReport {}; + st.cut.continuationRequest = &cutNext; + st.cut.rowDropBudgetBytes = rowDrop; + rw::MemoryStream refillStream; + std::FILE* const rm = rw::openChargeStream( refillStream ); + if( rm != nullptr ) + { + packSigs( rm, b.sigsTarget, &st.capped, &st.droppedPositive, &st.shown, &st.cut ); + } + const rw::MemoryStreamBytes refill = rm != nullptr ? refillStream.finish() : rw::MemoryStreamBytes {}; + if( rm != nullptr && refill.isWhole ) + { + st.json.assign( refill.bytes ); + return; + } + st.json = firstJson; st.cut = firstCut; st.capped = firstCap; st.droppedPositive = firstDrop; st.notes = firstNote; st.shown = firstIds; + if( redactFirst ) + { + *in.redact = *redactFirst; } - return stanza; } -// 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* sigsNextTryOther ) +inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) { using namespace rw; @@ -1589,11 +1473,13 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const 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; - // 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, - /*pasteHandle=*/true, /*ledgerGapBytes=*/0u, sigsNextPayFromRows }; + // forbudget-soft: the XML twin's two budgets (forSigsBudgets): the default target + the runaway guard, or a soft target + // over the rows. One dialect here, so the soft tier's header is this one. + const ForSigsBudgets sigsBudgets = forSigsBudgets( in.tokenBudget > 0 ? bundleBudget : 0u, fixedBytes, fixedBytes, /*tierAtDefault=*/false ); // no bodies ride here + const std::size_t sigsBudget = sigsBudgets.target; + // knob-honesty-068: the cut's continuation — exempt in every regime now (the XML twin's rule: no byte promise to pay into) + const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*json=*/true, + /*pasteHandle=*/true }; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, @@ -1630,11 +1516,13 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // row cap here (the degrade path below needs a stanza before any budget is knowable); the normal path // re-fits it after the sigs render (forLensJsonTailStanza above) — residual-funded, sigs untouched: // the reservation feeding sigsBudget above deliberately does NOT include these bytes. - std::string tailStanza = "," + renderFileTailJson( *in.fileTail, kForFileTailShownCap ); + std::string tailStanza = "," + renderFileTailJson( *in.fileTail, in.fileTail->paths.size() ); 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 + sigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: IN — the guard / the soft target's rows + sigsCut.pastTargetDisclosed = in.tokenBudget == 0; 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; @@ -1653,6 +1541,12 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const sigsJson.assign( block.bytes ); } } + // round C (RESERVE SECTIONS FIRST, the XML twin's rule): the rows past the soft target's cut fill what the tail left of T + if( isSigsBuffered && in.tokenBudget > 0 && sigsCut.pastTarget > 0 ) + { + forLensJsonSoftRefill( packSigs, in, ForJsonRefillState{ sigsJson, sigsCut, sigsCapped, sigsDroppedPositive, noteCounts, jsonShownIds }, + ForJsonRefillBudgets{ fixedBytes, bundleBudget, sigsBudget, sigsBudgets.rowDrop }, cutNext, redactBeforeSigs ); + } ForLensBlockCharge sigsCharge; // the sink: an unmeasured block takes the omitting path below if( !isSigsBuffered ) { @@ -1704,6 +1598,10 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const } sigsCutStanza += rw::nextFieldJson( sigsCut.next, "sigs_next" ); } + if( sigsCut.pastTargetDisclosed && sigsCut.pastTarget > 0 ) // forbudget-soft: the XML tag's past_target= + { + sigsCutStanza += ",\"sigs_past_target\":" + std::to_string( sigsCut.pastTarget ); + } if( sigsCut.docsDropped > 0 ) { sigsCutStanza += ",\"docs_dropped\":" + std::to_string( sigsCut.docsDropped ); @@ -1730,7 +1628,8 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // over, and adding its 20 bytes can only keep it over, never bring it back under. const std::size_t cappedClauseBytes = sigsCappedKey ? 14u : 15u; // ,"capped":true / ,"capped":false const std::size_t envelopeTextBytes = cappedClauseBytes + 14u + 8u + 1u + kJsonBundleSigsKey.size(); // + ,"est_tokens": + ,"sigs": + } + ,"bundle":"sigs" - const std::size_t ceilingAllowance = in.tokenBudget > 0 ? ceilingAllowanceBytes( in.tokenBudget ) : 0; + // forbudget-soft: the one over-ceiling this dialect can carry is the runaway guard's (a soft target is never "over") + const bool guardOver = sigsCut.rowDropCut && in.tokenBudget == 0; const std::string budgetStanza = forLensJsonBudgetStanza( in.tokenBudget ); // R1 // The XML root's budget_bytes= twin (runForLens, below its own long note): the DEFAULT byte ceiling, @@ -1743,15 +1642,12 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ? ",\"budget_bytes\":" + std::to_string( kForPayloadBudgetBytes ) : std::string(); - // DEEP-TAIL, explicit-regime fit (forLensJsonTailStanza above): residual-funded, sigs untouched. - // lane 2: same rule as the XML twin — the tail excludes the files of the rows actually emitted, not the whole surface - const rw::FileTail jsonTail = rw::computeFileTail( in.ing, in.rank, jsonShownIds, in.rootArg ); - tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, ceilingAllowance, - header.size() + sigsJson.size() + notesStanza.size() - + surfaceCountsStanza.size() + envelopeTextBytes - + budgetStanza.size() - + sigsCutStanza.size() // cut-fix lane A: the sigs_* keys ride the root too - + sigsCeilingStanza.size() ); // the residual the tail is funded from must see this disclosure's bytes + // DEEP-TAIL (FOLD C: page 0 of the file page), the explicit-regime fill (forLensJsonTailStanza above): reserve first — funded + // from what the rows AT THE CUT left of the bundle budget (the same share the refill above priced the rows against) + // lane 2: the rank rows exclude the files of the rows emitted (after a refill: the rows it kept — the XML twin's rule) + const std::size_t jsonCutRows = sigsJson.size() - std::min( sigsJson.size(), sigsCut.pastTargetBytes ); + const rw::FileTail jsonTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, jsonShownIds, in.rootArg, in.task ); + tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, forLensJsonTailShare( in.tokenBudget, bundleBudget, fixedBytes, jsonCutRows ) ); const std::size_t bundleBytesBase = header.size() + sigsJson.size() + notesStanza.size() + surfaceCountsStanza.size() + tailStanza.size() + envelopeTextBytes + droppedPositiveStanza.size() // A2: 0 bytes on the (overwhelming) no-drop path @@ -1768,8 +1664,7 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // R1: the same predicate on the same pair the post-loop emission reads — when isOver is false, // withKey == baseBytes and baseTokens == the settled estTokens, so the reservation and the key can // never disagree; when it is true the 20 bytes only make the document more over, never less. - const std::size_t baseTokens = std::size_t( double( baseBytes ) / kBytesPerTokenDefault + 0.5 ); - const bool isOver = forLensJsonOverCeiling( in.tokenBudget, ceilingAllowance, baseBytes, baseTokens ); + const bool isOver = guardOver; const std::size_t withKey = baseBytes + ( isOver ? 20u : 0u ); // ,"over_ceiling":true const std::size_t nextTokens = std::size_t( double( withKey ) / kBytesPerTokenDefault + 0.5 ); bundleBytes = withKey; @@ -1788,19 +1683,8 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // §C2: the SAME verdict the charge above solved for — read from the solved bundleBytes so the key that is // written and the bytes that were charged can never disagree (they used to be two separate comparisons, // one of which did not count the key it was deciding to emit). - std::string overCeiling; - if( forLensJsonOverCeiling( in.tokenBudget, ceilingAllowance, bundleBytes, estTokens ) ) - { - overCeiling = ",\"over_ceiling\":true"; - } + const std::string overCeiling = guardOver ? std::string( ",\"over_ceiling\":true" ) : std::string(); ASSUME( overCeiling.size() == 0 || overCeiling.size() == 20 ); // the 20 the charge above reserved for it - // 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() ) - { - *sigsNextTryOther = 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 ); @@ -1817,40 +1701,10 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const return 0; } -// 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. +// The --json bundle: one render (forbudget-soft: no charged "sigs_next" to pay for, so no payment-mode re-renders). 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; - const auto restoreTally = [ & ] - { - if( tallyAtEntry ) - { - *in.redact = *tallyAtEntry; - } - }; - bool unpaidOver = false; - const int unpaidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, &unpaidOver ); - if( !unpaidOver ) - { - return unpaidRc; - } - restoreTally(); - bool paidOver = false; - const int paidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, &paidOver ); - if( !paidOver ) - { - return paidRc; - } - restoreTally(); - return emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, nullptr ); + return emitForLensJsonPass( out, header, in ); } // ── T3: the terminal-by-default auto section (pre-registered: docs/EVALS.md §4) ───────────────── @@ -1888,10 +1742,10 @@ struct ForAutoBodiesResult rw::ChargedSection section; // rendered auto bytes; empty when nothing is emitted std::string attr; // the root disclosure; empty ⇒ no attribute (surface off / degrade) bool surfaceOff = false; // true ⇒ the caller rebuilds the header WITHOUT the bundle=auto legend - bool legendOff = false; // exhausted explicit ceiling: attr KEPT (its bytes are the reserve), legend + section dropped — - // the D10 fit contract and the T3 disclosure contract met at once (fordisclosurecheck #2). - // BOTH enrichment shapes set it: the body walk and the compact walk exhaust the same way, - // and a disclosure that survives only one serving path is not a disclosure. + bool hopsCutMarker = false; // forbudget-soft: a soft target left no row room for the compact : the + // counted marker (shown="0" … next=) is served and its present-only clause rides. + // The bundle legend always stays (a soft target never sheds the header); this + // replaced legendOff, the hard ceiling's attribute-only shape (fordisclosurecheck #2). // THE est_tokens SPLIT, filled by buildForEnrichment: exactly one of these is non-zero, because a // section is either markup (compact : summed with the rest of the markup and rounded ONCE) or // body text (T3 : charged at the body rate, which is why the sum splits by kind at all). @@ -1977,22 +1831,6 @@ void restrictBodiesToRouteAnchor( const rw::IngestResult& ing, std::vector 1,782 B -> 9,483 B, 8 rows -> 40) and crowded the auto body walk -// from 6 served bodies down to 2: a bundle both BIGGER than the natural default and WORSE on the axis -// the r10 judging credited (full bodies of the decisive symbols). The invariant now enforced: a wider -// ceiling never buys less decisive content. In auto-bundle mode with no explicit --pack-top-n, the sig -// side's claim is capped at the DEFAULT sig budget — a no-op in the default regime (bundleBudget IS -// kForPayloadBudgetBytes there, so the default bundle stays byte-identical), and at any explicit ceiling -// >= kForPayloadBudgetBytes + kForAutoBodyBudgetBytes the sig render equals the default regime's byte -// for byte, so the bodies' leftover is provably >= the default's: every body the default serves still -// fits, and everything beyond the frozen share flows to bodies. An explicit --pack-top-n is an explicit -// SIG posture and keeps the legacy whole-ceiling claim — the frozen share would trim the rows the caller -// literally asked for down to the ladder floor. A free function over runForLens' locals (the -// ForLensHeaderParts precedent) — runForLens is already one of the largest functions in this file. // knob-honesty-068: the flags that change WHICH rows the lens ranks (and so total=/the order 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 ) @@ -2007,41 +1845,6 @@ inline std::string forRankShapingFlags( const rw::Config& cfg ) return f; } -std::size_t forSigSideCeiling( bool autoBundleMode, int packTopN, std::size_t bundleBudget ) -{ - if( autoBundleMode && packTopN == 0 ) - { - return std::min( bundleBudget, rw::kForPayloadBudgetBytes ); - } - return bundleBudget; -} - -// ── THE TRAILING-SECTION RESERVE under an explicit ceiling (lego-charge fix round 4, estchargecheck #11 A7) ── -// §F1 charged --with-graph's block and the --detail bodies, but only AFTER had already claimed the whole -// sig-side ceiling: the graph block (a FIXED cost, no budget knob) and the first --detail body (packBodies serves -// it whole or head-cut even at a 1-byte budget, with its section wrapper and the omitted-bodies comment) then rode -// past the stated budget. MEASURED on base with `src --for --token-budget=2000 --detail=20 --with-graph`: 6177 B -// ("parse the command line flags") and 6167 B ("emit the legend") against a 5428 B allowance, both over_ceiling="1" -// with the ladder's last-rung note claiming the HEADER floor left no payload to trim — the trimmable payload was -// , sized as if the two trailing sections cost nothing. The fix is the forSigSideCeiling idea applied to these -// two sections: under an explicit --token-budget the sig side's claim is capped so the graph block and a first-body -// floor fit inside the bundle budget. The default regime (no --token-budget) and a budgeted run with neither flag -// pass reserveBytes=0, so the claim is the unchanged sigSideCeiling (byte-identical by construction). The cap only -// binds when the bundle cannot hold sigSideCeiling AND the reserve, so a wide explicit ceiling in auto-bundle mode -// (sig side already frozen at the default share) is untouched too — forbudgetmonotoncheck's invariant holds. -std::size_t forSigClaimWithTrailingReserve( std::size_t sigSideCeiling, std::size_t bundleBudget, std::size_t reserveBytes ) noexcept -{ - if( reserveBytes == 0 ) - { - return sigSideCeiling; - } - const std::size_t room = bundleBudget > reserveBytes ? bundleBudget - reserveBytes : 0; - const std::size_t claim = std::min( sigSideCeiling, room ); - ENSURES( claim <= sigSideCeiling && claim + reserveBytes <= std::max( bundleBudget, reserveBytes ), - "forSigClaimWithTrailingReserve: the sig claim grew past its ceiling or left the reserve no room" ); - return claim; -} - // The FIRST-BODY FLOOR's byte budget: the first --detail body served whole when it is at most a quarter of the // bundle budget, else head-cut at that quarter (packBodies' truncateOversizedFirst path, which carries lines= and // the next= that serves the rest — a disclosed, recoverable cut). Only the FIRST body is floored: the rest of @@ -2119,7 +1922,7 @@ std::vector computeAutoBodyCandidateIds( const rw::IngestResult& ing ForAutoBodiesResult buildForAutoBodies( 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, - const std::vector& anchorDefs, bool viaLegendInHead ) + const std::vector& anchorDefs, bool viaLegendInHead, bool splitLegendInHead = false ) { ForAutoBodiesResult out; // R-E (2026-08-17 harvest): same single-root condition every other verb's root= uses (sarif.h). @@ -2165,12 +1968,8 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR // for why nothing but the attribute appears. This branch used to turn the WHOLE surface off with // no attribute at all — the silent shape the 2026-08-22 Lane-AA mine caught on 5 of 26 real // --for calls (see the BUDGET paragraph above); the T3 registration makes no exception for it. - if( cfg.tokenBudget > 0 && leftBytes == 0 ) - { - out.attr = " bundle=\"auto\" bodies=\"0\" reason=\"budget\""; - out.legendOff = true; - return out; - } + // forbudget-soft: under a soft target an exhausted allowance no longer drops the legend — the walk below renders the + // honest empty shell () at a 1-byte budget and the root says reason="budget". if( autoBodyIds.empty() ) { // R9 fix (W3-S, 2026-08-19): this used to return with `out.section` untouched (default-constructed, @@ -2182,9 +1981,9 @@ 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, g.outNameOnly, cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, nullptr, /*truncateOversizedFirst=*/false, - /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead ); }, + /*withFileContext=*/false, fabRootArg, &lensRank, rw::CalleeLegendsInHead( viaLegendInHead, splitLegendInHead ) ); }, rw::kBytesPerTokenBody ); if( !out.section.isRendered ) { @@ -2204,9 +2003,9 @@ 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, g.outNameOnly, cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, &autoEmitted, /*truncateOversizedFirst=*/false, - /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead ); }, + /*withFileContext=*/false, fabRootArg, &lensRank, rw::CalleeLegendsInHead( viaLegendInHead, splitLegendInHead ) ); }, rw::kBytesPerTokenBody ); if( !out.section.isRendered ) @@ -2361,9 +2160,8 @@ inline std::vector forCompactHopIds( const std::vector& // once (it is appended to the hop nodes only when it is not already one of the six). RIPWIRE_NO_OWNER_HOP=1 turns the // whole plan off — the A/B handle, the RIPWIRE_NO_DOCS_AFTER_CODE precedent. inline constexpr std::size_t kForOwnerHopRows = 3; -inline constexpr std::string_view kOwnerCommonWords[] = { - "all", "call", "each", "from", "get", "has", "into", "new", "put", "read", "run", "set", "that", "then", "this", - "use", "when", "with", "write" }; +// kOwnerCommonWords, ownerFoldName, ownerQuestionWords and the found set itself (forOwnerCandidates) live in forowner.h: the +// body slot (lane for-body-slot) and the MCP twin read the same owners. struct ForOwnerPlan { @@ -2383,56 +2181,9 @@ struct ForSectionPromises { bool viaLegendInHead = false; const ForOwnerPlan* owners = nullptr; // nullptr ⇒ no owner rows (an empty plan reads the same) + bool splitLegendInHead = false; // attrib-multidef Q1: the header defines prov="split" (splitPresent) }; -// ASCII fold of one name or word, leading _ # $ @ removed (the owner match key) -inline std::string ownerFoldName( std::string_view n ) -{ - std::size_t i = 0; - while( i < n.size() && ( n[i] == '_' || n[i] == '#' || n[i] == '$' || n[i] == '@' ) ) - { - ++i; - } - std::string out( n.substr( i ) ); - for( char& c : out ) - { - c = ( c >= 'A' && c <= 'Z' ) ? char( c - 'A' + 'a' ) : c; - } - return out; -} - -// the question's distinct owner words, sorted (binary-searched below — any question length, no per-word scan) -inline std::vector ownerQuestionWords( std::string_view task ) -{ - std::vector words; - const auto isWordByte = []( unsigned char c ) noexcept - { return ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ) || c == '_' || c >= 0x80; }; - std::size_t i = 0; - while( i < task.size() ) - { - if( !isWordByte( static_cast( task[i] ) ) ) - { - ++i; - continue; - } - const std::size_t start = i; - while( i < task.size() && isWordByte( static_cast( task[i] ) ) ) - { - ++i; - } - std::string w = ownerFoldName( task.substr( start, i - start ) ); - if( w.size() >= 3 && !rw::isRouteStopword( w ) ) - { - words.push_back( std::move( w ) ); - } - } - std::sort( words.begin(), words.end() ); - words.erase( std::unique( words.begin(), words.end() ), words.end() ); - return words; -} - -static_assert( std::ranges::is_sorted( kOwnerCommonWords, rw::sortutil::svLess ), "kOwnerCommonWords is binary-searched: keep it sorted" ); - inline ForOwnerPlan forOwnerHopPlan( const rw::IngestResult& ing, const rw::Graph& g, std::string_view task, const std::vector& lensSurfaceIds, const std::vector& lensRank, const std::vector& hopIds ) @@ -2442,52 +2193,8 @@ inline ForOwnerPlan forOwnerHopPlan( const rw::IngestResult& ing, const rw::Grap { return plan; } - const std::vector words = ownerQuestionWords( task ); - if( words.empty() ) - { - return plan; - } - struct Owner - { - rw::NodeId id; - std::size_t rank; // 1-based position in the ranked surface - std::string word; - }; - std::vector found; - const std::size_t pool = std::min( lensSurfaceIds.size(), std::size_t( rw::kForLensDefaultTopN ) ); - for( std::size_t i = 0; i < pool; ++i ) - { - const rw::NodeId id = lensSurfaceIds[i]; - if( id >= ing.symbols.size() || lensRank[id] <= 0.0f ) - { - continue; - } - const rw::Symbol& s = ing.symbols[id]; - if( ( s.kind != rw::SymKind::Function && s.kind != rw::SymKind::Method ) || !rw::isDefinitionNotDeclaration( s ) || rw::isTestSymbol( ing, id ) ) - { - continue; - } - std::string key = ownerFoldName( s.name ); - if( std::binary_search( words.begin(), words.end(), key ) ) - { - found.push_back( Owner{ id, i + 1, std::move( key ) } ); - } - } - // the head-tier gate, per WORD: a word qualifies when one of its definitions ranks inside the head tier - std::vector headWords; - for( const Owner& o : found ) - { - if( o.rank <= rw::kForDocExcerptRankCount ) - { - headWords.push_back( o.word ); - } - } - std::sort( headWords.begin(), headWords.end() ); - std::erase_if( found, [ & ]( const Owner& o ) { return !std::binary_search( headWords.begin(), headWords.end(), o.word ); } ); - // rank order, common words after the rest (stable: rank order inside each group) - std::stable_partition( found.begin(), found.end(), - []( const Owner& o ) { return !std::ranges::binary_search( kOwnerCommonWords, std::string_view( o.word ), rw::sortutil::svLess ); } ); // svLess: #345 - for( Owner& o : found ) + std::vector found = rw::forOwnerCandidates( ing, rw::ownerQuestionWords( task ), lensSurfaceIds, lensRank ); + for( rw::OwnerCandidate& o : found ) { if( !rw::hopSlotHasProvenEdge( g.outOff, g.outNameOnly, o.id ) ) { @@ -2531,7 +2238,7 @@ inline std::vector forOwnerHopNodes( const ForOwnerPlan& plan, const // packHops' owner description of a plan. Default regime: the owner rows ride ON TOP of the hop budget (completeness first — // every ranked row keeps the budget it had, and the name guard is the only stop). Explicit ceiling: the owners are funded // first from what the ceiling left, minus the widest qword_cut/next= disclosure the walk could write (the envelope is the -// ranked rows' own reserve, kCompactFixedBytes, and is not charged twice). +// ranked rows' own reserve, the attribute and envelope reserves, and is not charged twice). inline rw::HopOwners forHopOwners( const rw::Config& cfg, const rw::IngestResult& ing, const ForOwnerPlan& plan, std::size_t leftBytes, std::string_view rootArg ) { @@ -2591,26 +2298,16 @@ inline constexpr std::string_view kForHopsBudgetCutLegend = "; hops shown=0 capped=1: this budget left out the call-hop rows (one hop of callees for each of total= top rows); next= serves them"; // …and the compact dialect's spelling (train 27, compactlegendcheck P1-B): that dialect defines total= shown= capped=1 and next= // in its un-droppable clauses (kForCompactLegendRows / kForCompactLegendFacts), so its hops clause says only what the element -// is — every reading is the shortest honest one. The reserve below stays the longer spelling's (the widest, like -// kCompactAttrReserve), so the compact sig room is the full dialect's to the byte and rows(compact) ⊇ rows(full) holds. +// is — every reading is the shortest honest one. inline constexpr std::string_view kForHopsBudgetCutLegendCompact = "; hops: this budget left out the call-hop rows (one hop of callees for each of total= top rows)"; static_assert( kForHopsBudgetCutLegendCompact.size() < kForHopsBudgetCutLegend.size(), - "forHopsCutReserve reserves the full spelling: it must be the wider one" ); -// the hops cut's continuation (` next="--for=TASK"`) — one spelling for the marker, the partial-cut splice and the reserve + "the compact hops clause is the shorter honest spelling of the full one" ); +// the hops cut's continuation (` next="--for=TASK"`) — one spelling for the marker and the partial-cut splice inline std::string forHopsCutNextAttr( const rw::Config& cfg ) { return rw::nextAttrXml( rw::nextFlag( "--for=", cfg.forTask ) ); } -// the bytes an explicit ceiling reserves out of the sig claim so the compact hops' cut disclosure always fits: the widest -// marker (``, total <= kPackTaskBodyCandidates, two digits) plus its -// present-only legend clause — the kCompactAttrReserve / kForFileTailShellReserve pattern. -inline std::size_t forHopsCutReserve( const rw::Config& cfg ) -{ - constexpr std::size_t kMarkerShell = std::string_view( "" ).size(); - return kMarkerShell + forHopsCutNextAttr( cfg ).size() + kForHopsBudgetCutLegend.size(); -} - rw::ChargedSection forHopsBudgetCutMarker( const rw::Config& cfg, const rw::IngestResult& ing, const std::vector& hopIds ) { rw::ChargedSection marker; @@ -2687,24 +2384,24 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest // as another. Gate: test/fordisclosurecheck.sh (compact arm). if( cfg.tokenBudget > 0 && leftBytes == 0 ) { - out.attr = " bundle=\"compact\" bodies=\"0\" reason=\"budget\""; - out.legendOff = true; + out.attr = " bundle=\"compact\" bodies=\"0\" reason=\"budget\""; // fix round 4 (rule 5, review rv-wrap-recipe-k29 F1): the hop rows this budget left out are NAMED, not dropped // silently — an empty carrying the count and the call that serves them (the same question without the - // budget, whose compact allowance is its own). The bytes are this section's and the ladder prices them. - out.section = forHopsBudgetCutMarker( cfg, ing, hopIds ); + // budget, which serves every hop row). forbudget-soft: the legend stays; the marker's clause rides (hopsCutMarker). + out.section = forHopsBudgetCutMarker( cfg, ing, hopIds ); + out.hopsCutMarker = !out.section.xml.empty(); return out; } - // the surface allowance MINUS this surface's own fixed disclosure — see kForCompactSurfaceBudgetBytes. - constexpr std::size_t kCompactFixedBytes = kForCompactBundleLegend.size() + kCompactAttrReserve + kCompactWrapReserve; // fix round 4: under an explicit ceiling a cut carries next= (forHopsCutNextAttr), so its bytes come out of the // rows' budget first; the default regime is unchanged (cutNext empty) const std::string cutNext = cfg.tokenBudget > 0 ? forHopsCutNextAttr( cfg ) : std::string(); const std::size_t rowRoom = leftBytes > cutNext.size() ? leftBytes - cutNext.size() : 1u; - const std::size_t hopBudget = std::min( rowRoom, rw::kForCompactSurfaceBudgetBytes > kCompactFixedBytes - ? rw::kForCompactSurfaceBudgetBytes - kCompactFixedBytes - : std::size_t( 1 ) ); + // forbudget-soft (D-3): the DEFAULT answer serves every hop row — the section is bounded by its own structure (at most + // kPackTaskBodyCandidates rows of at most 16 callees each), so the 1,000 B allowance no longer cuts it; the runaway guard's + // byte value is the only bound. An explicit soft target fills the rows from what the target left after (rowRoom), + // with no allowance of its own: a target wide enough for the whole section serves it whole, as the default does. + const std::size_t hopBudget = cfg.tokenBudget == 0 ? rw::kForRunawayGuardBytes : rowRoom; // THE QUESTION'S OWNER (forOwnerHopPlan): owner rows first, the candidates they were not, the edgeless owners last; // an empty plan leaves both exactly as they were (forOwnerHopNodes / forHopOwners). @@ -2712,8 +2409,8 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest const rw::HopOwners owners = forHopOwners( cfg, ing, ownerPlan, leftBytes, fcRootArg ); out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, g.outNameOnly, redactPtr, &lensRank, fcRootArg, - viaLegendInHead, &owners ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) + { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), redactPtr, &lensRank, fcRootArg, + rw::CalleeLegendsInHead( viaLegendInHead, promised.splitLegendInHead ), &owners ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) // 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 @@ -2753,7 +2450,8 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR const std::size_t committed = committedBytes + plan.attrReserve; ForAutoBodiesResult out = plan.compact ? buildForCompactHops( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, promised ) - : buildForAutoBodies( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, anchorDefs, promised.viaLegendInHead ); + : buildForAutoBodies( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, anchorDefs, promised.viaLegendInHead, + promised.splitLegendInHead ); if( plan.compact ) { out.markupBytes = out.section.xml.size(); @@ -2765,34 +2463,25 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR return out; } -// DEEP-TAIL d2 — the XML tail render + its explicit-regime fit, a free function over runForLens' locals -// (that function is already one of the largest in this file — the forSigSideCeiling precedent). Default -// regime: the full row cap, bytes riding on top (est_tokens measures them). Explicit regime: rows fit the -// residual the rendered bundle actually left; the shell's bytes were reserved ahead of the body walk -// (kForFileTailShellReserve inside the enrichment's committed sum), so the disclosure always fits. -// fix round 4 (rule 5, review rv-wrap-recipe-k29 F1): under an explicit ceiling the cut tail carries `cutNextAttr` — -// the --for file page (forTailCutNextAttr) that lists every positive-score file — and the shell reserve the caller -// took (forTailShellReserve) includes it, so the disclosure still always fits. +// DEEP-TAIL d2 — the XML tail render + its explicit-regime fill, a free function over runForLens' locals (that function +// is already one of the largest in this file — the ForLensHeaderParts precedent). Default regime: page 0's full 40 rows +// (FOLD C), bytes riding on top (est_tokens measures them). Explicit regime (forbudget-soft round C, reserve first): the +// tail keeps at least its SHARE — the bundle budget less `spentBytes` (the rows at the target cut and the sections served +// before the tail), never below the empty shell — so rows are added until the element reaches it (its last row may cross +// it: a soft target makes no byte promise). A cut tail carries next= (the page after its last row) in every regime. inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t tokenBudget, - std::size_t bundleBudget, std::size_t spentBytes, std::string_view cutNextAttr = {} ) + std::size_t bundleBudget, std::size_t spentBytes ) { std::vector esc; if( tokenBudget == 0 ) { - return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc ); + return rw::renderFileTailXml( tail, tail.paths.size(), esc ); // every row computeFileTail collected } - const std::size_t tailAllowed = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, - rw::kForFileTailShellReserve + cutNextAttr.size() ); - return rw::renderFileTailXml( tail, rw::fileTailShownForBudget( tail, tailAllowed, esc, cutNextAttr ), esc, cutNextAttr ); + const std::size_t share = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, rw::kForFileTailShellReserve ); + const auto render = [ & ]( std::size_t shown ) { return rw::renderFileTailXml( tail, shown, esc ); }; + return render( rw::fileTailShownToFill( tail, share, render ) ); } -// The explicit-budget tail's cut continuation: the --for file page (one row per positive-score file, pageable), the -// one call that lists the files a budget left out of . Empty without --token-budget: the default regime's tail is -// unchanged (a named deferral — adding it there moves every default --for answer). -inline std::string forTailCutNextAttr( const rw::Config& cfg ) -{ - return cfg.tokenBudget > 0 ? rw::nextAttrXml( rw::forWidenNext( cfg.forTask ) ) : std::string(); -} // R2-AF (round 2, S4): render the task's named-file/decl-impl-partner rows (rw::forNamedHeaderRows, // mention.h — the resolution rule and its rationale live there) as ``, root-relative like @@ -2806,25 +2495,7 @@ inline std::string renderForHdrRowsXml( const rw::IngestResult& ing, const std:: { return {}; } - const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); - const auto rel = [ & ]( std::uint32_t f ) -> std::string - { - return rootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) ); - }; - std::vector esc; - std::string x; - for( const rw::ForNamedHeaderRow& row : rows ) - { - // local invariant: forNamedHeaderRows only ever returns fileIds it read out of ing.files itself - // (mention.h), so an out-of-range id here would mean that resolver's own contract broke, not a - // renderer bug — worth trapping right at the point the bad index would otherwise index OOB below. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - x += ""; - } + std::string x = rw::forHdrRowsXml( ing, rows, rootArg ); // forpage.h: the one render, shared with the MCP twin // runForLens charges x.size() in every budget sum and emits x itself, so "no row" and "no bytes" must agree ENSURES( !x.empty(), "renderForHdrRowsXml: rows resolved but nothing rendered" ); return x; @@ -2865,29 +2536,16 @@ inline bool candidatePageWindowed( const rw::Config& cfg ) return cfg.tokenBudget != 0 && ( cfg.pageLimit > 0 || cfg.pageOffset > 0 ); } -// 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). -// `forAtStamp` (train 26c, CodeRabbit on #383): the at= stamp, read ONCE per run by runForLens — every render shares it. +// The --for lens's RENDER over the ranking runForLens computed once (`lr`). `forAtStamp` (train 26c, CodeRabbit on #383): the +// at= stamp, read ONCE per run by runForLens. forbudget-soft: one render per run — the C3 payment modes (unpaid / paid / +// overrun re-renders of a charged continuation) went with the hard ceiling they priced against. struct ForLensPassMode { - bool sigsNextPayFromRows = false; - bool* sigsNextTryOther = nullptr; std::string_view forAtStamp; - // train 27 (compactlegendcheck P1-B): the compact dialect's OVERRUN payment. A paid compact render that lands past its - // ceiling reports, through `overrunBytesOut`, the bytes its unlabelled document runs past the exact budget (plus what its - // rows already gave up past the handle); the next render pays `overrunReserveBytes` from the rows beside the handle - // (serialize.h SigsCutContinuation::overrunBytes) — see runForLens. - std::size_t overrunReserveBytes = 0; - std::size_t* overrunBytesOut = nullptr; }; std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, const ForLensPassMode& mode ) { using namespace rw; - EXPECTS( mode.overrunReserveBytes == 0 || mode.sigsNextPayFromRows, "runForLensPass: an overrun is paid from the rows only in the paying mode" ); - const bool sigsNextPayFromRows = mode.sigsNextPayFromRows; - bool* const sigsNextTryOther = mode.sigsNextTryOther; const std::string forAtStamp( mode.forAtStamp ); const Config& cfg = d.cfg; const IngestResult& ing = d.ing; @@ -3172,15 +2830,28 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { forConf.attrs += " budget_tokens=\"" + std::to_string( cfg.tokenBudget ) + "\""; } + // round C: the body ceiling's two disclosures (this attribute and its note below), measured so a soft target's cut can + // be priced without them — a body ceiling never moves a row (formaxtokenscheck (E)) + std::size_t forBodyCeilingDisclosureBytes = 0; if( forBodyCeiling ) { + const std::size_t before = forConf.attrs.size(); forConf.attrs += " max_tokens=\"" + std::to_string( cfg.maxTokens ) + "\""; + forBodyCeilingDisclosureBytes += forConf.attrs.size() - before; + } + // forbudget-soft (owner decision 2026-10-14): budget_tokens= is a SOFT target now — rows stop once it is reached and + // nothing else is shaped by it — so its reading says so (kForSoftBudgetNote); max_tokens= stays the hard body ceiling. + // No "--" in these strings: they ride inside an XML comment, where a double hyphen is ill-formed (G4). The flags are + // named without their dashes for exactly that reason. + if( forGateBudget ) + { + forConf.note += rw::kForSoftBudgetNote; } - if( forGateBudget || forBodyCeiling ) + if( forBodyCeiling ) { - // No "--" in this string: it rides inside an XML comment, where a double hyphen is ill-formed - // (G4). The flags are named without their dashes for exactly that reason. - forConf.note += " [budget_tokens=/max_tokens=: the token ceiling this bundle was shaped against]"; + const std::size_t before = forConf.note.size(); + forConf.note += " [max_tokens=: the token ceiling the detail bodies were shaped against]"; + forBodyCeilingDisclosureBytes += forConf.note.size() - before; } // §L10b: mention_anchored=/doc_mentions= — the machine form of mentionNote/docMentionNote, EACH @@ -3250,10 +2921,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co const ForEnrichmentPlan plan = planForEnrichment( autoBundleMode, conceptualRoute, cfg.autoBodies ); // NOT const: the enrichment block below may drop the LEGEND (autoBundle/compactBundle=false) and // rebuild the header without it — the ladder's later rebuilds read this struct through - // buildForHeader and must honor that decision. Two distinct causes reach it: the chargeSection - // degrade (surfaceOff — no attribute either) and an exhausted explicit ceiling (legendOff — the - // root ATTRIBUTE is kept, paid for out of its own reserve). A tight explicit budget no longer - // turns the disclosure off on EITHER serving shape — test/fordisclosurecheck.sh. + // buildForHeader and must honor that decision. One cause reaches it now: the chargeSection degrade + // (surfaceOff — no attribute either); forbudget-soft retired the exhausted-ceiling switch (a soft target + // keeps the legend and, on the compact route, serves the hops cut marker instead — hopsCutMarker below). + // A tight explicit budget never turns the disclosure off — test/fordisclosurecheck.sh. // PR #215 review: does any row this bundle could serve carry sc=? Both dialects' sc= readings are // present-only on this, and the dropped-legend note names sc= only when there was a reading to lose. // Decided from the RANKED SET and before the header is built, because the un-budgeted path ships the @@ -3329,9 +3000,17 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co const ForOwnerPlan ownerPlan = ( autoBundleMode && plan.compact ) ? forOwnerHopPlan( ing, g, cfg.forTask, lensSurfaceIds, lensRank, forCompactHopIds( lensSurfaceIds, lensRank ) ) : ForOwnerPlan{}; + // for-body-slot: THE ONE OWNER BODY of an explanatory question (forowner.h forBodySlotOwner), decided here for the same + // reason as the owner plan above: the header's slot clause reads it. The compact route only (an XML answer whose caller + // chose no body posture: autoBundleMode excludes --detail/--signatures-only, plan.compact excludes --auto-bodies and the + // name-exact route, which already serves the anchor's body). + const rw::ForBodySlotOwner slotOwner = ( autoBundleMode && plan.compact && !cfg.json && rw::forBodySlotAsked( cfg.forTask ) ) + ? rw::forBodySlotOwner( ing, g.outOff, g.outTargets, cfg.forTask, lensSurfaceIds, lensRank ) + : rw::ForBodySlotOwner{}; bool forViaPresent = false; bool forViaByOwnerOnly = false; // owner-hop: the six ranked hop ids alone answer no, the owner rows switch it on - if( !g.outNameOnly.empty() ) + bool forSplitPresent = false; // attrib-multidef Q1: the same question for prov="split", over the same ids + if( !g.outNameOnly.empty() || !g.outProv.empty() ) { std::vector viaIds; if( autoBundleMode && plan.compact ) @@ -3355,16 +3034,18 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { viaIds = forDetailIds( ing, lensRank, cfg.detail, forTopN ); } - forViaPresent = rw::namesOnlyOutAny( g.outOff, g.outNameOnly, viaIds ); + forViaPresent = rw::namesOnlyOutAny( g.outOff, g.outNameOnly, viaIds ); + forSplitPresent = rw::splitArmsOutAny( g.outOff, g.outNameOnly, g.outProv, viaIds ); } ForLensHeaderParts headerParts{ cfg.forTask, rootOpenStr, taskNote, adaptiveNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, forConf.attrs, forConf.note, forAtAttrStr, mentionDocAttrsStr, cfg.anchor, plan.autoBodies, plan.compact, cfg.legend == "compact", - /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg, + /*tailLegend=*/true, /*idRouteLegend=*/true, flRootArg, /*hdrLegend=*/!forHdrRows.empty(), forScPresent, forComposePresent, forLegoPresent, forLayerPresent, forModScopePresent, forEndLinePresent, forViaPresent, - ownerPlan.any(), forViaByOwnerOnly }; + ownerPlan.any(), forViaByOwnerOnly, slotOwner.any() }; + headerParts.splitPresent = forSplitPresent; // attrib-multidef Q1 (a trailing member: the aggregate above keeps its order) 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, {} ); @@ -3401,13 +3082,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // header above, and the body otherwise; "" keeps the --expand hint. const std::string forTopRowNext = forThin ? forWidenNext( cfg.forTask ) : std::string(); - // DEEP-TAIL d2: the file-grain tail candidates — one shared walk (serialize.h computeFileTail) for - // both dialects, computed from the SAME resolved surface selects, so the two dialects (and - // the MCP twin, which calls the same function) cannot select different tails. - const FileTail forFileTail = computeFileTail( ing, lensRank, lensSurfaceIds, flRootArg ); // the DEGRADE paths' tail (surface = head) - // H2H-Graft lane 2 (2026-09-07): the tail served must exclude only the files of the sigs rows actually - // EMITTED. Excluding the whole 40-candidate surface left every row the byte ladder trimmed (rank 5..40) - // in neither section: on rocksdb, three single-file answers at candidate rank 5/10/5 were served nowhere. + // DEEP-TAIL d2, FOLD C (forbudget-soft round C): the file-grain tail is page 0 of the --for file page — one shared + // model (forpage.h computeFileTail) for both dialects and the MCP twin, from the SAME ranking and term evidence the + // `--limit=40` page uses, so the tail, its next= page and the page itself cannot disagree. + const FileTail forFileTail = computeFileTail( ing, lensRank, lr.evidence, lensSurfaceIds, flRootArg, cfg.forTask ); // the DEGRADE paths' tail + // H2H-Graft lane 2 (2026-09-07): the tail's rank rows exclude only the files of the sigs rows actually EMITTED (a row the + // ladder trimmed must land in the tail, never nowhere). std::vector shownSigIds; // L2: --json — the ranking ("sigs") bundle, plus (§B1.4) a COUNT of what // @@ -3485,7 +3165,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co &forClone, testedPtr, ampPtr, redactPtr, cfg.packBudgetBytes, cfg.tokenBudget, notesPtr, legoTotal, composeTotal, routesTotal, flRootArg, - &forFileTail, d.notesDegraded, forDocsAfterCode, cfg.forTask, + &forFileTail, &lr.evidence, d.notesDegraded, forDocsAfterCode, 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. @@ -3601,7 +3281,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // The disclosure is exempt from the SIG-TRIM allowance in BOTH regimes. Charging it under an // explicit ceiling (the first cut of this change) shrank the sig section BELOW the default // regime's — an explicit ceiling wider than the default re-trimming sigs is exactly the - // inversion forSigSideCeiling exists to forbid (gate: forbudgetmonotoncheck, which caught it). + // inversion the frozen sig share existed to forbid (gate: forbudgetmonotoncheck, which caught it). // The est_tokens<=N promise is kept by the BODY side, not by this trim: committedBytes below // counts the emitted header verbatim, so leftBytes hands the bodies ~240 B less and the bundle // total never crosses the ceiling — charging the sig side as well was double-counting one cost. @@ -3640,8 +3320,15 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // on the same contract: exempt, so is byte-identical with and without the owner rows (the rows ride on top of // the hop budget; their reading never costs a signature row). Gate: ownerhopcheck (S). const std::size_t ownerLegendEmitted = ownerLegendExemptBytes( headerParts, compactLegendOn ); + // for-body-slot: the slot clause on the same contract — the body rides on top of the complete answer, so its reading never + // costs a signature row (forbodyslotcheck (D4): byte-identical with RIPWIRE_NO_BODY_SLOT=1) + const std::size_t bodySlotLegendEmitted = bodySlotLegend( headerParts, compactLegendOn ).size(); + // attrib-multidef Q1: the prov="split" clause is a disclosure on the same contract — exempt, as e= is, so (rows and + // its continuation's budget) is byte-identical with and without it (fordisclosurecheck #2: auto vs --signatures-only) + const std::size_t splitLegendEmitted = !headerParts.splitPresent ? 0u + : ( compactLegendOn ? rw::kForCompactSplitClause.size() : rw::kForSplitClause.size() ); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted - + endLineLegendEmitted + ownerLegendEmitted; + + endLineLegendEmitted + ownerLegendEmitted + bodySlotLegendEmitted + splitLegendEmitted; if( exemptBytes > headerStr.size() ) { DISCLOSE( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); @@ -3655,7 +3342,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // 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 + // forbudget-soft: the FULL dialect's charge for this header, in both dialects — a soft target's text tier is the default + // target's sig share as THIS number prices it, so the compact and full answers shrink their rows alike (forSigsBudgets) + std::size_t fullChargedHeaderBytes = chargedHeaderBytes; if( compactLegendOn ) { ForLensHeaderParts fullParts = headerParts; @@ -3666,32 +3355,30 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes() + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ) - + ownerLegendExemptBytes( headerParts, /*compact=*/false ); + + ownerLegendExemptBytes( headerParts, /*compact=*/false ) + + bodySlotLegend( headerParts, /*compact=*/false ).size() + + ( headerParts.splitPresent ? rw::kForSplitClause.size() : 0u ); // attrib-multidef Q1 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 ); - // knob-honesty-068, train 26b: the gap is read AFTER both exemptions (the e= clause and lean's zero note) on - // BOTH headers, so it measures only the sig room the compact dialect was given beyond its honest cost - compactLedgerGapBytes = fullCharged - chargedHeaderBytes; // >= 0 by the min above + chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); + fullChargedHeaderBytes = fullCharged; } // lego-charge: / cost what they are SERVED as — a collapsing section its stub, not its full render // (rw::forSectionsSigChargePlan states both rules and why --sections= keeps the default's charge without a ceiling). // A section the degrade path will stream unmeasured has no bytes here (legoStr/composeStr empty): blockCharge discloses it. - const bool explicitForCeiling = cfg.tokenBudget > 0; // the lens's only ceiling (--max-tokens bounds --detail bodies) + // forbudget-soft: the DEFAULT charge in both regimes — a soft target charges the rows alone, and the default + // target's share it tiers its text by is the default answer's (forSigsBudgets), sections priced as the default prices them. + const bool forSoftTarget = cfg.tokenBudget > 0; // an explicit --token-budget: a SOFT target over the rows const rw::ForStubbableSection legoChargeIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = legoPreRendered, .renderedBytes = legoStr.size() }; const rw::ForStubbableSection composeChargeIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = composePreRendered, .renderedBytes = composeStr.size() }; - const rw::ForSectionStubPlan sigChargePlan = rw::forSectionsSigChargePlan( cfg.sections, explicitForCeiling, cfg.forTask, legoChargeIn, composeChargeIn ); - const std::size_t fixedBytes = chargedHeaderBytes + sigChargePlan.servedBytes( legoChargeIn, composeChargeIn ) + routeStr.size() + 6; // + "" - // the auto bundle's SECTION SPLIT — the sig side's claim is capped so an explicit ceiling wider - // than the default cannot re-inflate the trimmed sig tail at the bodies' expense (the rule, its - // measured defect and the invariant: forSigSideCeiling above; gate: forbudgetmonotoncheck). - const std::size_t sigSideCeiling = forSigSideCeiling( autoBundleMode, cfg.packTopN, bundleBudget ); + const rw::ForSectionStubPlan sigChargePlan = rw::forSectionsSigChargePlan( cfg.sections, /*explicitCeiling=*/false, cfg.forTask, legoChargeIn, composeChargeIn ); + const std::size_t sectionsChargedBytes = sigChargePlan.servedBytes( legoChargeIn, composeChargeIn ) + routeStr.size() + 6; // + "" + const std::size_t fixedBytes = chargedHeaderBytes + sectionsChargedBytes; // §F1's graph block, rendered HERE (before ) rather than beside the bodies: it reads only lensRank, so the - // bytes are the same wherever it renders, and an explicit ceiling must know them before the sig side claims its - // share (fix round 4: forSigClaimWithTrailingReserve). Emission order is unchanged (detail, then graph). + // bytes are the same wherever it renders. Emission order is unchanged (detail, then graph). rw::ChargedSection graphSection, detailSection; if( cfg.withGraph ) { @@ -3703,42 +3390,19 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co DISCLOSE( blockCharge, ForLensBlockCharge::DisclosureWhy::SiblingBlockUnmeasured, "runForLens: the graph block streams uncharged — est_tokens omitted" ); } } - // fix round 4 (estchargecheck #11 A7): under an EXPLICIT ceiling the graph block and a first --detail body floor are - // reserved out of the sig side's claim, so cannot spend the bytes the two trailing sections will ship. The - // floor is MEASURED — the --detail section rendered at the floor budget (wrapper, the first body, the omitted-bodies - // comment) — not estimated. No explicit ceiling, or neither flag: reserve 0, claim unchanged (byte-identical). const std::vector detailIds = cfg.detail > 0 ? forDetailIds( ing, lensRank, cfg.detail, forTopN ) : std::vector{}; // (score desc, id asc) — same order as the sigs - // - // THE PRE-REGISTERED KILL (fix round 4, reports/lego-charge.md): with BOTH --detail and --with-graph the reserve is OFF. - // Measured on the 32 round-1 questions at --token-budget=2000 --detail=3 --with-graph, reserving both sections cut the - // gold-relevant owner Reactive._set (textual-12, r=8) out of ; header + graph + rows through r=8 + any real first - // body exceed the allowance there, so no floor keeps the row AND meets the budget. That shape keeps the ranked rows' - // claim, the first body still gets its floor (head-cut with next=, recoverable), and the overshoot is disclosed by the - // root's over_ceiling="1" — every cut named, nothing reserved away. Each section alone keeps its reserve (no gold row lost). - const bool reserveTrailing = explicitForCeiling && !( cfg.detail > 0 && cfg.withGraph ); - const std::size_t detailFloorBudget = explicitForCeiling ? forDetailFirstBodyFloor( ing, detailIds, bundleBudget ) : 0u; - std::size_t trailingReserve = reserveTrailing ? graphSection.xml.size() : 0u; - if( reserveTrailing && detailFloorBudget > 0 ) - { - // a MEASURE, not a rendering the reader gets: it redacts the same text into a scratch tally, so a secret the - // real render redacts is still counted once - std::optional floorRedact = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; - trailingReserve += rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailFloorBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, floorRedact ? &*floorRedact : nullptr, - /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); }, - rw::kBytesPerTokenBody ).xml.size(); // a degraded measure is 0 bytes: the reserve falls back to the graph alone - } - const std::string forTailNext = forTailCutNextAttr( cfg ); // fix round 4: the explicit-budget tail's cut next= (empty otherwise) - if( explicitForCeiling ) - { - // fix round 4 (rule 5): the two cut disclosures an explicit ceiling owes — the tail's next= and, on the compact - // route, the hops cut (marker or spliced next= + its clause) — are reserved like the trailing sections, so - // disclosing a cut never pushes the answer over the budget it was cut for - trailingReserve += forTailNext.size() + ( plan.compact ? forHopsCutReserve( cfg ) : 0u ); - } - const std::size_t sigClaim = forSigClaimWithTrailingReserve( sigSideCeiling, bundleBudget, trailingReserve ); - const std::size_t sigsBudget = sigClaim > fixedBytes ? sigClaim - fixedBytes : 1; // ≥1: 0 would mean "no budget" + // the first --detail body's floor under a soft target (head-cut with next= when it is over a quarter of the target): the + // target fills the rows first, and the floor keeps the one body the caller asked for from being cut to nothing + const std::size_t detailFloorBudget = forSoftTarget ? forDetailFirstBodyFloor( ing, detailIds, bundleBudget ) : 0u; + // THE TWO BUDGETS (forbudget-soft; serialize.h kForPayloadBudgetBytes / kForRunawayGuardBytes): the target the + // text-shrink steps aim at, and where the row-drop step aims — the runaway guard (default), or the soft target's rows. + // round C (reserve first): a soft target's cut is priced with the full dialect's header and every fixed section — the + // graph block included, which the retired ceiling reserved out of the sig claim — so both dialects cut alike + const std::size_t cutHeaderBytes = fullChargedHeaderBytes - std::min( fullChargedHeaderBytes, forBodyCeilingDisclosureBytes ); + const ForSigsBudgets sigsBudgets = forSigsBudgets( forSoftTarget ? bundleBudget : 0u, fixedBytes, + cutHeaderBytes + sectionsChargedBytes + graphSection.xml.size(), + /*tierAtDefault=*/autoBundleMode && cfg.packTopN == 0 ); + const std::size_t sigsBudget = sigsBudgets.target; // The two attributes SPLICED into the header AFTER the ceiling ladder has chosen a rung — est_tokens // (" est_tokens=\"NNNNNNNN\"", bounded well under 24 B: 8 digits covers ~100M tokens) and, when the @@ -3761,8 +3425,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co const bool compactEstLegend = cfg.legend == "compact"; const std::string_view kForEstTokensLegend = !compactEstLegend ? std::string_view( " est_tokens= prices this bundle in tokens" ) - : forGateBudget && forBodyCeiling ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens=/max_tokens= the token budget/body ceiling asked" ) - : forGateBudget ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens= the token budget asked" ) + : forGateBudget && forBodyCeiling ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens=/max_tokens= the soft row target (rows stop once it is reached)/body ceiling asked" ) + : forGateBudget ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens= the soft row target asked (rows stop once it is reached)" ) : forBodyCeiling ? std::string_view( " est_tokens= prices this bundle in tokens; max_tokens= the body ceiling asked" ) : std::string_view( " est_tokens= prices this bundle in tokens" ); // F2: spliced ONLY onto a document that actually carries over_ceiling="1" (the attribute is now @@ -3775,7 +3439,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // settled far above at the H9 splice), so the binding is const rather than constexpr. Keyed on the // attributes present, not on which ceiling was exceeded — see overCeilingLegendFor. A budget-only run // still selects kOverCeilingLegend, so nothing about --for --token-budget moves by a byte. - const std::string_view kForOverCeilingLegend = rw::overCeilingLegendFor( forGateBudget, forBodyCeiling ); + // forbudget-soft: budget_tokens= is a soft target, never a ceiling this root is "over" — only max_tokens= is + const std::string_view kForOverCeilingLegend = rw::overCeilingLegendFor( /*namesBudgetTokens=*/false, forBodyCeiling ); const std::size_t headerSpliceReserve = kEstTokensAttrReserve + kForEstTokensLegend.size() + ( forWeak ? kWeakAttrBytes : 0u ); // D10: --token-budget SHAPES this bundle (exit-0 trim) rather than gating it (exit-3 like the @@ -3791,31 +3456,22 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // 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 - // 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 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 PAID capped compact answer serves the rows the full one does and lands no further over its - // ceiling (compactlegendcheck P4). (Rows a PAID full answer would serve: where full's UNPAID answer fits and the compact - // one's does not — a compact header larger than full's, round 3 — full keeps a row the compact answer pays; that is the - // one exemption compactlegendcheck (P1-B) rules, orchestrator option B.) 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 - // (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; + // knob-honesty-068: a capped names the call that serves it uncut (serialize.h SigsCutContinuation). forbudget-soft: + // it is EXEMPT in every regime now — the default has no byte promise (a target and a runaway guard), and an explicit + // budget is a soft target with none either — so no row is ever dropped to pay for the handle and the lens renders ONCE + // (the C3 unpaid/paid/overrun re-renders are gone with the hard ceiling they priced against). 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, - sigsNextPayFromRows, /*overrunBytes=*/forSigsNextCharged ? mode.overrunReserveBytes : 0u }; + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, /*json=*/false, + /*pasteHandle=*/true }; 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 ) + forSigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: IN — where step F aims (guard / soft target) + forSigsCut.pastTargetDisclosed = !forSoftTarget; // …and the default's rows past its target are disclosed + // round C: the render is named so a soft target can narrow its rows past the cut once the sections are served (below); + // the redaction tally around it is snapshotted so a second render replaces this one's count instead of adding to it + const std::optional redactBeforeSigs = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; + const auto renderSigs = [ & ]( std::FILE* sm ) { packSignatures( sm, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, // Q3: churn/clone/tested/amp folded onto the blocks @@ -3825,14 +3481,15 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co flRootArg, // R-E: root-relative p= 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 + &shownSigIds, // lane 2: the rows actually emitted — the tail's rank rows exclude 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; // knob-honesty-068: carries forSigsNext IN SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: zero cx=/ccx=/in= omitted (legend: absent = 0) - }, - sigsStr ); + }; + sigsPreRendered = preRender( renderSigs, sigsStr ); + const std::optional redactAfterSigs = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; if( !sigsPreRendered ) { DISCLOSE( blockCharge, ForLensBlockCharge::DisclosureWhy::SigsUnmeasured, @@ -3903,13 +3560,19 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // forbudgetmonotoncheck pins exactly that identity across ceilings. No "--" in the clause: it rides // inside an XML comment, where a double hyphen is ill-formed (G4). const bool forDefaultCeiling = forSigsCapped && cfg.tokenBudget == 0; + // forbudget-soft: the default answer's ONLY row cut — the runaway guard (a soft target's row stop is no "over") + const bool forGuardCut = forSigsCut.rowDropCut && !forSoftTarget; const std::string sigsCeilingAttr = forDefaultCeiling ? " budget_bytes=\"" + std::to_string( rw::kForPayloadBudgetBytes ) + "\"" : std::string(); // 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::sigsCutReportLegend( forSigsCut ); // + the continuation's and the docs reorder's readings (docs_after_code=) + // forbudget-soft: on an answer whose ranked head ran PAST the target (past_target=), the number shaped row text and + // removed no row — its reading says "target" there; elsewhere the payload fit it and the ceiling reading stays true + std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( forSigsCut.pastTarget > 0 ? rw::kForBudgetBytesTargetNote : rw::kForBudgetBytesNote ) + : std::string() ) + + rw::sigsCutReportLegend( forSigsCut ) // + the continuation's and the docs reorder's readings (docs_after_code=) + + ( forGuardCut ? rw::forRunawayGuardNote() : std::string() ); // forbudget-soft: the runaway guard's reading, where it cut // ── 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 @@ -3929,7 +3592,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // block to the RENDERED sigs' files and re-render (a byte-subset of what the budget already charged // for, so the bundle only shrinks; before est_tokens so the header reports the delivered size). if( sigsPreRendered && legoPreRendered && !legoStr.empty() - && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr, flRootArg.empty() ? std::string_view() : rw::sarif::rootPrefixOf( flRootArg ) ) ) + && narrowLegoToRenderedSigs( ing, legoScoped, rw::sigsRowsAtTarget( sigsStr, forSigsCut ), // forbudget-soft: scoped at the target + flRootArg.empty() ? std::string_view() : rw::sarif::rootPrefixOf( flRootArg ) ) ) { legoStr = captureXml( [ & ]( std::FILE* f ) { packLego( f, ing, legoScoped, lensRank, 12, redactPtr, impurePtr, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &g.implementors ); } ); } @@ -3960,8 +3624,13 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // the one rule both surfaces share, its ONE restoring invocation (used at every site below: the buffered substitution // and the degrade-path fallback), and the DEGRADE path it documents: no rendered bytes to gate on there, so the // section never collapses and is streamed whole below. + // round C (reserve first): a soft target serves the sections whole when they fit what the rows at its cut left + const std::size_t sigsAtCutBytes = sigsStr.size() - std::min( sigsStr.size(), forSigsCut.pastTargetBytes ); + const std::size_t sectionsShare = bundleBudget - std::min( bundleBudget, forHdrXml.size() + sigsAtCutBytes + routeStr.size() + graphSection.xml.size() ); const rw::ForSectionStubPlan stubPlan = rw::planForSectionStubs( - cfg.sections, cfg.forTask, + forSoftTarget && legoPreRendered && composePreRendered ? rw::forSoftSectionsOptIn( cfg.sections, legoStr.size(), composeStr.size(), sectionsShare ) + : std::string_view( cfg.sections ), + cfg.forTask, rw::ForStubbableSection{ .hasContent = legoPreRendered ? !legoStr.empty() : legoStubTotal > 0, .preCapTotal = legoStubTotal, .rendered = legoPreRendered, .renderedBytes = legoStr.size() }, rw::ForStubbableSection{ .hasContent = composePreRendered ? !composeStr.empty() : composeStubTotal > 0, .preCapTotal = composeStubTotal, @@ -4021,6 +3690,15 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // both kept alive past the render so the isRendered=false degrade path below re-emits the SAME set at // the SAME budget (the map path's emitSection lambda has the identical contract); detailIds is computed above std::size_t detailBodyBudget = 0; + // forbudget-soft: WHAT THE TRAILING SECTIONS ARE FUNDED FROM (bodies, hops, tail), in the default answer's priority + // order. DEFAULT: the bundle as it stood at the 7.5 KB target — the rows served past it (past_target=) are not charged + // to the sections after them, so no section shrinks because the head was kept whole. SOFT TARGET (round C, reserve + // first): the same rule at the soft target's cut — the rows the retired hard ceiling served at this N — so each section + // keeps at least the share that ceiling gave it; the header and every disclosure are never charged there. + const std::size_t sigsFundedBytes = sigsStr.size() - std::min( sigsStr.size(), forSigsCut.pastTargetBytes ); + const std::size_t rowsSpentBytes = forHdrXml.size() + sigsFundedBytes + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size(); + const std::size_t headerSpentBytes = forSoftTarget ? 0u + : headerStr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve; // --detail=N (lever 3): importance-weighted detail — spend FULL bodies on only // the top-N ranked symbols (the head the rank identifies), leaving the rest as the signatures emitted @@ -4039,20 +3717,17 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co detailBodyBudget = cfg.maxTokens > 0 ? rw::budgetBytesForTokens( std::size_t( cfg.maxTokens ) ) : cfg.packBudgetBytes; - if( cfg.tokenBudget > 0 ) + if( forSoftTarget ) { - const std::size_t spentBytes = headerStr.size() + forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() - + routeStr.size() + graphSection.xml.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve - + rw::kForFileTailShellReserve + forTailNext.size(); // deep-tail: the shell's reserved bytes + its cut next= (explicit regime only — this branch) - const std::size_t leftBytes = bundleBudget > spentBytes ? bundleBudget - spentBytes : 1; - // fix round 4: never below the first-body floor the sig side reserved (forDetailFirstBodyFloor) — the header's - // exempt disclosures are paid by this side, and they must not cut the one body the reserve was taken for + // the soft target's rows left this much; never below the first body's floor (forDetailFirstBodyFloor: head-cut + // with next= when oversized), so the one body the caller asked for always ships + const std::size_t leftBytes = bundleBudget > rowsSpentBytes ? bundleBudget - rowsSpentBytes : 1; detailBodyBudget = std::min( detailBodyBudget, std::max( leftBytes, detailFloorBudget ) ); } detailSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); }, // L3: --detail bodies surface notes too (part of the --for bundle) + /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); }, // L3: --detail bodies surface notes too (part of the --for bundle) rw::kBytesPerTokenBody ); if( !detailSection.isRendered ) { @@ -4063,27 +3738,23 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // ── T3: the terminal-by-default auto section (pre-registered: docs/EVALS.md §4) ──────────── // The decision + render live in buildForAutoBodies (above runForLens — this function is already one // of the largest in the file); this site only wires its verdict in: keep the section + attribute, or - // rebuild the header WITHOUT the legend when either off-switch fires, so the ladder's later rebuilds - // honor the decision too. TWO switches, and they differ in exactly one thing — the root attribute: - // surfaceOff (the chargeSection degrade) cleared it, legendOff (an exhausted explicit ceiling on - // either serving shape) KEPT it, so the exhausted case still discloses on the root while paying - // only the bytes it reserved up front. + // rebuild the header WITHOUT the legend when its off-switch fires, so the ladder's later rebuilds + // honor the decision too. One switch since forbudget-soft: surfaceOff (the chargeSection degrade), which + // clears the root attribute as well; a spent soft target keeps the legend and serves the hops cut marker + // (hopsCutMarker), whose clause rides and which clears the readings only hop rows could need. ForAutoBodiesResult enrich; // .attr is spliced onto the root after the ladder; its bytes are priced there if( autoBundleMode && sigsPreRendered ) { + // the DEFAULT regime funds the bodies from the bundle at its target (sigsFundedBytes) — the tail rides on top there and + // the bodies stay what they were; a soft target funds them from the rows' leftover (headerSpentBytes is 0) enrich = buildForEnrichment( cfg, ing, g, lensSurfaceIds, lensRank, plan, routeAnchorDefs, redactPtr, - headerStr.size() + forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() - + routeStr.size() + graphSection.xml.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve - + ( cfg.tokenBudget > 0 ? rw::kForFileTailShellReserve + forTailNext.size() : 0u ), - // deep-tail: under an explicit ceiling the tail SHELL's bytes are - // reserved ahead of the body walk (the kAutoAttrReserve pattern) so - // the disclosure always fits; the DEFAULT regime reserves nothing — - // the tail rides on top there and the bodies stay byte-identical. - bundleBudget, ForSectionPromises{ headerParts.viaPresent, &ownerPlan } ); - if( enrich.surfaceOff || enrich.legendOff ) + headerSpentBytes + rowsSpentBytes, + bundleBudget, ForSectionPromises{ headerParts.viaPresent, &ownerPlan, headerParts.splitPresent } ); + if( enrich.surfaceOff ) { headerParts.autoBundle = headerParts.compactBundle = false; headerParts.viaPresent = false; // FE-B: the section that could carry it is gone (autoBundleMode excludes --detail) + headerParts.splitPresent = false; // attrib-multidef Q1: likewise headerParts.ownerPresent = false; // owner-hop: the same section headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); } @@ -4097,63 +3768,141 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co enrich.attr = headerParts.compactBundle ? " bundle=\"compact\" bodies=\"0\" reason=\"degraded\"" : " bundle=\"auto\" bodies=\"0\" reason=\"degraded\""; headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, kForDegradedBundleNote ); } + // ── for-body-slot: THE ONE OWNER BODY, after the compact (forowner.h states the pick; the header carries its clause). + // DEFAULT regime: a required section of the complete answer, riding on top — , and are byte-identical + // to RIPWIRE_NO_BODY_SLOT=1, est_tokens prices it at the body rate, the runaway guard never reads it. SOFT TARGET (reserve + // first): funded after the hops from what the rows AT THE CUT left of T; it rides only when its whole render fits that + // share, else it is SKIPPED — shown="0" capped="1" next= the call that serves it, root reason="budget" — and the tail's + // share and the rows' refill below both charge what it served, so the rows past the cut give way to it. + rw::BodySlotRender slot; + if( slotOwner.any() && autoBundleMode && sigsPreRendered && !enrich.surfaceOff ) + { + const std::size_t slotSpent = rowsSpentBytes + plan.attrReserve + enrich.section.xml.size(); // the rows at the cut + the hops + slot = rw::renderForBodySlot( rw::BodySlotInputs{ ing, g.outOff, g.outTargets, g.outNameOnly, lensRank, flRootArg, redactPtr, notesPtr, + headerParts.viaPresent, cfg.compress, &g.outProv, headerParts.splitPresent }, + slotOwner, forSoftTarget ? std::optional( rw::forSoftSectionsShare( bundleBudget, slotSpent ) ) + : std::nullopt ); + if( slot.served ) + { + enrich.attr = " bundle=\"compact\" bodies=\"1\" reason=\"owner\""; // never bodies="1" beside reason="compact-route" + } + else if( slot.skipped ) + { + enrich.attr = " bundle=\"compact\" bodies=\"0\" reason=\"budget\""; + } + else + { + // the render's buffer failed: no body rides and nothing names one — the clause goes with it (the T3 degrade contract) + headerParts.bodySlotPresent = false; + headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + } + } + else if( headerParts.bodySlotPresent ) + { + headerParts.bodySlotPresent = false; // the compact section degraded (surfaceOff) or the rows were never measured + headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, enrich.surfaceOff ? std::string_view() : kForDegradedBundleNote ); + } + ASSUME( !slot.served || headerParts.bodySlotPresent, "a served owner body rides with the clause that defines it" ); const rw::ChargedSection& autoSection = enrich.section; const std::string& autoAttr = enrich.attr; - if( enrich.legendOff && !autoSection.xml.empty() ) + if( enrich.hopsCutMarker ) { - // fix round 4: the hops budget-cut marker rides a header whose compact legend was just dropped — its one - // present-only clause says what the empty element is (priced by the ladder through rootFinish) + // fix round 4: the hops budget-cut marker's present-only clause says what the empty element is sectionsStubNote += headerParts.compactLegend ? kForHopsBudgetCutLegendCompact : kForHopsBudgetCutLegend; + // forbudget-soft: the marker carries no row, so the readings only hop rows could need (the via="name" and the + // owner qword clauses) have nothing to define — cleared as the old attribute-only shape cleared them, so a + // spent target answers byte-identically with and without the owner plan (ownerhopcheck (B2)); the bundle legend stays. + // train 28 (CR pre-pass): attrib-multidef's prov="split" reading the same way — the lane cleared it on the retired + // spent-ceiling switch this marker replaced — unless a served body slot's rows lean on the header's definition + const bool splitOnlyHops = headerParts.splitPresent && !slot.served; + if( headerParts.viaPresent || headerParts.ownerPresent || splitOnlyHops ) + { + headerParts.viaPresent = false; + headerParts.ownerPresent = false; + headerParts.splitPresent = headerParts.splitPresent && !splitOnlyHops; + headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + } } - // ── DEEP-TAIL d2: render the file-grain tail, funded LAST (see serialize.h kForFileTailShownCap) ── - // Rendered after the bodies decision so the explicit regime spends only the RESIDUAL the rendered - // bundle actually left — the sigs and the bodies are byte-identical to a tail-less bundle in the - // default regime by construction (nothing above charges these bytes), and under a hard ceiling the - // weakest-evidence section is the one that trims. The pre-ladder headerStr sizes the residual (a - // ladder rung can only SHRINK the header, so the fit stays conservative and deterministic). - const FileTail forFileTailShown = sigsPreRendered ? computeFileTail( ing, lensRank, shownSigIds, flRootArg ) : forFileTail; - const std::string tailStr = renderForFileTailXml( forFileTailShown, cfg.tokenBudget, bundleBudget, - headerStr.size() + forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() - + routeStr.size() + graphSection.xml.size() + detailSection.xml.size() - + autoSection.xml.size() + autoAttr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve, - forTailNext ); - - // N1: the last rung's note DEFINES the root attribute it accompanies (over_ceiling= …), so the attribute is - // never on a document whose legend does not explain it; the bracket spelling stays. It is PROSE ONLY now — - // M3 took the verdict off it. finishForLensHeader used to recover "the last rung fired" by finding this - // exact text in the finished header, which the verbatim task echo could also supply. - static constexpr rw::CeilingLadderNotes kNotes{ - " [task_echo: dropped (ceiling)]", " [task_echo + route_attr: dropped (ceiling)]", - " [over_ceiling= is 1 on the root: the header floor (verbatim task echo + fixed legend) exceeds this budget" - " - no payload left to trim]" }; - // fix round 5 (owner ruling 2026-10-09, option B; reports/lego-charge.md): the both-trailing-sections shape under an - // explicit ceiling KEEPS its ranked rows (reserveTrailing above is off) and goes over the budget on purpose — answers - // first, a budget never silently removes an answer. There the header floor is NOT what overshoots (measured: a ~1.8 KB - // header under a 5.4 KB allowance), so blaming it was false. This note names what really rides past the budget. It - // is chosen only when the header as built fits the allowance by itself; a header floor that alone exceeds it keeps - // the note above (true for this shape: estchargecheck #11 A7B-N). Single-flag shapes still get the note above even - // where their header fits — pre-existing, a named follow-up. Spelled without double hyphens: it rides inside the - // header XML comment. - static constexpr rw::CeilingLadderNotes kNotesKeptRows{ - kNotes.echoDropped, kNotes.echoAndRouteDropped, - " [over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections, and with the" - " with-graph block and the first detail body at its floor they exceed this budget - kept, not trimmed]" }; - // train 27 (compactlegendcheck P1-B): every other over-ceiling shape whose header fits the allowance by itself. The header - // floor note above was served there too (lego-charge round 5's named follow-up), and it was false: the overshoot is the - // rows and sections the header carries, each cut of which names its next=. Chosen only when the header as built fits - // the allowance alone (headerAloneFits below). No double hyphens: it rides inside the header XML comment. - static constexpr rw::CeilingLadderNotes kNotesPayloadOver{ - kNotes.echoDropped, kNotes.echoAndRouteDropped, - " [over_ceiling= is 1 on the root: the header alone fits this budget; the rows and sections served with it exceed" - " it]" }; + // ── DEEP-TAIL d2: render the file-grain tail (FOLD C: page 0 of the file page; serialize.h kForFileTailShownCap / kForFileTailPageRows) ── + // Rendered after the bodies decision. Default regime: the full 40 rows ride on top — the sigs and the bodies are + // byte-identical to a tail-less bundle by construction (nothing above charges these bytes). A soft target: the tail is + // the last trailing section, funded from what the rows AT THE CUT and the sections before it left (reserve first). + // its rank rows exclude the shown rows' files — under a soft target the rows AT THE CUT, the ones a refill below keeps + const FileTail forFileTailShown = sigsPreRendered ? computeFileTail( ing, lensRank, lr.evidence, + forSoftTarget ? rowsAtTargetIds( shownSigIds, lensRank, forSigsCut.pastTarget ) + : shownSigIds, + flRootArg, cfg.forTask ) + : forFileTail; + // round C: the --detail bodies are the caller's own request under their own ceiling (--max-tokens / the pack budget), so + // they neither fund nor are funded by the reserved sections: the tail's share and the rows' refill both leave them out, + // and a body ceiling never moves a row (formaxtokenscheck (E)) + const std::size_t tailSpentBytes = forSoftTarget ? rowsSpentBytes + autoSection.xml.size() + slot.xml.size() + : rowsSpentBytes + detailSection.xml.size() + autoSection.xml.size() + slot.xml.size(); + std::string tailStr = renderForFileTailXml( forFileTailShown, cfg.tokenBudget, bundleBudget, tailSpentBytes ); + + // ── round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the rows past the soft target's cut fill what the + // trailing sections left of T. The sections above were funded as if the rows stopped at the cut (sigsFundedBytes), so + // each kept at least the share the retired hard ceiling gave it; the rows are now narrowed to T less those sections' + // served bytes — never below the cut (a row budget at or under the target IS the cut: serialize.h sigsRowDropEffective). + // Re-rendered only when that budget is narrower than the first render's, which changes only rows past the cut: the + // sections' scope (sigsRowsAtTarget) and funding read the rows AT the cut. The first render's redaction count is + // replaced, never added to; a re-render whose buffer fails keeps the first render and its readings whole. + if( forSoftTarget && sigsPreRendered && forSigsCut.pastTarget > 0 ) + { + // the tail is charged at its RESERVE — its share, capped at the whole render of the head-scoped tail (forFileTail: the + // rows outside the 40-row surface, the same at every N) — not at its filled bytes nor at the cut-scoped tail's size: + // both step with N (a whole row at a time; a file moving from the rank rows into the page rows gains its sym=), and + // the rows past the cut would then fall as N grows. With this reserve the rows are monotone in N. + std::vector tailEsc; + const std::size_t tailReserve = std::min( std::max( bundleBudget > tailSpentBytes ? bundleBudget - tailSpentBytes : 0u, + rw::kForFileTailShellReserve ), + rw::renderFileTailXml( forFileTail, forFileTail.paths.size(), tailEsc ).size() ); + const std::size_t sectionsServed = forHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size() + + autoSection.xml.size() + slot.xml.size() + tailReserve; // not the --detail bodies (above) + const std::size_t rowDropFill = rw::forSoftRowDropFill( bundleBudget, sectionsServed ); + if( rowDropFill < sigsBudgets.rowDrop ) + { + const rw::SigsCutReport firstCut = forSigsCut; + const std::size_t firstDropped = forDroppedPositive; + const bool firstCapped = forSigsCapped; + const std::vector firstShown = shownSigIds; + const std::optional redactNow = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; + rw::redactTakeBack( redactPtr, redactBeforeSigs, redactAfterSigs ); // the first render's count comes back out + forSigsCut = rw::SigsCutReport {}; + forSigsCut.continuationRequest = &forSigsNext; + forSigsCut.rowDropBudgetBytes = rowDropFill; + forSigsCut.pastTargetDisclosed = false; + std::string refill; + if( preRender( renderSigs, refill ) ) + { + sigsStr = std::move( refill ); + // the first render's spelling of the same attribute (no second fixed buffer: fixedbufsweep's census) + droppedPositiveNote = forDroppedPositive > 0 ? " dropped_positive=\"" + std::to_string( forDroppedPositive ) + "\"" + : std::string(); + sigsCeilingNote = rw::sigsCutReportLegend( forSigsCut ); // a soft target: no default ceiling, no guard reading + // lane 2: a row the refill dropped lands in the tail's rank rows — re-listed against the rows now shown, at + // the same share (the share, not the rows, is what the refill priced) + tailStr = renderForFileTailXml( computeFileTail( ing, lensRank, lr.evidence, shownSigIds, flRootArg, cfg.forTask ), + cfg.tokenBudget, bundleBudget, tailSpentBytes ); + } + else + { + forSigsCut = firstCut; + forDroppedPositive = firstDropped; + forSigsCapped = firstCapped; + shownSigIds = firstShown; + if( redactNow ) + { + *redactPtr = *redactNow; + } + } + } + } // PR #135: the priced root this header will carry — every late splice and the est_tokens fixpoint, in - // finishForLensHeader (above runForLens, with each splice's rationale). Assembled BEFORE the ladder so the - // ladder prices the header it will EMIT. NOT const (M3): .lastRungFired is the ladder's answer, written - // from the rung climbCeilingLadderBy returns — it starts false, which is exactly the state of a run whose - // ladder never ran. - ForLensRootFinish rootFinish{ + // finishForLensHeader (above runForLens, with each splice's rationale). + const ForLensRootFinish rootFinish{ .autoAttr = autoAttr, .sigsCeilingAttr = sigsCeilingAttr, .capAttrs = capAttrsStr, @@ -4170,192 +3919,20 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // F2: everything OUTSIDE the header at the markup rate. enrich.markupBytes is the compact section, // folded in here rather than charged separately — one rate, one rounding (ForEnrichmentPlan). .nonHeaderMarkupBytes = forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size() - + enrich.markupBytes + tailStr.size() + 6, // + "" (deep-tail: the tail at the markup rate) - // T3: the body-rate sections; enrich.bodyTokens is zero on the compact route (markup, above) - .bodyTokens = detailSection.tokens + enrich.bodyTokens, - .tokenBudget = cfg.tokenBudget, + + enrich.markupBytes + tailStr.size() + slot.markupBytes + 6, // + "" (deep-tail: the tail at the markup rate) + // T3: the body-rate sections; enrich.bodyTokens is zero on the compact route (markup, above); for-body-slot: the + // owner body at the body rate (a skipped slot's element is markup, above) + .bodyTokens = detailSection.tokens + enrich.bodyTokens + slot.bodyTokens, + // forbudget-soft: a soft target is never "over" — budget_tokens= names a target, so it is not a ceiling this root + // labels against; max_tokens= (the detail bodies' hard ceiling) still is, and the runaway guard's cut is the default's + .tokenBudget = 0, .maxTokens = cfg.maxTokens, - .bodyCeiling = forBodyCeiling }; - - // W3FIX H2 — the ceiling ladder (rungs + rationale: serialize.h climbCeilingLadder), same rungs in the - // same order --pack-task climbs. The header IS charged to the budget above, but charging is not FITTING: at - // an explicit --token-budget the header floor (fixed legend + the task echoed twice) can exceed the stated - // ceiling by itself, sigsBudget then clamps to 1, and packSignatures' first-entry-whole floor still emits - // ~1.5 KB — a silent 5.3x overrun on a 900-char task, against a --help that promises "trims to fit". Runs - // BEFORE est_tokens so the estimate covers the disclosure; inert without an explicit --token-budget. - if( cfg.tokenBudget > 0 && sigsPreRendered ) - { - // §F1: the ladder prices what will actually be EMITTED, so the two sections charged above are in - // this sum. headerSpliceReserve covers the est_tokens (and weak="1") attributes spliced in below — - // see its definition for why a reserve rather than a measurement. - const std::size_t ladderPayloadBytes = forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() + routeStr.size() - + detailSection.xml.size() + autoSection.xml.size() + graphSection.xml.size() - + tailStr.size() // deep-tail: the tail is priced like every other section - + autoAttr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve; // + "" + the header splices below (autoAttr exact-counted; A2's own reserve is exact too) - const std::size_t ladderCeiling = rw::ceilingAllowanceBytes( cfg.tokenBudget ); - // PR #135: the ALLOWANCE shape FITS when the reserve-priced header above does AND the header it would - // emit does — finishForLensHeader's output plus every other byte stdout receives (the body section is - // --detail's, else the rendered auto one). The first half is the pre-#135 test verbatim, so a bundle - // that was inside its allowance picks the same shape byte for byte. The second half is what the reserve - // cannot see: over_ceiling="1" and its legend clause (70 B), owed whenever est_tokens exceeds - // budget_tokens. Both halves are BYTES, because the allowance is a byte bound; the exact ceiling below - // is a token comparison and shares neither sum. - const std::size_t emittedNonHeaderBytes = forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() + routeStr.size() + tailStr.size() - + detailSection.xml.size() + ( autoSection.isRendered ? autoSection.xml.size() : 0u ) - + graphSection.xml.size() + 6; // + "" - // The 70 B of over_ceiling="1" + the clause defining it are priced by finishForLensHeader itself, from - // the TOKEN comparison (est_tokens > budget_tokens) it makes on every candidate — so a candidate that - // will carry the label is measured carrying it. Nothing here decides that from text: the ladder's own - // verdict used to be recovered inside finishForLensHeader by looking for the rung's note in the - // candidate, which the verbatim task echo could supply, and injected text then added those bytes to - // EVERY rung's price and could push an honest bundle down the ladder for no reason. - // `rootFinish.lastRungFired` is FALSE here and stays false, because it is the ladder's answer and the - // ladder has not answered yet. M3's first version passed the candidate's rung in and set the flag from - // `rung == OverCeiling`, which no call could ever satisfy — the terminal rung is the branch that never - // asks `fits` — so the flag was invariantly false while a comment claimed it was doing the pricing. - // THE ALLOWANCE test — raw bytes, which is its contract: ceilingAllowanceBytes is a BYTE bound - // (budget x 2.36 x 1.15), the widest thing this lens may deliver, and rungs (b)/(c)/(d) are judged - // by it (serialize.h climbCeilingLadderBy). Both halves: the reserve-priced sum above AND the header - // that will actually be emitted, because the reserve cannot see over_ceiling="1" and its clause (70 B), - // owed whenever est_tokens exceeds budget_tokens and priced by finishForLensHeader's own fixpoint. - const auto fitsCeiling = [ & ]( std::string_view candidate ) - { - return candidate.size() + ladderPayloadBytes <= ladderCeiling - && finishForLensHeader( std::string( candidate ), rootFinish ).size() + emittedNonHeaderBytes <= ladderCeiling; - }; - // …AND THE EXACT CEILING IS NOT A BYTE TEST AT ALL (PR #215 review: "use mixed-rate accounting for the - // exact-ceiling test"). What the root PROMISES is `est_tokens <= budget_tokens`, and est_tokens is not - // bytes/2.50: finishForLensHeader prices MARKUP at kBytesPerTokenDefault and the --detail / auto BODIES - // at kBytesPerTokenBody (3.80), one rate per kind. Comparing the raw byte total against - // ceilingBytes( budget ) charges every body byte at 2.50, i.e. 1.52x what the root charges it, and it - // also re-prices the candidate through `ladderPayloadBytes` — a sum built from RESERVES and from the - // auto section whether or not that section is rendered, so it is not the document stdout receives. - // Both errors point the same way: a document whose root says it fits was judged not to. MEASURED on a - // git-less 5-symbol fixture at --detail=1 (test/estchargecheck.sh #18): at every budget in 1069..1099 - // the kept document priced at est_tokens=1069 with no over_ceiling=, and rung zero dropped all three - // clauses anyway and delivered est_tokens=715 — 354 tokens of headroom spent to buy nothing, and three - // definitions the reader no longer has. At 1090..1099 even the raw byte total fitted (2 736 B of 2 737) - // and only the reserve-priced half vetoed. - // ONE QUESTION, ASKED ONCE: build the candidate's FINAL document and read the number it will print. - // No second fixpoint, no second rate — the disagreement is only possible while two expressions exist. - // The maxTokens/--detail body ceiling is deliberately NOT folded in: this rung has always been about - // the --token-budget ceiling, forLensOverCeiling answers the wider question on the finished document, - // and widening a rung is a change to what it drops, not a fix to how it prices. - const auto fitsExactCeiling = [ & ]( std::string_view candidate ) - { - return finishForLensHeaderPriced( std::string( candidate ), rootFinish ).estTokens <= cfg.tokenBudget; - }; - // RUNG ZERO — the confidence LEGEND clause, before any of the ladder's own rungs: it is the one - // header string whose loss costs NO unique information (confidence=/margin_pct= stay on the root - // as facts; only their explanation goes), so it must fall before the verbatim task echo does — - // the L1 "first rung that costs unique information" ordering. Same silent-legend-drop shape as - // the enrichment legendOff above (the ATTRIBUTE is the disclosure that survives a spent - // ceiling). Cleared on headerParts itself so every later ladder rebuild stays note-free. - // Measured need: fornotesbudgetcheck's 850-ceiling fixture holds 35 tokens of headroom and the - // clause is ~55 — charged-not-exempt (the explicit-regime split above) still cannot fit it, - // because the floor there is notes + first-entry-whole, neither of which may trim. - // L1: and it DISCLOSES itself now, like rungs (b) and (c) do — legendDropped splices - // kForLegendDroppedNote, naming the attributes whose definitions just went. Silence here was the - // reader seeing confidence= margin_pct= budget_tokens= r= and the counts with nothing in the - // legend about any of them and no way to tell a budget cut from a feature that does not exist. - // RUNG ZERO TRIGGERS ON THE EXACT CEILING, not on the 1.15 allowance rungs (c)/(d) are judged by - // (2026-09-13, PR #215). kCeilingFirstEntryTolerance exists for the residual a lens cannot trim — a first - // signature that is not divisible — and it was also gating this rung, so a document 1..15% over its budget - // that STILL carried three droppable clauses shipped over_ceiling=1 with all three riding - // (fornotesbudgetcheck's 1640 rung at est_tokens=1755, forrootlegendcheck's 800 rung at 831, both CI). - // …AND "EXACT" IS THE ROOT'S OWN RATE, not the densest-language one (review item 1). This test priced at - // kMinBytesPerToken(2.36) while est_tokens=/over_ceiling= price at kBytesPerTokenDefault(2.50), so the rung - // fired on documents 6% INSIDE the budget their root reports: `test/cppqualfix --for="widget ping make box" - // --token-budget=1200` printed est_tokens="778", no over_ceiling=, and had dropped all three clauses. - // …AND THE RATE WAS ONLY HALF OF IT: a BYTE ceiling cannot express this root's promise at all, because - // est_tokens is two rates (markup 2.50, bodies 3.80) over a document the byte sum also mis-assembles - // from reserves. fitsExactCeiling above is the token comparison itself now; see its own note. - // - // ONE DROPPABLE BIT. These three fields moved in lock step at every read and write, and the guard that used - // to test all three was true whenever any clause remained, which is the only state this branch is reached - // in — an always-true condition documenting itself as a check. legendDroppable() asks the one question. - // - // AND THE DROP MUST PAY (review item 3). Rung zero removes readings and splices the note that names - // them, and in the COMPACT dialect the note is longer than the clauses: measured on the pre-fix binary, - // 110-164 B of clauses went out and a 161 B note came in — +51 B net on a route-less answer, so the rung - // made the very document it was trying to shrink BIGGER while costing the reader three definitions. The - // candidate is built and compared; a drop that does not pay is not taken, and the readings simply ride. - // This is the assertion, not a comment about one: there is no shape in which the byte-negative branch - // can be reached, because the branch is the comparison. - // train 27 (compactlegendcheck P1-B): THE ROWLESS-SECTION CLAUSES go before any rung that costs information. The - // compact dialect decides its (compose) and (lego) readings BEFORE the payload, on an over-approximation - // (composePresent / legoPresent: a ranked symbol with a compose edge / implementors). By now the stub decision is made: - // a section served as its counted stub, or not at all, prints none of those rows, so the clause defines nothing in this - // document. Under a spent exact ceiling it goes and no note is owed — no printed attribute loses its reading. A document - // inside its ceiling keeps every byte; a section streamed unmeasured (the degrade path) keeps its clause. - if( headerParts.compactLegend && !fitsExactCeiling( headerStr ) - && headerParts.dropRowlessSectionReadings( composePreRendered && ( composeWillStub || composeStr.empty() ), - legoPreRendered && ( legoWillStub || legoStr.empty() ) ) ) - { - headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); - } - if( headerParts.legendDroppable() && !fitsExactCeiling( headerStr ) ) - { - const ForLensHeaderParts keptParts = headerParts; - headerParts.dropDroppableLegend(); - std::string droppedHeader = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); - if( droppedHeader.size() < headerStr.size() ) - { - headerStr = std::move( droppedHeader ); - } - else - { - headerParts = keptParts; // byte-negative: keep the readings and say nothing - } - } - // M3: the ladder hands back the rung it took. That value — never a search of the emitted text — is - // what puts over_ceiling="1" on the root below. - // fix round 5: the last rung's reason must be TRUE — the header floor only when the header alone overruns. - // train 27 (compactlegendcheck P1-B): …in every shape, not only the both-trailing-sections one: a header that fits - // its allowance by itself gets the reason that IS true (kNotesPayloadOver), never the header floor. And the compact - // dialect never echoes the task in its comment (appendCompactForLegend), so its echo rung drops nothing and claims - // nothing (the --pack-task FX4 rule): rung (b) is its header as built, rung (c) names only the route attribute. - // "Alone" is measured on the header AS THE LAST RUNG WOULD SERVE IT: this note, every late splice, the over_ceiling - // label and (when the handle rides unpaid) its clause — the bytes a reader counts up to , never a pre-splice draft. - ForLensRootFinish lastRungFinish = rootFinish; - lastRungFinish.lastRungFired = true; - lastRungFinish.sigsUnpaidOver = forSigsCut.continuationUnpaidBytes > 0; - const bool keptShape = explicitForCeiling && !reserveTrailing; - const std::string_view fitsNote = keptShape ? kNotesKeptRows.overCeiling : kNotesPayloadOver.overCeiling; - const bool headerAloneFits = - finishForLensHeader( buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, fitsNote ), lastRungFinish ).size() + 6 - <= ladderCeiling; - const rw::CeilingLadderNotes ladderNotes = forLadderNotes( headerParts.compactLegend, kNotes, headerAloneFits ? fitsNote : kNotes.overCeiling ); - const std::string ladderInputHeader = headerStr; // train 27: the as-built shape the overrun is measured on - rw::CeilingLadderChoice chosen = rw::climbCeilingLadderBy( buildForHeader, headerStr, fitsExactCeiling, fitsCeiling, - /*hasRouteAttr=*/!routeNoteRaw.empty(), ladderNotes ); - headerStr = std::move( chosen.header ); - rootFinish.lastRungFired = ( chosen.rung == rw::CeilingRung::OverCeiling ); - // 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 (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 ) ) - { - *sigsNextTryOther = true; - // …but never on the both-trailing-sections shape: there the ranked rows are kept and the answer goes over on - // purpose (owner ruling 2026-10-09, option B; kNotesKeptRows), so no row is dropped to pay an overrun - if( mode.overrunBytesOut != nullptr && !keptShape ) - { - *mode.overrunBytesOut = forUnlabelledOverrunBytes( ladderInputHeader, rootFinish, cfg.tokenBudget, forSigsCut.continuationPaidSurplus ); - } - 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; - } + .bodyCeiling = forBodyCeiling, + .guardOver = forGuardCut }; + + // forbudget-soft: no header ladder. The W3FIX ceiling ladder (task echo / route= / legend clauses dropped "(ceiling)", + // over_ceiling's header-floor note) existed to land a HARD ceiling; an explicit --token-budget is a soft target over the + // rows now (owner decision 2026-10-14), so the header — every legend clause and the verbatim task echo — always ships. headerStr = finishForLensHeader( std::move( headerStr ), rootFinish ); std::fwrite( headerStr.data(), 1, headerStr.size(), stdout ); @@ -4370,6 +3947,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { rw::SigsCutReport degradeSigsCut; // knob-honesty-068: carries the continuation request IN, as on the buffered path degradeSigsCut.continuationRequest = &forSigsNext; + degradeSigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: the same two budgets + degradeSigsCut.pastTargetDisclosed = !forSoftTarget; packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, rw::forLensRules( forDocsAfterCode, forEndLines ), nullptr, nullptr, nullptr, // the degrade path selects identically @@ -4420,10 +3999,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // 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, g.outNameOnly, + rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); } ); + /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); } ); } else if( autoSection.isRendered && !autoSection.xml.empty() ) { @@ -4433,6 +4012,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // contract for this optional enrichment, unlike --detail's explicit request (see the block above). std::fwrite( autoSection.xml.data(), 1, autoSection.xml.size(), stdout ); } + // for-body-slot: the owner body (or its skip disclosure) after the hops, from the bytes est_tokens priced ("" when none) + std::fwrite( slot.xml.data(), 1, slot.xml.size(), stdout ); // R8: --with-graph — a compact mermaid flowchart of the ranked bundle's anchor neighborhood, // right before . Off by default (G5): omitted, this is a no-op and output is byte-identical. @@ -4448,15 +4029,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co return std::nullopt; } -// 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. +// The --for lens: the ranking runs once, the render once (forbudget-soft: no payment-mode re-renders — the lens has no hard +// ceiling left to land a charged continuation under). std::optional runForLens( const MainDispatch& d ) { const rw::Config& cfg = d.cfg; @@ -4469,52 +4043,7 @@ std::optional runForLens( const MainDispatch& d ) // the at= stamp (gitstamp.h): one read per run — every payment-mode render below shares it, like the ranking const bool forSingleRoot = d.ing.realPaths.empty() && cfg.roots.size() == 1; const std::string forAtStamp = forSingleRoot ? rw::gitstamp::stampAt( d.root ) : std::string(); - // 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 ), ForLensPassMode{ .sigsNextPayFromRows = true, .forAtStamp = forAtStamp } ); - } - const rw::RedactCounts tallyAtEntry = d.redactCounts; - bool unpaidOver = false; - std::optional rc = runForLensPass( d, lr, ForLensPassMode{ .sigsNextPayFromRows = false, .sigsNextTryOther = &unpaidOver, .forAtStamp = forAtStamp } ); - if( !unpaidOver ) - { - return rc; - } - ASSUME( !rc ); // the unpaid render that handed back wrote nothing - d.redactCounts = tallyAtEntry; - bool paidOver = false; - std::size_t overrunBytes = 0; - const bool compactPays = cfg.legend == "compact"; - rc = runForLensPass( d, lr, ForLensPassMode{ .sigsNextPayFromRows = true, .sigsNextTryOther = &paidOver, .forAtStamp = forAtStamp, - .overrunBytesOut = compactPays ? &overrunBytes : nullptr } ); - if( !paidOver ) - { - return rc; - } - ASSUME( !rc ); // the paid render that handed back wrote nothing - d.redactCounts = tallyAtEntry; - // train 27 (compactlegendcheck P1-B): THE COMPACT DIALECT ALSO PAYS ITS HEADER. Its sig ledger is capped at the full - // dialect's header charge (runForLensPass, the L1 fix round: rows(default) ⊇ rows(full)), so the bytes its header emits - // beyond that charge are paid by nobody; lego-charge charged the stubbed / as served, which removed the - // slack that used to absorb them, and the paid compact answer landed past budgets the full answer fits. Where paying the - // handle is not enough, pay the MEASURED overrun from the rows too — once, and serve it only when it then fits (C3: no row is - // dropped unless that buys the fit; the rows it drops are the ruled exemption, compactlegendcheck P1-B). The full dialect's - // ledger is not capped — it charges its own header, less the disclosures both dialects exempt — so this payment is the - // compact dialect's only, and a full answer past its ceiling after paying keeps C3's unpaid fallback below, unchanged. - if( compactPays && overrunBytes > 0 ) - { - bool overrunOver = false; - rc = runForLensPass( d, lr, ForLensPassMode{ .sigsNextPayFromRows = true, .sigsNextTryOther = &overrunOver, .forAtStamp = forAtStamp, - .overrunReserveBytes = overrunBytes } ); - if( !overrunOver ) - { - return rc; - } - ASSUME( !rc ); // the overrun-paying render that handed back wrote nothing - d.redactCounts = tallyAtEntry; - } - return runForLensPass( d, std::move( lr ), ForLensPassMode{ .sigsNextPayFromRows = false, .forAtStamp = forAtStamp } ); + return runForLensPass( d, std::move( lr ), ForLensPassMode{ .forAtStamp = forAtStamp } ); } std::optional runTargetedViews( const MainDispatch& d ) @@ -4568,7 +4097,7 @@ std::optional runTargetedViews( const MainDispatch& d ) rw::legoClosureLegendComment( g.implementors, g.implementorBound, focus ).c_str() ); // lane lego-transitive: present-only, the deeper rows const std::string legoTargetedAttr = rw::unprovenDefsAttrXml( legoUnprovenDefs ) + rw::graphCountFloorAttrXml( g ); // outlives the call packLego( stdout, ing, g.implementors, flat, 1, d.redactPtr, &legoImpure, focus, /*withPaths=*/true, tvRootArg, - rw::LegoTargetedArgs( legoTargetedAttr, cfg.pageLimit, cfg.pageOffset, &g.implementorBound ) ); // H1 + M15: residue, gauge, marker; the deeper-row page + rw::LegoTargetedArgs( legoTargetedAttr, cfg.pageLimit, cfg.pageOffset, &g.implementorBound ).withMainRows( g.rubyMainMixins ) ); // H1 + M15: residue, gauge, marker; the deeper-row page rw::emitRaw( stdout, "" ); reportRedactions( stderr, d.redactCounts ); // W3-N1: a contract sig is a redacting seam — disclose the tally return 0; @@ -4773,6 +4302,7 @@ std::optional runPackTask( const MainDispatch& d ) PackTaskInputs in; in.budgetTokens = cfg.tokenBudget; // F5: stays std::size_t end-to-end (0 ⇒ the shared default) + in.softTarget = cfg.tokenBudget > 0 && cfg.partitionCount == 0; // forbudget-soft: the caller's budget is a soft target in.compactLegend = cfg.legend == "compact" && !cfg.json; // L1 fix round: the ladder prices what the compact layer delivers in.sigLadderBudgetBytes = cfg.packBudgetBytes; in.compress = cfg.compress; diff --git a/src/verbs_grep.h b/src/verbs_grep.h index 2e07f03d1..9d678beb4 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -41,8 +41,41 @@ struct GrepEncOptions bool handles; std::string_view root; std::vector& esc; + std::span carriers; // L4: parallel to the rows (grepcarrier.h), or empty when no row is a carrier + bool singleRoot; // reads_at= paths strip rootPrefix exactly when the hit rows' p= do + std::string_view rootPrefix; }; +// L4 (grepcarrier.h): a carrier row's reads=/reads_at= (the uses verb's read sites for its name; absent = not counted) +// and vals=/vals_total= (the function values it stores). Pure serialization of grepCarriers' verdict. +std::string grepCarrierAttrs( const rw::GrepCarrier& c, bool singleRoot, std::string_view rootPrefix, std::vector& esc ) +{ + const rw::GrepCarrierText t = rw::grepCarrierText( c, singleRoot, rootPrefix ); + const auto joined = [ & ]( const std::vector& items ) + { + std::string out; + for( std::size_t k = 0; k < items.size(); ++k ) + { + out += ( k > 0 ? " " : "" ) + std::string( rw::escapeXml( items[k], esc ) ); + } + return out; + }; + std::string out; + if( !t.readsAt.empty() ) + { + out += " reads=\"" + std::to_string( c.reads ) + "\" reads_at=\"" + joined( t.readsAt ) + "\""; + } + if( !t.vals.empty() ) + { + out += " vals=\"" + joined( t.vals ) + "\""; + if( c.valsTotal > c.vals.size() ) + { + out += " vals_total=\"" + std::to_string( c.valsTotal ) + "\""; + } + } + return out; +} + class GrepHandleAttrs { public: @@ -145,11 +178,129 @@ void emitGrepEncRows( const rw::IngestResult& ing, const rw::Graph& g, std::span { rw::emitTo( stdout, " tested=\"1\"" ); } + if( r < opt.carriers.size() && opt.carriers[r].isCarrier ) + { + std::fputs( grepCarrierAttrs( opt.carriers[r], opt.singleRoot, opt.rootPrefix, opt.esc ).c_str(), stdout ); + } std::fputs( handleAttrs.forRow( row ).c_str(), stdout ); rw::emitTo( stdout, "/>" ); } } +// L4 (grepcarrier.h): the carrier verdict for a page's rows. vals= reads this run's own value index (both ingest +// families capture value references). reads= needs role="read" references, which only the RICH family captures, and +// --grep runs LEAN (cli.h needsValueUses): so when the TOP hit sits in a carrier the uses verb can read for +// (grepCarriersWantReads — the row the root next= is decided on) and this run is lean, the same root(s) are ingested once +// more, rich, and dropped before the answer prints. Any other page pays nothing: a lower carrier keeps its floor_next= +// (the uses verb on its name) as the clue instead of reads=. +// * cache: the rich family's own warm blob per root (the uses verb's, so a tree that answered the uses verb already +// pays a cache load, not a parse) — unless the user NAMED a cache (--cache=F): F holds the lean family, the rich +// family would evict it (the two never cross-hit), and nothing else may be written behind the user's back, so the +// second ingest then runs uncached. --no-cache: uncached too. +// * workspace (multi-root): one rich ingest per root, merged exactly as main() merges the lean ones, so reads_at= +// spells the same label/path the MCP workspace index (rich) prints. +// * memory: the guard covers it like any ingest — the soft line inside it (--max-memory), then the hard line after +// each root. A stop drops the rich ingest; the answer keeps the lean rows and its root says so in band +// (reads_skipped= and reads_next=, the uses verb on the top hit's carrier), never a silent drop or stderr alone. +struct GrepCarrierReadsSource +{ + enum class DisclosureWhy : std::uint8_t { MemoryStop, HardLimit }; + std::optional stop; // set by a stopped second ingest: reads= stays unknown and the root names why + void disclose( DisclosureWhy why ) noexcept { stop = why; } +}; + +// The root's reads_skipped= spelling of each stop, indexed by DisclosureWhy (the legends define both). +inline constexpr std::array kGrepReadsSkippedName = { "memory_stop", "hard_limit" }; + +struct GrepPageCarriers +{ + std::vector carriers; + std::string skippedAttrs; // " reads_skipped=… reads_next=…" on a memory-stopped second ingest, else "" +}; + +// The rich ingest of each root of this run, in order: one for a single root (uncached under --no-cache or a NAMED +// --cache=F, see above), one per workspace root (main()'s per-root auto-cache keying). A workspace stops adding roots +// at the first one the guard stopped or that left the footprint over the hard line. +std::vector grepReadsRoots( const MainDispatch& d ) +{ + using namespace rw; + const Config& cfg = d.cfg; + const auto one = [ & ]( const std::string& dir, std::string_view label, std::string_view cachePath ) + { + return ingest( dir.c_str(), cfg.excludes, cachePath, cfg.maxFileBytes, /*captureValueUses=*/true, label, /*respectGitignore=*/!cfg.noIgnore ); + }; + std::vector parts; + if( !d.multiRoot ) + { + parts.push_back( one( d.root, {}, cfg.noCache || !cfg.cacheFile.empty() ? std::string() : defaultCachePath( d.root, true ) ) ); + return parts; + } + for( const WorkspaceRoot& r : d.ws ) + { + parts.push_back( one( r.arg, r.label, cfg.noCache ? std::string() : defaultCachePath( r.arg, true ) ) ); + if( parts.back().memoryStop.isSet() || memguard::overHardLimit() ) + { + break; // the next root would only add to an ingest already over the line + } + } + return parts; +} + +// The second, rich ingest of this run's root(s), or nullopt when the memory guard stopped it (`source` then says why). +std::optional grepReadsIngest( const MainDispatch& d, GrepCarrierReadsSource& source ) +{ + using namespace rw; + std::vector parts = grepReadsRoots( d ); + const bool stopped = std::ranges::any_of( parts, []( const IngestResult& p ) { return p.memoryStop.isSet(); } ); + if( stopped || parts.size() < ( d.multiRoot ? d.ws.size() : 1 ) || memguard::overHardLimit() ) + { + memguard::answerStops(); // answered here: reads= stays absent (unknown), the rest of the answer is the lean run's + if( stopped ) + { + DISCLOSE( source, GrepCarrierReadsSource::DisclosureWhy::MemoryStop, "grep: the rich re-ingest for reads= stopped at the memory guard's soft line; the root carries reads_skipped=" ); + } + else + { + DISCLOSE( source, GrepCarrierReadsSource::DisclosureWhy::HardLimit, "grep: over the memory guard's hard line after the rich re-ingest for reads=; the root carries reads_skipped=" ); + } + return std::nullopt; + } + IngestResult rich = d.multiRoot ? mergeWorkspaceIngests( d.ws, parts ) : std::move( parts.front() ); + if( d.cfg.ignoreTests ) + { + applyIgnoreTests( rich ); // the same population the lean ingest answers from + } + return rich; +} + +GrepPageCarriers grepPageCarriers( const MainDispatch& d, std::span hits, std::span rows ) +{ + const rw::IngestResult& ing = d.ing; + if( std::ranges::none_of( rows, [ & ]( const rw::GrepEncRow& row ) { return rw::isGrepCarrierRow( ing, row ); } ) ) + { + return {}; + } + const rw::ValueRefIndex vri( ing ); + if( !rw::grepCarriersWantReads( ing, hits, rows ) ) + { + return { rw::grepCarriers( ing, vri, rows, nullptr ), {} }; + } + if( d.valueUses ) + { + return { rw::grepCarriers( ing, vri, rows, &ing ), {} }; + } + GrepCarrierReadsSource source; + const std::optional rich = grepReadsIngest( d, source ); + if( !rich ) + { + ASSUME( source.stop.has_value(), "grepReadsIngest returns nothing only after a DISCLOSE named the stop" ); + const std::string next = rw::nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ); + return { rw::grepCarriers( ing, vri, rows, nullptr ), + std::string( " reads_skipped=\"" ) + std::string( kGrepReadsSkippedName[ static_cast( *source.stop ) ] ) + "\"" + rw::nextAttrXml( next, "reads_next" ) }; + } + return { rw::grepCarriers( ing, vri, rows, &*rich ), {} }; +} + // 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. @@ -166,6 +317,27 @@ std::vector grepEncFloors( const rw::IngestResult& ing, const r // 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. +// L4 (grepcarrier.h): the carrier attributes and the next= rule they switch, spliced only when a row on the page carries +// one (so a page without a carrier keeps every legend byte). No attribute=value literal (this legend's rule) and no +// double dash (an XML comment). +inline constexpr const char* kGrepCarrierClause = + "On a row whose definitions are all variables, constants or initialisers (a carrier), reads= counts the read sites the " + "uses verb lists for that NAME (matched by name, every same-named definition; present only when nonzero and only where " + "that verb indexes reads, so its absence means none counted, never none exist) and reads_at= names the first three as " + "file:line in tier, path, line order (source before tests, then path and line, not the order the uses verb prints); vals= names up to three function values the " + "carrier itself stores (resolved by name to an indexed function), as slot=function@line: the slot key (absent for a " + "plain assignment) and the line of the one definition that name resolves to (path:line when it is in another file, no " + "line when the name resolves to several); vals_total= counts all of them when more than three. When the top hit sits in a carrier with " + "reads=, the root next= is the uses verb on that carrier instead of the at verb. "; + +// L4: the root's disclosure when the memory guard stopped the rich ingest that counts reads (grepReadsIngest) — spliced +// only on such an answer. Same rules as the clause above. +inline constexpr const char* kGrepCarrierSkipClause = + "When the root carries reads_skipped, the top hit sits in a carrier whose reads were NOT counted: the second, rich index " + "that counts them stopped at the memory guard (memory_stop: the soft line of the max memory flag or system pressure " + "during it; hard_limit: the footprint was over the hard line after it), so no row has reads= or reads_at= and the root " + "next= is unchanged; reads_next is the uses verb on that carrier, the call that lists those reads on its own. "; + 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 " @@ -685,11 +857,19 @@ std::vector grepHitRefs( const rw::Config& cfg, const rw::IngestResu return refs; } -int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, - const std::vector* amp, const std::vector* tested, - const GrepScanPhases* prefetched ) +// The dispatch context whole: amp/tested are non-null only when a co-run (--metrics) computed them at dispatch build +// time — grep itself never asks for the analysis (R1's no-new-analysis rule); root and valueUses feed L4's reads pass. +int emitGrepReport( const MainDispatch& d ) { using namespace rw; + const Config& cfg = d.cfg; + const IngestResult& ing = d.ing; + const Graph& g = d.g; + const std::vector* amp = d.ampPtr; + const std::vector* tested = d.testedPtr; + const GrepScanPhases* prefetched = d.grepPhases; + const std::string& root = d.root; + const bool valueUses = d.valueUses; const std::string pat( cfg.grep ); // §P0.4: an invalid --regex used to scan nothing and print hits="0" at exit 0 with an EMPTY stderr — @@ -764,6 +944,9 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw 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; } ); + const GrepPageCarriers pageCarriers = grepPageCarriers( d, hits, encRows ); // L4, before the legend + const std::vector& encCarriers = pageCarriers.carriers; + const bool anyCarrierAttr = std::ranges::any_of( encCarriers, []( const GrepCarrier& c ) { return ( c.readsKnown && c.reads > 0 ) || !c.vals.empty(); } ); const std::vector hitRefs = grepHitRefs( cfg, ing, tierReport, encRows, hits ); // idea #5: ref= (empty off a code+string answer) std::vector esc; const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; @@ -912,7 +1095,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: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=) + "{}{}{}" // count-floor: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=); L4: kGrepCarrierClause and kGrepCarrierSkipClause, the same "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 @@ -965,7 +1148,8 @@ 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. " - "{} -->", anyEncFloor ? kGrepEncFloorClause : "", rw::kPageRaiseCapClause ); + "{} -->", anyEncFloor ? kGrepEncFloorClause : "", anyCarrierAttr ? kGrepCarrierClause : "", + pageCarriers.skippedAttrs.empty() ? "" : kGrepCarrierSkipClause, rw::kPageRaiseCapClause ); } emitGrepHandleLegend( cfg.grepHandles ); // G3: terms=/scope=/suppressed= — only when AND/NOT was actually given, so a plain --grep answer @@ -996,6 +1180,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw { scanShortAttr += rw::kGraphCountFloorAttrXml; } + scanShortAttr += pageCarriers.skippedAttrs; // L4: the reads the memory guard stopped (reads_skipped=, reads_next=), or "" const char* schemaAttr = cfg.legend == "compact" ? " schema=\"ripwire.grep/v1\"" : ""; // P3 (L7, nextverb.h): the one follow-up. A CUT answer → the next page, under the compact legend (the page // is what the agent wants, not the prose it has already read); a hit → the enclosing-definition chain at the @@ -1005,6 +1190,10 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw { grepNext = rw::nextFlag( cfg.grepRegex ? "--regex=" : "--grep=", pat ) + " --offset=" + std::to_string( grepPage.end ) + " --legend=compact"; } + else if( const std::string carrierNext = rw::grepCarrierNext( ing, hits, encRows, encCarriers ); !carrierNext.empty() ) + { + grepNext = carrierNext; // L4: the top hit sits in a carrier the uses verb lists reads for — those reads are the next hop + } else if( !hits.empty() ) { grepNext = rw::nextFlag( "--at=", std::string( pathFor( hits[ 0 ].fileId ) ) + ":" + std::to_string( hits[ 0 ].line ) ); @@ -1113,7 +1302,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 }; + cfg.roots.size() == 1 ? cfg.roots[0] : std::string_view(), esc, encCarriers, singleRoot, rootPrefix }; emitGrepEncRows( ing, g, encRows, encFloors, encOpt ); // ── R1a: the zero-hit follow-up — suggestions, labeled as such, never matches (helper above) ─── @@ -1134,9 +1323,7 @@ std::optional runGrep( const MainDispatch& d ) { return std::nullopt; // not this verb — fall through the dispatch chain } - // body: emitGrepReport() above. amp/tested are non-null only when a co-run (--metrics) computed - // them at dispatch build time — grep itself never asks for the analysis (R1's no-new-analysis rule). - return emitGrepReport( d.cfg, d.ing, d.g, d.ampPtr, d.testedPtr, d.grepPhases ); + return emitGrepReport( d ); // body: emitGrepReport() above } } // namespace — verbs_grep.h section of main.cpp diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index cbcc9acdb..726fc3277 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -214,6 +214,25 @@ std::optional runCallHierarchy( const MainDispatch& d ) const rw::RowCallSites chSites = wantCallers ? rw::rowCallSites( ing, matches, chPage, rw::VrRender{ chSingleRoot, chRootPrefix } ) : rw::RowCallSites{}; + // attrib-multidef (defattrib.h): the definition table this page lists (every group up to the cap, else the ones its rows + // name) and whether a row on it is a split arm of a one-group answer (Q1) — computed once for every dialect below. + const rw::OneHopAttribution& chAttr = chRows.attr; + std::vector chNamed; + rw::defTableMarkNamed( chNamed, chAttr.groups, chAttr.tokens, pw.begin, pw.end ); + const rw::DefTableListing chListing = rw::defTableListing( chAttr.groups, chNamed ); + bool chHasSplit = false; + for( std::size_t i = pw.begin; i < pw.end && !chHasSplit; ++i ) + { + chHasSplit = chAttr.splitArm( i ); + } + // lane declined-sites (declinedsites.h): the rows — the declined call SITES declined_calls= counts, in callhierarchy.h's + // served order, cut at the default cap (--limit raises it; dc_next= names that call) — computed once for every dialect below. + const rw::DeclinedSiteCut chDcCut = rw::declinedSiteCut( chRows.declinedSites.size(), cfg.pageLimit, wantCallers ? "--callers=" : "--callees=", sym, + std::size_t( effectiveRowCap( cfg.pageLimit, rw::kCallHierarchyRowCap ) ) ); + const std::vector chDc = + rw::declinedSiteViews( ing, g, std::span( chRows.declinedSites ).first( chDcCut.shown ), wantCallers, matches, chAttr.groups, + rw::VrRender{ chSingleRoot, chRootPrefix }, cfg.legend == "full" ); + const std::string chDcRows = rw::declinedSitesXml( chDc, wantCallers ); // §H4 §3.4: the FIRST legend these two verbs have ever shipped (0 bytes before — which is why every // one of their root attributes sits in test/legendcoverage_baseline.txt), and the floor marker that @@ -239,17 +258,20 @@ std::optional runCallHierarchy( const MainDispatch& d ) bool chHasVia = false; for( std::size_t i = pw.begin; i < pw.end && !chHasVia; ++i ) { - chHasVia = rowNameOnly( g, matches, result[ i ], wantCallers ); + chHasVia = chAttr.nameOnly( i ); // FE-B's via="name": every edge to the matches by name (defattrib.h, one pass) } 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::declinedCallsLegendWithGates( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + rw::declinedCallsLegendForRows( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0, !chDc.empty(), + !wantCallers && rw::inertDeclinedMadeBy( g, matches ) > 0 ) // exactly when the root carries declined_calls=; the inert sentence only where THESE definitions made an inert quoted site + + rw::declinedSitesLegend( rw::declinedSitesLegendFlags( chDc, wantCallers, chDcCut ) ), // and the rows, exactly when one rides std::string( rw::declinedIfaceLegend( chRows.declinedIface > 0 ) ) // likewise, exactly when declined_iface= is there + rw::stdMemberCallsLegend( chRows.stdCalls.calls > 0 && !cfg.columnar ), // and when the line is (XML only) ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there + 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::viaNameLegend( chHasVia, false, cfg.columnar ) // FE-B: likewise for via="name" + + rw::defTableLegend( chAttr.groups.multi(), chListing.cut ) + rw::splitArmLegend( chHasSplit ) ).c_str(), // attrib-multidef: likewise ( std::string( rw::modScopeLegend( chHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is + rw::callSitesAtLegend( chSites.any, chSites.anyCut, rw::kCallSitesPerRowCap ) ).c_str(), // CALLSITE-AT: likewise for sites_at=/sites_total= ( rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ) + rw::valueRefsLegend( !chRows.valueRefs.rows.empty(), wantCallers ) @@ -263,7 +285,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) // Callees keep expand on the original selector; nextFlag retains quoting and the 120-byte contract. const std::string chNextAttr = rw::nextAttrXml( rw::nextFlag( wantCallers ? "--uses=" : "--expand=", chNextSelector ) ); // 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 ); + const std::string chDeclinedAttr = rw::declinedCallsAttrXml( chRows.declinedCalls ) + rw::declinedSiteCutAttrXml( chDcCut ) // + the cut, beside the count it cuts + + rw::declinedIfaceAttrXml( chRows.declinedIface ); // Reference-as-value round: value_refs= beside count= (never in it), absent at zero; the window after the rows. const rw::VrRender chVr { chSingleRoot, chRootPrefix }; const std::string chValueRefsAttr = ( rw::valueRefsCountAttrXml( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthAttrXml( ing, chRows.valueRefs.depthCut, chVr ) ); // + the depth cut that makes it a floor @@ -286,6 +309,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) + ( !wantCallers && bodylessDefsCount > 0 ? " bodyless_defs=\"" + std::to_string( bodylessDefsCount ) + "\"" : "" ) + chUnprovenAttr // H1: the decl→def residue, on BOTH directions + rw::crossKindAttrXml( chRows.crossKind ) // hono-07: defs of 2+ kinds, beside defs= + + rw::defsListedAttrXml( chAttr.groups, chListing ) // attrib-multidef: a cut definition table says so + chTested.xmlAttr // A6: hop_tested=/hop_untested=, the same partition on every dialect + chDeclinedAttr // the tier-3 declines, beside the count they are not in + chValueRefsAttr // value uses, beside the count they are not in (rows: the XML form) @@ -294,13 +318,22 @@ 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 - std::vector pageVia( page.size(), 0 ); // FE-B: the rows' hedge, a dense column when any row carries it + std::vector pageVia( page.size(), 0 ); // FE-B: the rows' hedge, a dense column when any row carries it + std::vector pageSplit( page.size(), 0 ); // attrib-multidef Q1: likewise for a one-group answer's split arms + std::vector pageOfDef; // attrib-multidef: of_def per row, only on a multi-group answer for( std::size_t i = 0; i < page.size(); ++i ) { - pageVia[ i ] = rowNameOnly( g, matches, page[ i ], wantCallers ) ? 1 : 0; + pageVia[ i ] = chAttr.nameOnly( pw.begin + i ) ? 1 : 0; + pageSplit[ i ] = chAttr.splitArm( pw.begin + i ) ? 1 : 0; + if( chAttr.groups.multi() ) + { + pageOfDef.push_back( rw::ofDefColumnValue( chAttr.groups, chAttr.tokens, pw.begin + i ) ); + } } + const std::string chDefRows = rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ); emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, - rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals } ); // CALLSITE-AT: sites_at only when a row has sites + rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals, // CALLSITE-AT: sites_at only when a row has sites + chAttr.groups.multi() ? &pageOfDef : nullptr, &chDefRows, &pageSplit, &chDcRows } ); // + the rows verbatim return 0; } @@ -315,12 +348,14 @@ std::optional runCallHierarchy( const MainDispatch& d ) { rw::emitTo( stdout, ",\"bodyless_defs\":{}", bodylessDefsCount ); } - rw::emitTo( stdout, "{}{}", rw::unprovenDefsKeyJson( chRows.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin - rw::crossKindKeyJson( chRows.crossKind ).c_str() ); // hono-07: absent unless defs span 2+ kinds + rw::emitTo( stdout, "{}{}{}", rw::unprovenDefsKeyJson( chRows.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin + rw::crossKindKeyJson( chRows.crossKind ).c_str(), // hono-07: absent unless defs span 2+ kinds + rw::defTableJson( ing, chAttr.groups, chListing, chRootPrefix, /*mcp=*/false ) ); // attrib-multidef: the table // R-E: the JSON twin of the XML root= below — right after the leading identifying fields. 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::declinedCallsKeyJson( chRows.declinedCalls ) + rw::declinedSiteCutKeyJson( chDcCut ) + + rw::declinedIfaceKeyJson( chRows.declinedIface ) // A6; then the XML root's declined_calls=/dc_shown=/dc_next=/declined_iface= + rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ), // and the XML line ( rw::valueRefsCountKeyJson( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthKeyJson( ing, chRows.valueRefs.depthCut, chVr ) ) ); rw::emitTo( stdout, "{}{}", pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, @@ -331,14 +366,16 @@ std::optional runCallHierarchy( const MainDispatch& d ) std::vector chAtKeys( result.size() ); // CALLSITE-AT: the rows' sites_at/sites_total keys, absent when none for( std::size_t i = pw.begin; i < pw.end; ++i ) { - chVia[ i ] = rowNameOnly( g, matches, result[ i ], wantCallers ) ? 1 : 0; + chVia[ i ] = chAttr.nameOnly( i ) ? 1 : 0; if( chSites.any ) { chAtKeys[ i ] = rw::callSitesKeyJson( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ); } + chAtKeys[ i ] += rw::ofDefKeyJson( chAttr.groups, chAttr.tokens, i ) + rw::splitArmKeyJson( chAttr.splitArm( i ) ); // attrib-multidef } printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia, &chAtKeys ); - rw::emitTo( stdout, "]{}}}", rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); + rw::emitTo( stdout, "]{}{}}}", rw::declinedSitesJson( chDc, wantCallers, /*mcp=*/false ), // the XML rows, keys = its attribute names + rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); return 0; } @@ -349,25 +386,29 @@ std::optional runCallHierarchy( const MainDispatch& d ) { rw::emitTo( stdout, " bodyless_defs=\"{}\"", bodylessDefsCount ); } - rw::emitTo( stdout, "{}{}{}{}{}", chUnprovenAttr.c_str(), rw::crossKindAttrXml( chRows.crossKind ).c_str(), // H1's residue; hono-07's cross_kind= + rw::emitTo( stdout, "{}{}{}{}{}{}", chUnprovenAttr.c_str(), rw::crossKindAttrXml( chRows.crossKind ).c_str(), // H1's residue; hono-07's cross_kind= + rw::defsListedAttrXml( chAttr.groups, chListing ), // attrib-multidef: a cut table says so chTested.xmlAttr.c_str(), chDeclinedAttr.c_str(), chValueRefsAttr ); // then A6's partition, the declines, the value uses rw::emitTo( stdout, "{}{}{}>", pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ), rw::graphCountFloorAttrXml( g ).c_str(), chNextAttr.c_str() ); // P3 (L7): callers → the SITES (--uses=SELECTOR, spelling mirrored); callees → the body (--expand=SYM) rw::writeMultiRootTable( stdout, ing ); // M12: the roots list this element's own root= cannot carry + rw::emitRaw( stdout, rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ).c_str() ); // attrib-multidef: the groups the rows name, by ix= for( std::size_t i = pw.begin; i < pw.end; ++i ) { const Symbol& s = ing.symbols[ result[i] ]; 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, + 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 + viaNameAttr( chAttr.nameOnly( i ) ), // FE-B: a name-only edge says so on its row + rw::splitArmAttrXml( chAttr.splitArm( i ) ), rw::ofDefAttrXml( chAttr.groups, chAttr.tokens, i ), // attrib-multidef chSites.any ? rw::callSitesAttrXml( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ) : std::string() ); // CALLSITE-AT } - rw::emitTo( stdout, "{}{}", rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/false ), // the declined C++ standard-member calls, after the rows + rw::emitTo( stdout, "{}{}{}", rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/false ), // the declined C++ standard-member calls, after the rows + chDcRows, // and each declined call SITE () rw::valueRefsXml( ing, chRows.valueRefs, wantCallers, chVr, chVrNext ), tag ); return 0; } @@ -906,6 +947,10 @@ struct SafeDeleteLegendFlags bool hasSitesAt = false; // a row is on this page (CALLSITE-AT) bool sitesCut = false; // a row is on this page: its sites_at= list was cut at kCallSitesPerRowCap bool valueRefDepthCut = false; // the depth disclosure is on the root: the value walk stopped at its depth cap + bool defTable = false; // attrib-multidef: the table + of_def= rows (a multi-group selector) + bool defTableCut = false; // ... and the table lists only the groups this page's rows name (defs_listed=) + bool splitArms = false; // a row is on this page (a one-group selector's split arm, Q1) + rw::DeclinedSitesLegendFlags declinedSites{}; // lane declined-sites: what the rows on this answer carry (declinedsites.h) }; // count-floor (countfloor.h) + CALLSITE-AT: the safe-delete legend's clauses for callers_floor=, uses_floor=, next= and @@ -941,6 +986,7 @@ inline std::string safeDeleteFloorLegend( const SafeDeleteLegendFlags& flags, st { clause += "next= is that uses call. "; // runSafeDelete names it exactly then (sdNextAttr) } + clause += rw::defTableLegend( flags.defTable, flags.defTableCut ) + rw::splitArmLegend( flags.splitArms ); // attrib-multidef return clause; } @@ -956,12 +1002,15 @@ inline constexpr SafeDeleteRiskReading kSafeDeleteRiskReadings[] = { { "uses-exist", "uses-exist: callers or uses exist, and at least part of the radius is test-covered. " }, { "unmodelled", "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. " }, + // lane declined-sites (gates review Q3): what none-found read beside declined_calls= before the rows named the calls + { "possible-callers", "possible-callers: zero proven callers and zero uses, but declined_calls=K call sites (the rows) could have " + "meant this definition; not a safety reading. " }, }; inline const char* safeDeleteRiskSentence( std::string_view risk ) noexcept { 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" ); + ASSUME( it != end, "safeDeleteRiskSentence: runSafeDelete assigns only the five values listed" ); return it != end ? it->sentence : ""; } @@ -974,11 +1023,11 @@ inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs 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::declinedCallsLegendWithGates( declinedCalls > 0, flags.gateDeclined, flags.stdDeclined ); - if( declinedCalls > 0 && risk == "none-found" ) - { - declinedClause += "none-found beside declined_calls= is not a safety reading: those calls may reach this definition. "; - } + // Since lane declined-sites a definition some declined call could have meant never reads none-found: it reads + // possible-callers, whose own sentence says it is not a safety reading, and its rows name the calls. + ASSUME( declinedCalls == 0 || risk != "none-found", "runSafeDelete reads possible-callers (or unmodelled) wherever declined_calls= rides" ); + std::string declinedClause = rw::declinedCallsLegendForRows( declinedCalls > 0, flags.gateDeclined, flags.stdDeclined, flags.declinedSites.any ) + + rw::declinedSitesLegend( flags.declinedSites ); 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 declinedClause += safeDeleteFloorLegend( flags, risk ); // count-floor + CALLSITE-AT: present-only clauses @@ -1102,6 +1151,9 @@ std::optional runSafeDelete( const MainDispatch& d ) return sa.line != sb.line ? sa.line < sb.line : sa.name < sb.name; } ); + // attrib-multidef (defattrib.h): which definition each caller row belongs to — the callers verb's own computation. + const rw::OneHopAttribution sdAttr = rw::attributeOneHopAnswer( ing, g, defs, callerIds, /*towardMatches=*/true ); + // transitive blast radius: the --impact walk, verbatim. const std::vector reach = rw::transitiveCallers( g, defs ); @@ -1196,7 +1248,11 @@ std::optional runSafeDelete( const MainDispatch& d ) ENSURES( !usesFloor || sdFloor.isFloor ); // a NotCalls definition always floors callers= too (callerFloors' first rule) const bool anyEvidence = !callerIds.empty() || !sites.empty() || !sdValueRefs.rows.empty(); // a decorator row is not a use site, still evidence + // lane declined-sites: declined calls whose candidate lists hold a definition (as --callers counts and lists them) — the + // rows, file-first, cut at the callers verb's own cap (one constant, gates review Q6). + const auto [ sdDeclinedCalls, sdDeclinedSites ] = rw::declinedCallsAndSites( ing, g, defs, /*wantCallers=*/true ); const char* risk = ( !anyEvidence && usesFloor ) ? "unmodelled" + : !anyEvidence && sdDeclinedCalls > 0 ? "possible-callers" // gates review Q3: never none-found beside a declined possible caller : !anyEvidence ? "none-found" : ( !reach.empty() && radiusTested == 0 ) ? "untested-radius" : "uses-exist"; @@ -1219,7 +1275,8 @@ std::optional runSafeDelete( const MainDispatch& d ) // #60: exactly when an owner is one of the rows this page prints. pageWindow is pure, so recomputing // it here (the legend streams BEFORE the rows) cannot disagree with the window the rows below take. const PageWindow sdLw = pageWindow( callerIds.size(), effectiveRowCap( cfg.pageLimit, 40 ), cfg.pageOffset ); - const std::size_t sdDeclinedCalls = declinedCallsNaming( g, defs ); // declined calls that could have meant a def (as --callers counts them) + const rw::DeclinedSiteCut sdDcCut = rw::declinedSiteCut( sdDeclinedSites.size(), cfg.pageLimit, "--safe-delete=", cfg.safeDeleteSym, + std::size_t( effectiveRowCap( cfg.pageLimit, 40 ) ) ); // CALLSITE-AT: a row's p= is where the CALLER is defined; the lines to open are its call sites, as pasteable // file:line tokens (editcheck.h rowCallSites: the uses verb's call-role filters, one scan of the reference table, the @@ -1229,12 +1286,25 @@ std::optional runSafeDelete( const MainDispatch& d ) // Designated (train 27): two lanes each inserted a bool here at a different position; named fields keep two adjacent // bools from trading places silently in a positional aggregate. + std::vector sdNamed; + rw::defTableMarkNamed( sdNamed, sdAttr.groups, sdAttr.tokens, sdLw.begin, sdLw.end ); + const rw::DefTableListing sdListing = rw::defTableListing( sdAttr.groups, sdNamed ); + bool sdHasSplit = false; + for( std::size_t i = sdLw.begin; i < sdLw.end && !sdHasSplit; ++i ) + { + sdHasSplit = sdAttr.splitArm( i ); + } + const std::vector sdDc = + rw::declinedSiteViews( ing, g, std::span( sdDeclinedSites ).first( sdDcCut.shown ), /*callersForm=*/true, defs, sdAttr.groups, + sdVr, cfg.legend == "full" ); emitSafeDeleteLegend( defs.size(), sdUnprovenDefs, ambiguousCallers, risk, SafeDeleteLegendFlags{ .singleRoot = sdSingleRoot, .hasUnindexed = rw::graphGaugeClauses( g ), .hasModScope = anyModuleScopeRow( ing, std::span( callerIds ).subspan( sdLw.begin, sdLw.end - sdLw.begin ) ), .declinedCalls = sdDeclinedCalls, .gateDeclined = g.gateDeclinedCalls > 0, .stdDeclined = g.stdMemberDeclinedCalls > 0, .valueRefs = sdValueRefs.rows.size(), .callersFloor = sdFloor.isFloor, .usesFloor = usesFloor, - .hasSitesAt = sdSites.any, .sitesCut = sdSites.anyCut, .valueRefDepthCut = sdValueRefs.depthCut.files > 0 } ); + .hasSitesAt = sdSites.any, .sitesCut = sdSites.anyCut, .valueRefDepthCut = sdValueRefs.depthCut.files > 0, + .defTable = sdAttr.groups.multi(), .defTableCut = sdListing.cut, .splitArms = sdHasSplit, + .declinedSites = rw::declinedSitesLegendFlags( sdDc, /*callersForm=*/true, sdDcCut ) } ); const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified walks ascending id — defs[0] is the // lowest, same convention --impact/--uses/--callers's of=/defs= @@ -1253,15 +1323,17 @@ std::optional runSafeDelete( const MainDispatch& d ) : rw::nextFlag( "--uses=", cfg.safeDeleteSym ) ); rw::emitTo( stdout, "", + "dead_code_candidate=\"{}\" risk=\"{}\"{}{}{}{}{}{}{}{}>", ex( cfg.safeDeleteSym ).c_str(), symTag( lead.kind ), ex( sdPathRel( lead.fileId ) ).c_str(), lead.line, defs.size(), callerIds.size(), sdCallersFloorAttr, ambiguousCallers, reach.size(), sites.size(), sdUsesFloorAttr, testedSelf ? 1 : 0, radiusTested, radiusUntested, deadCodeCandidate ? 1 : 0, risk, rw::unprovenDefsAttrXml( sdUnprovenDefs ).c_str(), // H1: beside the verdict it qualifies; absent at zero - rw::declinedCallsAttrXml( sdDeclinedCalls ).c_str(), // beside risk= too: declined calls that may reach it; absent at zero + ( rw::declinedCallsAttrXml( sdDeclinedCalls ) + rw::declinedSiteCutAttrXml( sdDcCut ) ).c_str(), // beside risk= too: declined calls that may reach it (+ the cut); absent at zero ( rw::valueRefsCountAttrXml( sdValueRefs.rows.size() ) + rw::valueRefsDepthAttrXml( ing, sdValueRefs.depthCut, sdVr ) ), // value uses: in uses=, never in callers=/impact_reaches=; absent at zero pageDisclosure( cab, sizeof( cab ), cw.end - cw.begin, callerIds.size(), cw.end, cfg.pageLimit, cfg.pageOffset, true ), - rw::graphCountFloorAttrXml( g ).c_str(), sdRootAttr.c_str(), sdNextAttr.c_str() ); + rw::graphCountFloorAttrXml( g ).c_str(), sdRootAttr.c_str(), sdNextAttr.c_str(), + rw::defsListedAttrXml( sdAttr.groups, sdListing ) ); // attrib-multidef: a cut table says so + rw::emitRaw( stdout, rw::defTableXml( ing, sdAttr.groups, sdListing, sdSingleRoot ? std::string_view( sdRootPrefix ) : std::string_view() ).c_str() ); for( std::size_t i = cw.begin; i < cw.end; ++i ) { const NodeId callerId = callerIds[i]; @@ -1273,10 +1345,10 @@ std::optional runSafeDelete( const MainDispatch& d ) { rw::emitTo( stdout, " amb=\"{}\"", g.ambOut[ callerId ] ); // M15: the same COUNT a map row's amb= prints — one meaning, one unit } - rw::emitTo( stdout, "/>" ); + rw::emitTo( stdout, "{}{}/>", rw::splitArmAttrXml( sdAttr.splitArm( i ) ), rw::ofDefAttrXml( sdAttr.groups, sdAttr.tokens, i ) ); // attrib-multidef } - rw::emitTo( stdout, "{}", rw::valueRefsXml( ing, sdValueRefs, true, sdVr, - "--uses=" + std::string( cfg.safeDeleteSym ) ) ); + rw::emitTo( stdout, "{}{}", rw::declinedSitesXml( sdDc, /*callersForm=*/true ), // lane declined-sites: each declined possible caller, after the rows + rw::valueRefsXml( ing, sdValueRefs, true, sdVr, "--uses=" + std::string( cfg.safeDeleteSym ) ) ); return 0; } @@ -2098,9 +2170,9 @@ accumulateExternalSurface( const rw::IngestResult& ing, const rw::HashMap> ext; for( const Reference& r : ing.references ) { - if( r.isCompose || r.isDocLink ) + if( r.isCompose || r.isDocLink || r.role == RefRole::Subscript ) { - continue; // type edge / doc mention — not a code reference + continue; // type edge / doc mention / an element access (its operator is a builtin's unless graph.h proves the class) — not a code reference } if( r.lang == Lang::Markdown ) { @@ -2326,13 +2398,18 @@ 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{}; const bool pthHasVia = rw::pathHasNameOnlyHop( g, path ); // FE-B: exactly when a hop of THIS path is via="name" + // attrib-multidef (defattrib.h): from_p=/to_p= only for an endpoint the path bound; prov="split" on a split-arm hop + const rw::PathEndpointBinding pthBound = rw::pathEndpointBinding( g, rw::endpointDefinitionCount( ing, srcDefs ), rw::endpointDefinitionCount( ing, dstDefs ), path ); + const bool pthHasSplit = pthBound.splitHop; rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) ).c_str(), + "graph holds none. {}{}{}{}-->{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + + rw::splitArmLegend( pthHasSplit ) + rw::pathUnboundLegend( !pthBound.from || !pthBound.to ) ).c_str(), ( std::string( rw::toValueRefsLegend( pthToValueRefs.count > 0 ) ) + rw::valueRefsDepthLegend( pthToValueRefs.depthCut.files > 0 ) ).c_str(), rw::pathGapsLegend( pthGaps.any() ), rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( pthSingleRoot ) ); - rw::emitTo( stdout, " runPath( const MainDispatch& d ) 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 ] ); // 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, "", 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::splitArmAttrXml( rw::pathHopSplitArm( g, path, i ) ) ); } rw::emitTo( stdout, "" ); return 0; @@ -2483,8 +2560,21 @@ struct ImpactView 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" const rw::ImpactFileRollup& fileRollup; // idea #6: graph.h impactFileRollup over the FULL reach set + const rw::ReachAttribution& attrib; // attrib-multidef (defattrib.h): the definition groups + each reached row's ties + const rw::DefTableListing& defListing; // ... and the groups this page's table lists }; +// attrib-multidef: the window's of_def JSON keys in row order (indexed like `show`), "" where none +inline std::vector impactOfDefKeys( const ImpactView& v ) +{ + std::vector keys( v.show.size() ); + for( std::size_t i = v.page.begin; i < v.page.end && v.attrib.groups.multi(); ++i ) + { + keys[ i ] = v.attrib.ofDefJson( v.show[ i ] ); + } + return keys; +} + // 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 ) { @@ -2514,6 +2604,7 @@ int emitImpactColumnar( const ImpactView& v ) const std::string attr = "of=\"" + std::string( escapeXml( v.sym, esc ) ) + "\" defs=\"" + std::to_string( v.defs ) + "\" reaches=\"" + std::to_string( v.reaches ) + "\"" + rw::unprovenDefsAttrXml( v.unprovenDefs ) // H1: where the XML root carries it + + rw::defsListedAttrXml( v.attrib.groups, v.defListing ) // attrib-multidef: a cut table says so + rw::byDepthAttrXml( v.byDepth ) // 0.6.5: the XML root's, same place + " importers=\"" + std::to_string( v.imports.files.size() ) + "\"" + rw::importsUnresolvedAttrXml( v.imports.importsUnresolved ) // #220: the XML root's, absent at 0 @@ -2542,7 +2633,15 @@ int emitImpactColumnar( const ImpactView& v ) + rw::nextAttrXml( rw::nextFlag( "--safe-delete=", v.sym ) ); // P3 (L7): the XML root's next=, same set 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, rw::ColumnarRowExtras{ &rowVia, nullptr, nullptr } ); + std::vector rowOfDef; // attrib-multidef: of_def per row, only on a multi-group selector + for( std::size_t i = v.page.begin; i < v.page.end && v.attrib.groups.multi(); ++i ) + { + const std::size_t at = v.attrib.indexOf( v.show[ i ] ); + rowOfDef.push_back( at == SIZE_MAX ? std::string() : rw::ofDefColumnValue( v.attrib.groups, v.attrib.tokens, at ) ); + } + const std::string defRows = rw::defTableXml( v.ing, v.attrib.groups, v.defListing, v.rootPrefix ); + emitColumnarSymbolRows( stdout, v.ing, "impact", attr.c_str(), rows, v.rootPrefix, v.testReach, &v.depth, + rw::ColumnarRowExtras{ &rowVia, nullptr, nullptr, v.attrib.groups.multi() ? &rowOfDef : nullptr, &defRows, nullptr } ); return 0; } @@ -2558,8 +2657,9 @@ int emitImpactJson( const ImpactView& v ) using namespace rw; char ipab[ kPageDisclosureCap ]; const std::size_t shownRows = v.page.end - v.page.begin; - rw::emitTo( stdout, "{{\"of\":\"{}\",\"defs\":{},\"reaches\":{}{}{}", jsonStr( v.sym ).c_str(), v.defs, v.reaches, + rw::emitTo( stdout, "{{\"of\":\"{}\",\"defs\":{},\"reaches\":{}{}{}{}", jsonStr( v.sym ).c_str(), v.defs, v.reaches, rw::unprovenDefsKeyJson( v.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin + rw::defTableJson( v.ing, v.attrib.groups, v.defListing, v.rootPrefix, /*mcp=*/false ), // attrib-multidef: the table rw::byDepthKeyJson( v.byDepth ) ); // 0.6.5: the XML root's by_depth=, as an array rw::emitTo( stdout, ",\"importers\":{},\"shown_importers\":{},\"importers_capped\":{}", v.imports.files.size(), v.imports.shown, v.imports.capped ? "true" : "false" ); @@ -2578,8 +2678,9 @@ 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 - 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 ); + const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); // FE-B: the rows' hedge, per entry + const std::vector keys = impactOfDefKeys( v ); // attrib-multidef: each row's of_def + printJsonSymbolRows( v.ing, v.show, v.page.begin, v.page.end, v.rootPrefix, v.testReach, &v.depth, &via, &keys ); rw::emitTo( stdout, "],\"import_reach\":[" ); rw::emitImportRowsJson( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); rw::emitRaw( stdout, "]" ); @@ -2601,7 +2702,7 @@ int emitImpactXml( const ImpactView& v ) char ipab[ kPageDisclosureCap ]; const std::size_t shownRows = v.page.end - v.page.begin; rw::emitTo( stdout, "", - ex( v.sym ).c_str(), v.defs, v.reaches, rw::unprovenDefsAttrXml( v.unprovenDefs ).c_str(), // H1: beside the reaches= it qualifies + ex( v.sym ).c_str(), v.defs, v.reaches, rw::unprovenDefsAttrXml( v.unprovenDefs ) + rw::defsListedAttrXml( v.attrib.groups, v.defListing ), // H1: beside the reaches= it qualifies; attrib-multidef rw::byDepthAttrXml( v.byDepth ), // 0.6.5: partitions reaches= by hop depth v.imports.xmlAttrs.c_str(), v.radiusTested, v.radiusUntested, ( rw::declinedCallsAttrXml( v.declinedCalls ) + rw::declinedIfaceAttrXml( v.declinedIface ) ).c_str(), // tier-3 declines into the radius @@ -2611,6 +2712,7 @@ int emitImpactXml( const ImpactView& v ) v.pageLimit, v.pageOffset, true ), rw::graphCountFloorAttrXml( v.g ).c_str(), rw::renderDisclosure( v.prD, rw::DiscloseAs::XmlAttrs ).c_str(), rw::nextAttrXml( rw::nextFlag( "--safe-delete=", v.sym ) ).c_str() ); // P3 (L7): "is it safe to change" → "can it go" + rw::emitRaw( stdout, rw::defTableXml( v.ing, v.attrib.groups, v.defListing, v.rootPrefix ).c_str() ); // attrib-multidef: the groups, by ix= rw::emitImpactFileRollupXml( stdout, v.ing, v.fileRollup, v.rootPrefix ); // idea #6: before the symbol window for( std::size_t i = v.page.begin; i < v.page.end; ++i ) { @@ -2618,10 +2720,11 @@ 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\"" : "", - viaNameAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ) ) ); // FE-B (D3): reached only through a name-only edge + viaNameAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ) ), // FE-B (D3): reached only through a name-only edge + v.attrib.ofDefXml( v.show[i] ) ); // attrib-multidef } 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 }, @@ -2693,8 +2796,18 @@ std::optional runImpact( const MainDispatch& d ) const rw::PageWindow imPage = pageWindow( show.size(), effectiveRowCap( cfg.pageLimit, rw::kCallHierarchyRowCap ), cfg.pageOffset ); // idea #6: the rollup rides only beside a CUT symbol window (graph.h impactFileRollup) // fix27: on a multi-definition selector the rows keep the symbol rows' via="name" hedge through the fold - const rw::ImpactFileRollup imFiles = rw::impactFileRollup( ing, reach, imDepth, cfg.pageLimit, cfg.impactSym, - imPage.end - imPage.begin < show.size(), seeds.size() > 1 ? imProven : rw::kNoImpactHedge ); + rw::ImpactFileRollup imFiles = rw::impactFileRollup( ing, reach, imDepth, cfg.pageLimit, cfg.impactSym, + imPage.end - imPage.begin < show.size(), seeds.size() > 1 ? imProven : rw::kNoImpactHedge ); + // attrib-multidef (defattrib.h): which definition each reached row (and rollup file) is a dependant of, and the table + // this page lists — the groups its rows and rollup rows name, all of them up to the cap. + const rw::ReachAttribution imAttr = rw::attributeReachAnswer( ing, g, seeds, reach ); + std::vector imNamed; + rw::attachRollupOfDef( imFiles, ing, imAttr, imNamed ); + for( std::size_t i = imPage.begin; i < imPage.end && imAttr.groups.multi(); ++i ) + { + imAttr.markNamed( imNamed, show[ i ] ); + } + const rw::DefTableListing imListing = rw::defTableListing( imAttr.groups, imNamed ); // Reference-as-value round: SYM's own binding sites, beside the radius they are not in. const rw::ValueRefIndex imVri( ing ); const rw::ValueRefRows imValueRefs = rw::valueRefCallerRows( ing, imVri, seeds ); @@ -2712,17 +2825,20 @@ std::optional runImpact( const MainDispatch& d ) const bool imHasModScope = anyModuleScopeRow( ing, std::span( show ).subspan( imPage.begin, imPage.end - imPage.begin ) ); 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, + ( std::string( cfg.columnar ? rw::kImpactImportTierColumnarLegend : rw::kImpactImportTierLegend ) + + rw::importersNextLegend( !imports.next.empty() && !cfg.columnar ) ).c_str(), // cut-fix E: exactly when the root carries it ( 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= - rw::declinedCallsLegendWithGates( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + rw::declinedCallsLegendWithGates( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ) + rw::declinedSitesImpactPointer( imDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( imDeclinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::modScopeLegend( imHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is - + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ) ) ).c_str(), // idea #6; fix27 + + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ), + !imports.next.empty() && !cfg.columnar ) // idea #6; fix27; FIX1 R1: the XML root's importers_next= (columnar names it in lens=) + + rw::defTableLegend( imAttr.groups.multi(), imListing.cut ) ).c_str(), // attrib-multidef: exactly when the table rides ( std::string( rw::valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs= + rw::valueRefsDepthLegendFor( imValueRefs ) ).c_str(), // ... and the depth disclosure rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str() ); @@ -2736,7 +2852,7 @@ std::optional runImpact( const MainDispatch& d ) 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, - imProven, imFiles }; + imProven, imFiles, imAttr, imListing }; if( cfg.columnar ) { return emitImpactColumnar( view ); } if( cfg.json ) { return emitImpactJson( view ); } @@ -2912,7 +3028,7 @@ std::optional runAround( const MainDispatch& d ) aroundAnn.seed = { ing.symbols[ focus ].name, cfg.aroundDepth, cfg.aroundFanout, definitionCountOfName( ing, focus ), eg.fanoutCut, eg.depthTruncated }; aroundAnn.seed.unprovenDefs = arUnprovenDefs; // H1: unproven_defs= beside defs= on the root, its clause in the map legend - serialize( stdout, ing, rank, g.outOff, g.outTargets, int( eg.nodes.size() ), cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, false, g.outProv.empty() ? nullptr : &g.outProv, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, aroundCompose.tokens + aroundRoutes.tokens + wrap.tokens, aroundAnn, /*statsFirstScreen=*/false, aroundRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, /*gateDeclinedCalls=*/0, g.outNameOnly.empty() ? nullptr : &g.outNameOnly ); + serialize( stdout, ing, rank, g.outOff, g.outTargets, int( eg.nodes.size() ), cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, false, g.outProv.empty() ? nullptr : &g.outProv, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, aroundCompose.tokens + aroundRoutes.tokens + wrap.tokens, aroundAnn, /*statsFirstScreen=*/false, aroundRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, /*gateDeclinedCalls=*/0, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, /*stdMemberDeclinedCalls=*/0, g.inertDeclinedCalls ); if( !g.composeEdges.empty() ) { diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 7a86e8bbf..594f10084 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -70,6 +70,16 @@ std::string noBaselineFatalMessage( const std::string& baselineFile, const rw::q // `sameRef` is the A==B case. It is a legal question with an empty answer, and it is answered by materializing // ONE tree and comparing it with itself rather than by a special-cased early return: the empty delta then // falls out of the ordinary machinery instead of being asserted by a branch nobody re-tests. +// qd-memory-degrade: a memory-guard stop in one phase of the range form. Recorded, never printed where it happens: the +// form first tries the changed files, and only if those do not fit either does it say anything (refPairStopRefusal). +struct PhaseStop +{ + bool stopped = false; + bool hard = false; // the hard line (the limit itself); else a soft stop (soft below) + std::string where; // "ingest of the base commit", "graph of the target commit", … + rw::MemoryStop soft; // a soft stop's cause, copied off the cut ingest +}; + struct RefPairDelta { rw::quality::TmpTreeGuard targetGuard; // declared BEFORE the tree so teardown outlives its use @@ -82,72 +92,119 @@ struct RefPairDelta std::string rangeSpan; // R1: ".." — the range the rename // record is read over, against the REAL repo (both trees // here are temp dirs with no history of their own) + // qd-memory-degrade — which kinds this answer runs, and, when the full trees did not fit, what it answers from instead + rw::quality::KindMask selected = rw::quality::kAllKinds; // --quality-kinds (every kind when absent) + rw::quality::KindMask kinds = rw::quality::kAllKinds; // the kinds actually run: selected, less what the changed files cannot decide + bool degraded = false; // answered from both commits' CHANGED files (ingest_scope="changed-files") + std::string memoryStop; // after a stop: "crawl" | "parse" | "hard"; empty when the projection decided + bool memoryPressure = false; // the stop was the OS pressure signal + std::string stopWhere; // the stop's phase, as its sentence names it + std::uint64_t needBytes = 0; // the projected need (the projection decided and measured it) + std::uint64_t ingestNeedBytes = 0; // the projection's ingest need (judged against half the limit) + std::vector baseMacros; // the base commit's whole C-family macro-name set, when its full tree was read + bool haveBaseMacros = false; + std::vector targetFiles; // the target commit's files, for --scope's test (degraded + scoped only) + std::string baseSha, targetSha; + PhaseStop stop; // the guard's stop in a full tree, when there was one + std::uint64_t ingestPeak = 0; // this process's peak at the end of the base commit's ingest (the projection's input) const rw::quality::RefTree& target() const noexcept { return targetTree; } + const std::vector* macroOverride() const noexcept { return degraded && haveBaseMacros ? &baseMacros : nullptr; } + const std::vector* scopeFiles() const noexcept { return degraded ? &targetFiles : nullptr; } }; -// #387: what loadRefPairDelta prints and returns when one commit's tree could not be scored. A memory-guard stop is named -// as one — exit 5 and the guard's own line — because "could not materialize or parse" would send the reader looking for -// a broken commit; anything else is the environment, exit 1, with the sha named. -int refPairLoadRefusal( rw::quality::RefTreeLoad load, const rw::quality::RefTree& tree, const std::string& sha, std::string_view side ) +// #387: what loadRefPairDelta prints and returns when one commit's tree could not be materialized at all — the environment, +// exit 1, with the sha named. A memory-guard stop is not this: it is a PhaseStop, and the range form falls to the changed +// files (qd-memory-degrade). +int refPairUnavailable( const std::string& sha ) { - using namespace rw; - if( load == quality::RefTreeLoad::OverHardLimit ) + rw::emitTo( stderr, "ripwire: --quality-delta: could not materialize or parse the tree at {}\n", sha.c_str() ); + return 1; +} + +// The load outcome of one tree → a PhaseStop, or an exit code for the environment. Loaded = nothing to record. +std::optional recordLoad( rw::quality::RefTreeLoad load, const rw::quality::RefTree& tree, const std::string& sha, std::string_view side, PhaseStop& stop ) +{ + using rw::quality::RefTreeLoad; + if( load == RefTreeLoad::Loaded ) { - memguard::answerStops(); - DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: over the memory guard's hard limit after a commit's ingest, before its graph — exit 5, one stderr line" ); - rw::emitTo( stderr, "ripwire: {}\n", memguard::hardStopLine( "ingest of the " + std::string( side ) + " commit" ) ); - return 5; + return std::nullopt; } - if( load == quality::RefTreeLoad::MemoryStopped ) + if( load == RefTreeLoad::Unavailable ) { - memguard::answerStops(); // this refusal answers for the stop the commit's ingest recorded - DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: the memory guard cut a commit's ingest, so nothing is scored — exit 5, one stderr line" ); - rw::emitTo( stderr, "ripwire: {}\n", memguard::verbRefusalLine( tree.ing.memoryStop, "--quality-delta=A..B" ) ); - return 5; + return refPairUnavailable( sha ); } - rw::emitTo( stderr, "ripwire: --quality-delta: could not materialize or parse the tree at {}\n", sha.c_str() ); - return 1; + stop.stopped = true; + stop.hard = load == RefTreeLoad::OverHardLimit; + stop.where = "ingest of the " + std::string( side ); + stop.soft = tree.ing.memoryStop; + return std::nullopt; } // #387: the hard line between the range form's phases. main() checks it after the working-tree ingest and graph build, -// which this form now dispatches before, so these are its only readings, in order: after each commit's ingest -// (loadRefTree, before the graph is built) and after its graph; for the base, after its snapshot and again once its index -// is released. Over the line nothing answers: exit 5 and one line naming the phase. -bool refPairOverHardLimit( std::string_view phase ) +// which this form dispatches before, so these are its only readings, in order: after each commit's ingest (loadRefTree, +// before the graph is built) and after its graph; for the base, after its snapshot and again once its index is +// released. Over the line the phase is RECORDED as a stop and nothing is printed (qd-memory-degrade: the changed files +// answer next, and refPairStopRefusal speaks only if they cannot) — where #387's refPairOverHardLimit refused on the spot. +bool hardLineStops( std::string_view phase, PhaseStop& stop ) { - using namespace rw; - if( !memguard::overHardLimit() ) + if( !rw::memguard::overHardLimit() ) { return false; } - memguard::answerStops(); - DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: over the memory guard's hard limit between its phases — exit 5, one stderr line" ); - rw::emitTo( stderr, "ripwire: {}\n", memguard::hardStopLine( phase ) ); + stop.stopped = true; + stop.hard = true; + stop.where = std::string( phase ); return true; } +// The one line and exit 5 when the range form cannot answer at all: the stop (or the projection) that sent it to the +// changed files, and why those did not answer either. Every stop recorded so far is answered for by this refusal. +int refPairStopRefusal( const PhaseStop& stop, const RefPairDelta& refs, std::string_view why ) +{ + using namespace rw; + memguard::answerStops(); + DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: the full trees did not fit and the changed files did not answer either — exit 5, one stderr line" ); + if( stop.stopped && stop.hard ) + { + rw::emitTo( stderr, "ripwire: {}\n", memguard::hardStopLine( stop.where, std::string( why ) + "; " ) ); + } + else if( stop.stopped ) + { + rw::emitTo( stderr, "ripwire: the memory guard stopped the {} {} in the {}; --quality-delta=A..B cannot answer from a partial index, and {} — {}\n", + memguard::phaseName( stop.soft.phase ), memguard::stopCause( stop.soft ), stop.where, why, memguard::kOverride ); + } + else + { + rw::emitTo( stderr, "ripwire: memory limit {} ({}): the target phase of --quality-delta=A..B was projected to need ~{}; {}; stopped cleanly without an answer — {}\n", + memguard::limitSpelling( memguard::limits().hardBytes ), memguard::limitSourceClause(), memguard::limitSpelling( refs.needBytes ), why, memguard::kOverride ); + } + return 5; +} + // #387: the base commit lives only in this function. Its snapshot is everything the comparison needs from it, so its -// index, graph and temp dir are released on return, before the target tree is materialized. Exit code on a refusal. -std::optional scoreBaseCommit( const std::string& root, const std::string& sha, const rw::Config& cfg, rw::quality::Snapshot& out ) +// index, graph and temp dir are released on return, before the target tree is materialized. qd-memory-degrade: it also +// keeps the base tree's C-family macro NAMES (defect-shape's one tree-wide input, names only) and reads this process's +// peak at the end of the ingest, for the projection. Exit code on an environment failure; a memory stop goes to `stop`. +std::optional scoreBaseCommit( const std::string& root, const std::string& sha, const rw::Config& cfg, RefPairDelta& refs ) { using namespace rw; quality::TmpTreeGuard baseGuard; quality::RefTree baseTree; const quality::RefTreeLoad load = quality::loadRefTree( root, sha, cfg.excludes, cfg.maxFileBytes, "qdpair-base", baseGuard, baseTree ); - if( load != quality::RefTreeLoad::Loaded ) + refs.ingestPeak = os::mem_footprint_peak(); + if( const std::optional failed = recordLoad( load, baseTree, sha, "base commit", refs.stop ) ) { - return refPairLoadRefusal( load, baseTree, sha, "base" ); + return failed; } - if( refPairOverHardLimit( "graph of the base commit" ) ) + if( refs.stop.stopped || hardLineStops( "graph of the base commit", refs.stop ) ) { - return 5; - } - out = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root ); - if( refPairOverHardLimit( "snapshot of the base commit" ) ) - { - return 5; + return std::nullopt; } + refs.baseSnapshot = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root, nullptr, { refs.selected } ); + refs.baseMacros = quality::cFamilyMacroNames( baseTree.ing ); + refs.haveBaseMacros = true; + (void)hardLineStops( "snapshot of the base commit", refs.stop ); return std::nullopt; } @@ -156,29 +213,202 @@ std::optional loadTargetCommit( const std::string& root, const std::string& { using namespace rw; const quality::RefTreeLoad load = quality::loadRefTree( root, sha, cfg.excludes, cfg.maxFileBytes, "qdpair-target", out.targetGuard, out.targetTree ); - if( load != quality::RefTreeLoad::Loaded ) + if( const std::optional failed = recordLoad( load, out.targetTree, sha, "target commit", out.stop ) ) { - return refPairLoadRefusal( load, out.targetTree, sha, "target" ); + return failed; } - if( refPairOverHardLimit( "graph of the target commit" ) ) + if( out.stop.stopped || hardLineStops( "graph of the target commit", out.stop ) ) { - return 5; + return std::nullopt; } if( out.sameRef ) { - out.baseSnapshot = quality::computeSnapshot( out.targetTree.ing, out.targetTree.g, out.targetTree.root ); - if( refPairOverHardLimit( "snapshot of the base commit" ) ) + out.baseSnapshot = quality::computeSnapshot( out.targetTree.ing, out.targetTree.g, out.targetTree.root, nullptr, { out.selected } ); + (void)hardLineStops( "snapshot of the base commit", out.stop ); + } + return std::nullopt; +} + +// review C1: the degraded line's last clause — the kinds memory skipped, or, when it skipped none (a selector of kinds +// the changed files decide), that every selected kind ran: never an empty "skipped" list +std::string memorySkippedClause( rw::quality::KindMask memorySkipped ) +{ + return memorySkipped != 0 ? "; skipped " + rw::quality::kindList( memorySkipped, ", " ) : std::string( "; every selected kind ran" ); +} + +// qd-memory-degrade — the stderr line of a degraded answer: the limit, why the full trees were not read, the kinds it +// skipped, the replay. One line, beside the gating line, so a caller reading only $? and stderr knows the answer is partial. +std::string degradedStderrLine( const RefPairDelta& refs, rw::quality::KindMask memorySkipped, std::string_view kindsNext ) +{ + using namespace rw; + const std::uint64_t limit = memguard::limits().hardBytes; + std::string why = refs.memoryStop.empty() + ? "the target phase was projected to need ~" + memguard::limitSpelling( refs.needBytes ) + : refs.memoryStop == "hard" ? "the memory limit was reached during the " + refs.stopWhere + "; the full need was not measured" + : "the memory guard stopped the " + refs.stopWhere + ( refs.memoryPressure ? " under critical system memory pressure" : "" ) + "; the full need was not measured"; + std::string line = "memory limit " + ( limit == 0 ? std::string( "unknown" ) : memguard::limitSpelling( limit ) ) + " (" + std::string( memguard::limitSourceClause() ) + + "): --quality-delta=A..B answered from the changed files only (" + why + ")" + memorySkippedClause( memorySkipped ); + if( !kindsNext.empty() ) + { + line += " — run them with " + std::string( kindsNext ); + if( refs.memoryStop.empty() ) + { + const std::uint64_t machine = memguard::machineMemoryBytes(); + if( machine != 0 && refs.needBytes >= machine ) + { + line += " (more than this machine's " + memguard::limitSpelling( machine ) + ": recoverable only on a larger machine)"; + } + } + else { - return 5; + line += " (its --max-memory is a guess, twice the limit: no need was measured)"; } } + return line; +} + +// qd-memory-degrade — THE CHANGED-FILES ANSWER. The full trees are gone (released, or never read); both commits' changed +// files are ingested alone (never through a cache) and the base snapshot is taken from the base's. A kind runs here only +// if its rows are decided by those files: the LOCAL kinds always, defect-shape when the changed files define the same +// C-family macro names on both sides (then the tree's set is unchanged) and either the base's whole set was read or no +// changed file is C++ (the only scanner that reads it). Exit code on a refusal; nullopt when `out` is ready. +std::optional loadChangedFiles( const std::string& root, const rw::Config& cfg, RefPairDelta& out, const PhaseStop& stop ) +{ + using namespace rw; + if( cfg.qualityAck ) + { + return refPairStopRefusal( stop, out, "--quality-ack needs every kind's rows, so nothing was acked (the ledger is untouched)" ); + } + out.kinds = out.selected & static_cast( ~quality::kWholeTreeKinds ); + if( out.kinds == 0 ) + { + return refPairStopRefusal( stop, out, "the selected kinds are whole-tree kinds, which the changed files cannot decide" ); + } + if( memguard::overHardLimit() ) + { + // the footprint still held after the full tree was released (on macOS the allocator keeps its pages): nothing of + // the changed files was ingested, so the line says they were not tried — never that they did not fit + return refPairStopRefusal( stop, out, "this process still held more than the limit after releasing the full tree, so the changed files were not tried" ); + } + quality::ChangedFiles changed = quality::changedFilesBetween( root, out.baseSha, out.targetSha, !cfg.qualityScope.empty() ); + if( !changed.ok ) + { + memguard::answerStops(); + rw::emitTo( stderr, "ripwire: --quality-delta={}..{}: the full trees did not fit the memory limit, and git could not list the changed files to answer from\n", + out.baseSha.c_str(), out.targetSha.c_str() ); + return 1; + } + if( !changed.refusal.empty() ) + { + return refPairStopRefusal( stop, out, "the changed files cannot stand in for the trees: " + changed.refusal ); + } + PhaseStop subsetStop; + quality::TmpTreeGuard baseGuard; + quality::RefTree baseTree; + quality::RefTreeLoad load = quality::loadRefSubset( root, out.baseSha, changed.base, cfg.excludes, cfg.maxFileBytes, "qdpair-base-changed", baseGuard, baseTree ); + if( const std::optional failed = recordLoad( load, baseTree, out.baseSha, "changed files of the base commit", subsetStop ) ) + { + return failed; + } + load = subsetStop.stopped ? quality::RefTreeLoad::Loaded + : quality::loadRefSubset( root, out.targetSha, changed.target, cfg.excludes, cfg.maxFileBytes, "qdpair-target-changed", out.targetGuard, out.targetTree ); + if( const std::optional failed = recordLoad( load, out.targetTree, out.targetSha, "changed files of the target commit", subsetStop ) ) + { + return failed; + } + if( subsetStop.stopped ) + { + return refPairStopRefusal( subsetStop, out, "the changed files alone were cut too" ); + } + // defect-shape's one tree-wide input (see the header comment of this function) + const std::vector baseChanged = quality::cFamilyMacroNames( baseTree.ing ); + const std::vector targetChanged = quality::cFamilyMacroNames( out.targetTree.ing ); + const auto anyCpp = []( const IngestResult& ing ) { return std::any_of( ing.files.begin(), ing.files.end(), []( const std::string& f ) { return langOfPath( f ) == Lang::Cpp; } ); }; + if( baseChanged != targetChanged || ( !out.haveBaseMacros && ( anyCpp( baseTree.ing ) || anyCpp( out.targetTree.ing ) ) ) ) + { + out.kinds &= static_cast( ~quality::kindBit( "defect-shape" ) ); + } + if( out.kinds == 0 ) // the selection was defect-shape (with whole-tree kinds) and the changed files cannot decide it: never an empty pass + { + return refPairStopRefusal( stop, out, "the selected kinds need the whole tree (defect-shape too: the changed files' C-family macro names differ, or a C++ file changed without the base's whole set)" ); + } + out.degraded = true; + out.baseSnapshot = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root, nullptr, { out.kinds, out.macroOverride() } ); + if( !cfg.qualityScope.empty() ) + { + out.targetFiles = std::move( changed.targetTree ); + } + out.memoryStop = !stop.stopped ? std::string() : stop.hard ? std::string( "hard" ) : std::string( memguard::phaseName( stop.soft.phase ) ); + out.memoryPressure = stop.stopped && !stop.hard && stop.soft.byPressure; + out.stopWhere = stop.stopped ? ( stop.hard ? stop.where : std::string( memguard::phaseName( stop.soft.phase ) ) + " of the " + stop.where.substr( stop.where.find( "the " ) + 4 ) ) : std::string(); + memguard::answerStops(); // the degraded answer answers for the stop that sent it here return std::nullopt; } +// qd-memory-degrade: the two full trees, as #387 reads them — the base scored and released, then the target — with the +// projection between them. A stop in either is recorded in out.stop; `projectedNoFit` says the projection chose the +// changed files before the target was read. The projection is asked only when the changed files could answer a selected +// kind for certain: a selection of whole-tree kinds (with or without defect-shape, which the changed files may not +// decide) gains nothing from pre-empting the full attempt — that is the replay kinds_next= names, and it runs in full, +// refused only by a real stop. Exit code on an environment failure only. +std::optional loadFullTrees( const std::string& root, const rw::Config& cfg, RefPairDelta& out, bool& projectedNoFit ) +{ + using namespace rw; + // --quality-ack needs every kind's rows, which the changed files never give: like a whole-tree selection it is never + // pre-empted by the projection, and only a real stop refuses it (review C2: base acked within the cap, head refused) + const quality::KindMask certainLocal = cfg.qualityAck ? quality::KindMask( 0 ) + : static_cast( out.selected & ~( quality::kWholeTreeKinds | quality::kindBit( "defect-shape" ) ) ); + if( !out.sameRef ) + { + const std::uint64_t before = os::mem_footprint(); + if( const std::optional failed = scoreBaseCommit( root, out.baseSha, cfg, out ) ) + { + return failed; + } + // #387: the base tree's index, graph and temp dir were released when scoreBaseCommit returned. + if( out.stop.stopped || hardLineStops( "base commit's snapshot, with its index released", out.stop ) || certainLocal == 0 ) + { + return out.stop.stopped ? std::nullopt : loadTargetCommit( root, out.targetSha, cfg, out ); + } + const std::uint64_t basePeak = os::mem_footprint_peak(); + const std::uint64_t after = memguard::measuredFootprint(); + const memguard::TargetProjection proj = memguard::projectTargetPhase( before, out.ingestPeak, basePeak, after ); + out.needBytes = proj.needBytes; + out.ingestNeedBytes = proj.ingestNeedBytes; + projectedNoFit = !proj.fits; + if( std::getenv( "RIPWIRE_STATS" ) ) // the projection's own readings, the calibration's instrument (kProjectionScalePct) + { + rw::emitTo( stderr, "ripwire: memory-projection before={} ingest_peak={} base_peak={} after={} need={} measured={} fits={}\n", + memguard::limitSpelling( before ), memguard::limitSpelling( out.ingestPeak ), memguard::limitSpelling( basePeak ), + memguard::limitSpelling( after ), memguard::limitSpelling( proj.needBytes ), proj.measured ? 1 : 0, proj.fits ? 1 : 0 ); + } + if( projectedNoFit ) + { + return std::nullopt; + } + } + return loadTargetCommit( root, out.targetSha, cfg, out ); +} + +// A cut target is never scored: its index, graph and temp dir go before the changed files are read, and so does the full +// base snapshot (the changed files' own base snapshot replaces it). +void releaseFullTrees( RefPairDelta& out ) +{ + out.targetTree = {}; + out.baseSnapshot = {}; + rw::quality::TmpTreeGuard dropped; + std::swap( dropped.p, out.targetGuard.p ); // its destructor removes the cut tree's temp dir now +} + // Resolve the spec and materialize both sides. Returns an EXIT CODE on any refusal or environment failure // (already reported on stderr), or nullopt when `out` is ready to compare. Splitting user error from // environment failure is the same line --dmm's handler draws: a revision that does not resolve is a typo the // caller can fix, everything else is the machine. +// +// qd-memory-degrade — THE LADDER. Full trees first, exactly as before, so a run that fits is the run it always was. A +// memory-guard stop in either commit's full tree (a soft crawl/parse stop or the hard line), or a projection after the +// base phase that says the target phase will not fit, releases what was loaded and answers from both commits' changed +// files instead (loadChangedFiles); exit 5 only when those do not fit or cannot stand in for the trees. std::optional loadRefPairDelta( const std::string& root, std::string_view spec, const rw::Config& cfg, RefPairDelta& out ) { using namespace rw; @@ -219,25 +449,25 @@ std::optional loadRefPairDelta( const std::string& root, std::string_view s rw::emitRaw( stderr, "ripwire: --quality-delta needs TWO commits (A..B); use the bare --quality-delta for the working tree\n" ); return 1; } + out.baseSha = ref.baseSha; + out.targetSha = ref.targetSha; + out.kinds = out.selected; // DISTINCT tags — see quality::loadRefTree's header. The base tree is gone before the target is extracted, but // a shared tag would still let one side's extraction delete the other's directory if that order ever changed. out.sameRef = ( ref.baseSha == ref.targetSha ); - if( !out.sameRef ) + bool projectedNoFit = false; + if( const std::optional failed = loadFullTrees( root, cfg, out, projectedNoFit ) ) + { + return failed; + } + if( out.stop.stopped || projectedNoFit ) { - if( const std::optional refused = scoreBaseCommit( root, ref.baseSha, cfg, out.baseSnapshot ) ) + releaseFullTrees( out ); + if( const std::optional refused = loadChangedFiles( root, cfg, out, out.stop ) ) { return refused; } - // #387: the base tree's index, graph and temp dir were released when scoreBaseCommit returned. - if( refPairOverHardLimit( "base commit's snapshot, with its index released" ) ) - { - return 5; - } - } - if( const std::optional refused = loadTargetCommit( root, ref.targetSha, cfg, out ) ) - { - return refused; } // Both values are bare 40-char object names straight out of `rev-parse --verify` (resolveRefSpec accepts @@ -280,6 +510,7 @@ struct DeltaBasis std::size_t valueRefExcluded = 0; // dead-code exemption: held as a VALUE (valuerefindex.h) std::size_t apiNewSurface = 0; // Q-DIAL-4: new PUBLIC symbols this change added — the count that replaced one never-gating row each std::size_t acksBadLines = 0; // 2026-09-06: .ripwire_quality_acks lines skipped as unparseable (disclosed on the root) + bool ackHealingSkipped = false; // qd-memory-degrade: a changed-files answer with a non-empty ledger (also_skipped ack-healing) // #228: WHICH basis produced baseSel.snapshot when the marker is one of the git-HEAD family, and WHY when // the fast one was refused for a reason the caller can act on. nullptr = the attribute is absent, which is // the ordinary archived comparison every git-HEAD marker has always meant. See quality::HeadBasis. @@ -291,6 +522,33 @@ struct DeltaBasis std::vector cloneIdioms; }; +// The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the floor, tree B the judged +// tree, the ledger healed against it, the delta taken. +void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +{ + using namespace rw; + out.baseSel.snapshot = std::move( refs.baseSnapshot ); + out.baseSel.marker = "ref-pair"; + out.deltaRoot = refs.target().root; + // R1 IDENTITY. The SPAN is what makes this honest here: both trees are materialized OUT of the repo + // into temp dirs, so asking them about renames answers "not a git repo" — the renames are recorded in + // the REAL repo, over exactly the range this comparison is about. `root` (the repo) supplies the + // record; `refs.rangeSpan` scopes it to the pair. Everything else is identical to the working-tree + // form, which is the point of having one healIdentity. + // qd-memory-degrade: over the changed files the two tree-wide rescues are skipped (healIdentity's header) — said + // as also_skipped ack-healing when the ledger holds any row they could have re-filed. The clone-idiom export is + // asked for only where it is read (the --quality-ack backfill), so a kind selection without the clone kinds skips + // the clone passes. + out.acks = quality::readAckRecords( quality::acksPath( root ) ); + out.ackHealingSkipped = refs.degraded && !out.acks.empty(); + out.healing = quality::healIdentity( out.baseSel.snapshot, out.acks, refs.target().ing, refs.target().g, + out.deltaRoot, root, cfg.qualityAck, refs.rangeSpan, /*treeWideRescues=*/!refs.degraded ); + out.regs = quality::computeDelta( refs.target().ing, refs.target().g, out.baseSel.snapshot, + out.deltaRoot, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded, &out.apiNewSurface, + cfg.qualityAck ? &out.cloneIdioms : nullptr, &out.declinedCallExcluded, &out.valueRefExcluded, + quality::KindScope{ refs.kinds, refs.macroOverride() } ); +} + // Returns an EXIT CODE when there is nothing to compare against (already reported), nullopt when `out` holds // a usable comparison. `refs` is the CALLER's because it owns the materialized trees' teardown, and those // trees must outlive every read of `out.regs`. @@ -312,20 +570,7 @@ std::optional resolveDeltaBasis( const MainDispatch& d, const std::string& // sidecar's whole contract is "pinned at the current HEAD", which says nothing about a pair of // arbitrary commits — and the bare form's self-heal deleting a user's pinned floor as a side effect // of a wave-level measurement would be a side effect nobody asked for. - out.baseSel.snapshot = std::move( refs.baseSnapshot ); - out.baseSel.marker = "ref-pair"; - out.deltaRoot = refs.target().root; - // R1 IDENTITY. The SPAN is what makes this honest here: both trees are materialized OUT of the repo - // into temp dirs, so asking them about renames answers "not a git repo" — the renames are recorded in - // the REAL repo, over exactly the range this comparison is about. `root` (the repo) supplies the - // record; `refs.rangeSpan` scopes it to the pair. Everything else is identical to the working-tree - // form, which is the point of having one healIdentity. - out.acks = quality::readAckRecords( quality::acksPath( root ) ); - out.healing = quality::healIdentity( out.baseSel.snapshot, out.acks, refs.target().ing, refs.target().g, - out.deltaRoot, root, cfg.qualityAck, refs.rangeSpan ); - out.regs = quality::computeDelta( refs.target().ing, refs.target().g, out.baseSel.snapshot, - out.deltaRoot, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded, &out.apiNewSurface, - &out.cloneIdioms, &out.declinedCallExcluded, &out.valueRefExcluded ); + refPairBasis( cfg, root, refs, out ); return std::nullopt; } @@ -472,8 +717,11 @@ std::optional expandScopeDiff( rw::quality::Scope& scope, bool refPair, con // that includes only ingest.h. Returns an exit code (already reported on stderr) or nullopt when the scope // is usable. `judged` is the tree the findings were taken from — the working tree, or tree B in the // ref-pair form — because "names nothing indexed" has to be asked of the corpus that was actually measured. +// `commitFiles` (qd-memory-degrade): a changed-files answer indexed only the changed files, so a scope naming an UNCHANGED +// file is not a typo there — the test reads the target commit's whole file list instead (its root-relative paths). std::optional refuseUnusableScope( const rw::Config& cfg, const rw::quality::Scope& scope, - const rw::IngestResult& judged, std::string_view deltaRoot ) + const rw::IngestResult& judged, std::string_view deltaRoot, + const std::vector* commitFiles = nullptr ) { if( !scope.active() ) { @@ -499,6 +747,10 @@ std::optional refuseUnusableScope( const rw::Config& cfg, const rw::quality return std::nullopt; } } + if( commitFiles != nullptr && std::any_of( commitFiles->begin(), commitFiles->end(), [ & ]( const std::string& p ) { return scope.matchesPath( p ); } ) ) + { + return std::nullopt; + } rw::emitTo( stderr, "ripwire: --scope={} matches no indexed path — an exit 0 under a scope that owns nothing is a failure, not a clean tree\n" " (patterns are matched ROOT-RELATIVE, the same spelling p= prints: --scope=src or --scope=src/quality.h, not an absolute path)\n", std::string_view( cfg.qualityScope.data(), cfg.qualityScope.size() ) ); return 1; @@ -920,8 +1172,45 @@ struct QualityDeltaLegendParts bool anyDeclinedCallExcluded = false; // declined-call-excluded= is on the root bool anyValueRefExcluded = false; // value-ref-excluded= is on the root bool staleCollapsed = false; // idea #11: stale_by_kind=/stale_next= are on the root + bool kindsSkipped = false; // qd-memory-degrade: kinds_skipped=/skipped_why= are on the root + bool degraded = false; // ... ingest_scope= memory_limit= memory_need= also_skipped= kinds_next= + bool memoryStopped = false; // ... memory_stop= (and memory_pressure= when it was pressure) + bool memoryPeak = false; // ... memory_peak= (any range answer whose peak went over the limit) + bool memorySkippedShare = false; // ... memory_skipped= (skipped_why="selector,memory") }; +// qd-memory-degrade — present-only: each rides only with the attributes it defines. No double dash anywhere in them +// (they sit inside the legend comment): a flag is named by its words, never by its spelling. +inline constexpr const char* kQdKindsSkippedLegend = + "kinds_skipped= names the kinds this answer did NOT run (alphabetical): no row of them is in this document, which is not a " + "statement that none exists, and regressions=, gating= and every count cover only the kinds that ran; a count only a " + "skipped kind feeds (register-macro-excluded=) is absent, never 0. skipped_why=\"selector\": the caller chose the kinds " + "(the kinds selector), and stale acks were judged for the chosen kinds only; skipped_why=\"memory\": see ingest_scope=. "; +inline constexpr const char* kQdMemorySkippedLegend = + "skipped_why=\"selector,memory\": both; memory_skipped= names memory's share (what kinds_next= runs), the rest the caller left out. "; +inline constexpr const char* kQdDegradedLegend = + "ingest_scope=\"changed-files\": the two full trees did not fit the memory guard's limit (memory_limit=, the max-memory value " + "that sets it), so both commits' CHANGED files were ingested alone and the kinds those files decide ran with the same code and " + "bars as a full answer: their rows are the full answer's rows of those kinds, and the skipped kinds can only ADD rows. " + "memory_need= is the full answer's projected need, from the base commit's measured peak (a run within that margin of the " + "limit may be answered this way though it would have fit), or \"unmeasured\" after a stop. also_skipped= names the " + "whole-answer facts not computed because they read the whole target tree: config-warnings (the .ripwire_config check), " + "stale-acks (no stale= and no stale sa rows), ack-healing (acks matched by their recorded key only: a row that only a " + "legacy-scheme re-key or a content-id match would hide in the full answer is shown here, and can gate). kinds_next= is the " + "call that runs exactly the skipped kinds over the same two commits, at a max-memory just above memory_need= (after a stop: " + "twice the limit, a guess, since no need was measured); it carries no output flag (rows only). EXIT 2 when a row of a kind that ran gates (final: the skipped kinds " + "only add rows, though with ack-healing skipped that row may be one an identity-healed ack hides in the full answer); " + "otherwise EXIT 6, a partial answer with no gating row, never 0 — even when every selected kind ran, since the also_skipped= " + "facts are still missing. "; +inline constexpr const char* kQdMemoryStopLegend = + "memory_stop= names the guard's stop that sent this answer to the changed files: crawl (the crawl line, an eighth of the " + "limit grown) or parse (half the limit) is a soft line, memory_pressure=1 when the OS pressure signal stopped it instead, " + "hard is the limit itself; a soft stop does not show the full need exceeds the limit. "; +inline constexpr const char* kQdMemoryPeakLegend = + "memory_peak= is this process's peak footprint when it went OVER the memory limit: the answer and its exit code are what " + "they would be without it (complete, or the changed-files answer ingest_scope= names), but the last stretch of the " + "comparison ran past the limit the guard holds between phases. "; + // A DEFINITION IS EMITTED WHEN THE THING IT DEFINES IS IN THE DOCUMENT. Nothing is dropped and no limit is // softened: a reader can never meet an undefined name, because a name and its sentence appear together or // not at all. See the constants above for what each section covers and why it is conditional. @@ -1051,6 +1340,26 @@ inline void emitQualityDeltaLegend( const QualityDeltaLegendParts& p, std::FILE* { std::fputs( kForeignAcksLegend, out ); } + if( p.kindsSkipped ) + { + std::fputs( kQdKindsSkippedLegend, out ); + } + if( p.degraded ) + { + std::fputs( kQdDegradedLegend, out ); + } + if( p.memorySkippedShare ) + { + std::fputs( kQdMemorySkippedLegend, out ); + } + if( p.memoryStopped ) + { + std::fputs( kQdMemoryStopLegend, out ); + } + if( p.memoryPeak ) + { + std::fputs( kQdMemoryPeakLegend, out ); + } if( p.staleCollapsed ) { // idea #11: the collapse, named where its attributes are. No flag spelling (a double dash is illegal in a comment). @@ -1241,6 +1550,167 @@ int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFil // branches, one of which was 298 lines. That one body is now runQualityDelta below; the residual // runQualityViews keeps only --dead-code. ONE extraction, verbatim: the 298-line body is unsplit, because // its interior is a sequential pipeline over shared locals, not independent arms. +// qd-memory-degrade: --quality-kinds=K[,K] (range form only; cli.h refuses it elsewhere) — the kinds to run. The vocabulary +// is quality.h's, so an unknown name is refused here, before any tree is read. nullopt when absent or valid. +std::optional applyKindSelector( const rw::Config& cfg, RefPairDelta& refs ) +{ + if( cfg.qualityKinds.empty() ) + { + return std::nullopt; + } + std::string bad; + refs.selected = rw::quality::parseKindList( cfg.qualityKinds, bad ); + if( refs.selected != 0 ) + { + return std::nullopt; + } + rw::emitTo( stderr, "ripwire: --quality-kinds: '{}' is not a kind — name one or more of: {}\n", bad, rw::quality::kindList( rw::quality::kAllKinds, ", " ) ); + return 1; +} + +// L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the ledger against, so it +// judges none (also_skipped= says so). Under --quality-kinds only the selected kinds' acks are judged, against a snapshot of +// those kinds: a dead-code ack read against a snapshot that never computed the dead set would read finding-gone, which is +// false (R9). Every kind selected: the ledger and snapshot every range answer has always used. +std::vector refPairStaleAcks( const gtl::btree_map& acks, const RefPairDelta& refs ) +{ + using namespace rw; + if( refs.degraded ) + { + return {}; + } + if( refs.selected == quality::kAllKinds ) + { + return quality::computeStaleAcks( acks, quality::computeSnapshot( refs.target().ing, refs.target().g, refs.target().root ) ); + } + gtl::btree_map selectedAcks; + for( const auto& [ mapKey, rec ] : acks ) + { + if( quality::kindSelected( refs.selected, std::string_view( rec.kind ).substr( 0, rec.kind.find( ':' ) ) ) ) // the kind, without an origin facet + { + selectedAcks.emplace( mapKey, rec ); + } + } + return selectedAcks.empty() ? std::vector{} + : quality::computeStaleAcks( selectedAcks, quality::computeSnapshot( refs.target().ing, refs.target().g, refs.target().root, nullptr, { refs.selected } ) ); +} + +// qd-memory-degrade — what an answer that did not run every kind says about it, built ONCE for both emitters (the +// identityDisclosure rule: the two surfaces cannot disclose different things). Empty strings on a full answer, which then +// keeps every byte it had. kindsNext is the replay of the kinds memory skipped (empty when none). +struct KindsDisclosure +{ + std::string xml, json, kindsNext; + rw::quality::KindMask memorySkipped = 0; + std::uint64_t peakOver = 0; // memory_peak=: the peak, when it went over the limit (0 = absent) +}; +KindsDisclosure kindsDisclosure( const rw::Config& cfg, const RefPairDelta& refs, const DeltaBasis& basis, bool refPair ) +{ + using namespace rw; + KindsDisclosure out; + if( !refPair ) + { + return out; + } + const auto add = [ & ]( std::string_view name, const std::string& value, bool list ) + { + out.xml += " " + std::string( name ) + "=\"" + value + "\""; + if( !list ) + { + out.json += ",\"" + std::string( name ) + "\":\"" + rw::jsonesc::escapeMcp( value ) + "\""; + return; + } + out.json += ",\"" + std::string( name ) + "\":["; + for( std::size_t at = 0; at <= value.size(); ) + { + const std::size_t comma = std::min( value.find( ',', at ), value.size() ); + out.json += ( at == 0 ? "\"" : ",\"" ) + value.substr( at, comma - at ) + "\""; + at = comma + 1; + } + out.json += "]"; + }; + // review C1: the kinds the CALLER left out and the kinds MEMORY skipped are two sets with two reasons — a selector's + // exclusions are never disclosed as memory cuts, and kinds_next= replays exactly the memory set + const quality::KindMask skipped = static_cast( quality::kAllKinds & ~refs.kinds ); + const quality::KindMask selector = static_cast( quality::kAllKinds & ~refs.selected ); + if( refs.degraded ) + { + out.memorySkipped = static_cast( refs.selected & ~refs.kinds ); + } + if( skipped != 0 ) + { + add( "kinds_skipped", quality::kindList( skipped, "," ), true ); + const bool both = out.memorySkipped != 0 && selector != 0; // then memory_skipped= names memory's share + add( "skipped_why", both ? "selector,memory" : out.memorySkipped != 0 ? "memory" : "selector", false ); + if( both ) + { + add( "memory_skipped", quality::kindList( out.memorySkipped, "," ), true ); + } + } + if( refs.degraded ) + { + const std::uint64_t limit = memguard::limits().hardBytes; + add( "ingest_scope", "changed-files", false ); + if( limit != 0 ) + { + add( "memory_limit", memguard::limitSpelling( limit ), false ); + } + if( !refs.memoryStop.empty() ) + { + add( "memory_stop", refs.memoryStop, false ); + if( refs.memoryPressure ) + { + add( "memory_pressure", "1", false ); + } + } + add( "memory_need", refs.memoryStop.empty() ? memguard::limitSpelling( refs.needBytes ) : std::string( "unmeasured" ), false ); + add( "also_skipped", basis.ackHealingSkipped ? "ack-healing,config-warnings,stale-acks" : "config-warnings,stale-acks", true ); + if( out.memorySkipped != 0 ) + { + out.kindsNext = "--quality-delta=" + refs.baseSha + ".." + refs.targetSha + " --quality-kinds=" + quality::kindList( out.memorySkipped, "," ); + if( limit != 0 ) + { + out.kindsNext += " --max-memory=" + memguard::limitSpelling( memguard::replayLimitBytes( refs.needBytes, refs.ingestNeedBytes, refs.memoryStop.empty() ) ); + } + if( !cfg.qualityScope.empty() ) + { + out.kindsNext += " " + rw::nextFlag( "--scope=", cfg.qualityScope ); // the same partition, so the union keeps its gating + } + add( "kinds_next", out.kindsNext, false ); + } + } + out.peakOver = memguard::peakOverLimit(); + if( out.peakOver != 0 ) + { + add( "memory_peak", memguard::limitSpelling( out.peakOver ), false ); + } + return out; +} + +// qd-memory-degrade: the stderr lines of kindsDisclosure's facts — the degraded answer's one line, and the peak's. +void emitMemoryLines( const KindsDisclosure& kd, const RefPairDelta& refs ) +{ + using namespace rw; + if( refs.degraded ) + { + rw::emitTo( stderr, "ripwire: {}\n", degradedStderrLine( refs, kd.memorySkipped, kd.kindsNext ) ); + } + if( kd.peakOver != 0 ) + { + rw::emitTo( stderr, "ripwire: memory peak {} went over the {} limit ({}): {}, but its last stretch ran past the limit the guard holds " + "between phases — {}\n", + memguard::limitSpelling( kd.peakOver ), memguard::limitSpelling( memguard::limits().hardBytes ), memguard::limitSourceClause(), + refs.degraded ? "the changed-files answer stands as stated above" : "the answer is complete", memguard::kOverride ); + } +} + +// The exit code every --quality-delta emitter returns: 2 when a row gates; a changed-files answer with no gating row is 6 +// (a partial answer, never a clean pass — the kinds it skipped could still gate); otherwise 0. +int qualityDeltaExit( std::size_t gatingCount, const RefPairDelta& refs ) noexcept +{ + return gatingCount > 0 ? 2 : refs.degraded ? 6 : 0; +} + std::optional runQualityDelta( const MainDispatch& d ) { using namespace rw; @@ -1317,6 +1787,10 @@ std::optional runQualityDelta( const MainDispatch& d ) const bool refPair = !cfg.qualityDeltaRange.empty(); RefPairDelta refs; DeltaBasis basis; + if( const std::optional refused = applyKindSelector( cfg, refs ) ) + { + return refused; + } if( const std::optional refused = resolveDeltaBasis( d, baselineFile, refs, basis ) ) { return refused; @@ -1334,7 +1808,7 @@ std::optional runQualityDelta( const MainDispatch& d ) { return refused; } - if( const std::optional refused = refuseUnusableScope( cfg, scope, judged, deltaRoot ) ) + if( const std::optional refused = refuseUnusableScope( cfg, scope, judged, deltaRoot, refs.scopeFiles() ) ) { return refused; } @@ -1547,10 +2021,11 @@ std::optional runQualityDelta( const MainDispatch& d ) // R-I: "the CURRENT tree" is the JUDGED tree, which in the ref-pair form is tree B — checking the // ledger against the working tree there would report acks as stale because of edits that have nothing // to do with the comparison being made. + // qd-memory-degrade: refPairStaleAcks owns the two range-form departures (a changed-files answer judges none; a + // kind selection judges its own kinds' acks only). const std::vector staleAcks = acks.empty() ? std::vector{} - : quality::computeStaleAcks( acks, refPair - ? quality::computeSnapshot( refs.target().ing, refs.target().g, refs.target().root ) - : quality::computeSnapshot( ing, g, cfg.rootPath ) ); + : refPair ? refPairStaleAcks( acks, refs ) + : quality::computeStaleAcks( acks, quality::computeSnapshot( ing, g, cfg.rootPath ) ); // P1.4 — the family now carries THREE whys, and the third is counted SEPARATELY: stale= means // "this ack no longer applies", and a foreign ack applies fine — it is the SESSION that was wrong, @@ -1610,8 +2085,16 @@ std::optional runQualityDelta( const MainDispatch& d ) // registerMacroConfigWarningAttr just above runQualityViews for why the stderr wording lives there). // #387: the inert-name test reads the JUDGED tree's index, which in the ref-pair form is tree B — the working tree // is not indexed there at all (runQualityDeltaRangeBeforeIngest). - const rw::quality::RegisterMacroConfigDiagnostics configDiag = rw::quality::diagnoseRegisterMacroConfig( judged, root ); + // qd-memory-degrade: not over a changed-files index, which cannot tell an inert name from one the tree defines (also_skipped=) + const rw::quality::RegisterMacroConfigDiagnostics configDiag = refs.degraded ? rw::quality::RegisterMacroConfigDiagnostics{} + : rw::quality::diagnoseRegisterMacroConfig( judged, root ); const std::string configWarnAttr = registerMacroConfigWarningAttr( configDiag ); + // qd-memory-degrade: the kinds this answer did not run, why, and the replay — plus the peak, read once now that the + // comparison is done (kindsDisclosure). Their stderr lines come first: an answer that is partial, or that ran past + // the limit, says so on the channel a human reads before anything else. + const KindsDisclosure kd = kindsDisclosure( cfg, refs, basis, refPair ); + emitMemoryLines( kd, refs ); + const bool deadRan = !refPair || quality::kindSelected( refs.kinds, "dead-code" ); // dead-code's side counts exist only when it ran // P2.5 — one stderr line NAMING the gating finding, in --token-budget's style ("ripwire: --token-budget // exceeded: est_tokens=… > budget=…"). stdout is the machine artifact and a caller that only checks @@ -1679,11 +2162,14 @@ std::optional runQualityDelta( const MainDispatch& d ) + ( basis.declinedCallExcluded == 0 ? std::string() // the XML twin's declined-call-excluded=, absent at zero : ",\"declined-call-excluded\":" + std::to_string( basis.declinedCallExcluded ) ) + rw::countFieldOrEmpty( "value-ref-excluded", basis.valueRefExcluded, /*json=*/true ); - rw::emitTo( stdout, "{{\"baseline\":\"{}\",\"regressions\":{},\"minor\":{},\"acked\":{},\"stale\":{}," - "\"preexisting-worse\":{},\"new-symbol\":{},\"gating\":{},\"register-macro-excluded\":{},\"api-new-surface\":{},\"at\":{}{}{}{}{}{}{},\"r\":[", - jsonStr( baseMarkerJ ).c_str(), regs.size(), minorCount, ackedCount, staleAcks.size(), - preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, basis.apiNewSurface, atJsonJ.c_str(), refs.jsonAttrs.c_str(), - identityJson.c_str(), scopeJson.c_str(), configWarnJson.c_str(), absorbedJson.c_str(), headBasisJson.c_str() ); + // qd-memory-degrade: stale and register-macro-excluded are absent (never 0) when their pass did not run + const std::string staleJ = refs.degraded ? std::string() : ",\"stale\":" + std::to_string( staleAcks.size() ); + const std::string rmeJ = deadRan ? ",\"register-macro-excluded\":" + std::to_string( basis.registerMacroExcluded ) : std::string(); + rw::emitTo( stdout, "{{\"baseline\":\"{}\",\"regressions\":{},\"minor\":{},\"acked\":{}{}," + "\"preexisting-worse\":{},\"new-symbol\":{},\"gating\":{}{},\"api-new-surface\":{},\"at\":{}{}{}{}{}{}{}{},\"r\":[", + jsonStr( baseMarkerJ ).c_str(), regs.size(), minorCount, ackedCount, staleJ, + preexistingCount, newSymbolCount, gatingCount, rmeJ, basis.apiNewSurface, atJsonJ.c_str(), refs.jsonAttrs.c_str(), + identityJson.c_str(), scopeJson.c_str(), configWarnJson.c_str(), absorbedJson.c_str(), headBasisJson.c_str(), kd.json ); // P1: one row emitter, called for both halves of the scope partition — the disclosed rows carry // the identical key set, so nothing about a row changes by being someone else's. `gatingAllowed` // is the ONE difference: an out-of-scope row is not what the exit code fires on, so claiming @@ -1761,7 +2247,7 @@ std::optional runQualityDelta( const MainDispatch& d ) } std::fputs( quality::staleAcksJsonArray( saRows ).c_str(), stdout ); // L2 — "sa":[...], same taxonomy as the XML sa= rows below rw::emitRaw( stdout, "}" ); - return gatingCount > 0 ? 2 : 0; + return qualityDeltaExit( gatingCount, refs ); } std::vector esc; @@ -1900,7 +2386,9 @@ std::optional runQualityDelta( const MainDispatch& d ) emitQualityDeltaLegend( { baseSel.marker, refPair, identityAttrs, !served.empty(), ackedCount > 0, basis.registerMacroExcluded > 0, configDiag.total() > 0, regs, outOfScope, scope.active() || !foreignAcks.empty(), !foreignAcks.empty(), baselineAbsorbed, - basis.headBasis, basis.declinedCallExcluded > 0, basis.valueRefExcluded > 0, collapse }, out ); + basis.headBasis, basis.declinedCallExcluded > 0, basis.valueRefExcluded > 0, collapse, + refs.kinds != quality::kAllKinds, refs.degraded, !refs.memoryStop.empty(), kd.peakOver != 0, + kd.memorySkipped != 0 && refs.selected != quality::kAllKinds }, out ); const char* baseMarker = baseSel.marker; // R3: ditto — one seam decides staleness AND names it // 2026-09-06: what the sidecar readers skipped, on the root (absent means none) — see kQdBaseHeadUnreadable std::string sidecarHealthAttrs; @@ -1912,14 +2400,17 @@ std::optional runQualityDelta( const MainDispatch& d ) if( basis.declinedCallExcluded > 0 ) { sidecarHealthAttrs += " declined-call-excluded=\"" + std::to_string( basis.declinedCallExcluded ) + "\""; } sidecarHealthAttrs += rw::countAttrXmlOrEmpty( "value-ref-excluded", basis.valueRefExcluded ); // absent at zero, beside its sibling // at= anchors this regression list to the commit (+dirty state) it was computed against. - rw::emitTo( out, "", - baseMarker, regs.size(), minorCount, ackedCount, staleAcks.size(), preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, basis.apiNewSurface, + // qd-memory-degrade: stale= and register-macro-excluded= are absent (never 0) when their pass did not run + const std::string staleX = refs.degraded ? std::string() : " stale=\"" + std::to_string( staleAcks.size() ) + "\""; + const std::string rmeX = deadRan ? " register-macro-excluded=\"" + std::to_string( basis.registerMacroExcluded ) + "\"" : std::string(); + rw::emitTo( out, "", + baseMarker, regs.size(), minorCount, ackedCount, staleX, preexistingCount, newSymbolCount, gatingCount, rmeX, basis.apiNewSurface, // R-I: at= is OMITTED for the ref-pair form rather than stamped with the working tree's // sha, which would anchor the list to a commit it was not computed from. base_ref= and // target_ref= are the anchor there, and they carry FULL shas because a wave measurement // gets quoted into handoffs where a 9-char prefix is one collision from unverifiable. atAttrs.c_str(), refs.attrs.c_str(), identityAttrs.c_str(), - scopeAttrs.c_str(), configWarnAttr.c_str(), baselineAbsorbedAttr.c_str(), ( sidecarHealthAttrs + ( collapse ? staleCollapseAttrs : std::string() ) ).c_str() ); + scopeAttrs.c_str(), configWarnAttr.c_str(), baselineAbsorbedAttr.c_str(), ( sidecarHealthAttrs + ( collapse ? staleCollapseAttrs : std::string() ) + kd.xml ).c_str() ); for( const quality::Regression& r : regs ) { emitRow( out, r, /*gatingAllowed=*/true ); @@ -1961,7 +2452,7 @@ std::optional runQualityDelta( const MainDispatch& d ) if( !collapseStale ) { emitDocument( stdout, /*collapse=*/false ); - return gatingCount > 0 ? 2 : 0; + return qualityDeltaExit( gatingCount, refs ); } const Rendered full = renderToString( [ & ]( std::FILE* f ) { emitDocument( f, /*collapse=*/false ); } ); const Rendered folded = renderToString( [ & ]( std::FILE* f ) { if( verdictOnly ) { emitVerdict( f ); } else { emitDocument( f, /*collapse=*/true ); } } ); @@ -1969,11 +2460,11 @@ std::optional runQualityDelta( const MainDispatch& d ) { // renderToString DISCLOSEd the failed buffer; the full document is the superset, written straight to stdout emitDocument( stdout, /*collapse=*/false ); - return gatingCount > 0 ? 2 : 0; + return qualityDeltaExit( gatingCount, refs ); } const std::string& chosen = foldPays( folded.text, full.text ) ? folded.text : full.text; // compactlegend.h: the margin and why std::fwrite( chosen.data(), 1, chosen.size(), stdout ); - return gatingCount > 0 ? 2 : 0; // r26: only a PREEXISTING-worse AND major regression gates (== gating=) + return qualityDeltaExit( gatingCount, refs ); // r26: only a PREEXISTING-worse AND major regression gates (== gating=) } return std::nullopt; } diff --git a/test/anchorcheck.sh b/test/anchorcheck.sh index 834f5ca0d..fc128b200 100755 --- a/test/anchorcheck.sh +++ b/test/anchorcheck.sh @@ -124,6 +124,8 @@ QUERY="frobnicate widget cache" # 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 +# RE-PIN 2026-10-15 (forbudget-soft round C, FOLD C; rule 4): golden_for.xml re-captured — the ONLY bytes that moved are the +# (page rows with sym= after the rank rows; total= every positive-score file), the tail legend clause and est_tokens=. diff -q "$TMP/plain_full.xml" "$ROOT/test/anchorfix/golden_for.xml" >/dev/null \ && ok "golden-neutral: plain --for --no-route byte-identical to the pre---anchor golden" \ || no "plain --for --no-route drifted from test/anchorfix/golden_for.xml (--anchor leaked into the default lens)" diff --git a/test/anchorfix/golden_for.xml b/test/anchorfix/golden_for.xml index 09364161d..249b7639b 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() children of its root (CLI JSON +# "def_table":[{"ix","t","sc","p"}], MCP "def_table":[{"ix","kind","scope","file","line"}]); every row carries +# of_def="TOKENS" (JSON/MCP "of_def":[TOKENS]): space-separated K (a ) with a mark — K alone = a proven edge +# ties the row to group K, K~n = by name alone, K~s = only as one arm of a call the resolver split between +# same-named definitions, K~t = bound to a primary template whose specialisation is also a listed group (which of +# them the call reaches is unproven). callers: row -> group; callees: group -> row; impact: a chain of calls from +# the row to the group (a by-name hop makes it ~n, else a split hop ~s); safe-delete rows as callers; the +# impact rollup row: the union of its symbols' tokens, the strongest mark per K. Above 16 groups the table +# lists the groups the page's rows name and the root says defs_listed=N; there a row prints its K, K~s and K~t tokens +# and ONE bare ~n when it also has by-name ties (a row tied by name alone prints no of_def: its via="name" hedges it). +# * Every other answer (one definition, or one definition with its own declarations) carries no table and no of_def; +# its rows that are one arm of a split call carry prov="split" (the map's per-edge vocabulary), whatever defs= is. +# * --path: a to= with several definitions carries no to_p= unless a proven hop bound it (unreached, or the hop into +# it is a split arm or by name: dropped); a from= with several definitions carries no from_p= when unreached. + +# Corpus test/attribmultideffix (in-tree, outside test/fixture); the stand-in dir per known-items row in [brackets]: +# cpp/ [ninja-07] StringPiece::empty vs EvalString::empty (UsesPiece/UsesEval/UsesBoth, DrivePiece/DriveEval at depth 2 +# in their own files; UsesStd, a std::string::empty confounder), [ninja-26] ManifestParser::ParseDefault (decl + +# def) calling the CanonicalizePath overload pair (a split call), near misses in fmt.{h,cc}: Fmt::put overloads in +# ONE class (split), outer::run vs outer::inner::run (nested namespace), Box::get vs the Box +# specialisation; ReadPath decl + def; detail.{h,cc}: a::detail::tally declared, b::detail::tally defined (same +# leaf scope, another chain: a foreign declaration). +# c/ plain C: a static helper in helper_a.c + a same-named prototype in helper_b.c (foreign declaration); proto.h's +# lost + its definition in proto.c (the tmux prototype + definition shape: one group). +# go/ [nfpm-06] three Get (files.Get fn, modtime Get fn, (*Info).Get method), cross_kind fn:2,method:1. +# py/ts/java/ two same-named methods of different classes IN ONE FILE (entity identity: owner, not file + name); +# py/ also draw_mixed: a proven Widget.render call and a by-name thing.render call in one row. [kysely-06] ts/; +# ts/overloads.ts: overload signatures of differing arity + one implementation (one group). +# js/rb/ two same-named methods of two classes, constructor-typed receivers. +# rs/ [mdbook-06] Book::iter vs Shelf::iter on param-typed receivers (by name + split today: hedged tokens). +# objc/ two -area methods, typed message receivers (split today: hedged tokens). +# u2/ [final review R1] scan_tok defined in prog1/util.c, an untied prototype in prog2/main.c (no #include proves the +# tie), a static namesake in prog3/other.c: a printed table lists all three groups (the prototype bodyless=1). +# (generated at run time) 20 Python classes with one same-named method each: the definition-table cap (16); a second +# 20-class file with a by-name and a mixed caller (final review R2: a cut table enumerates no by-name tie). +# +# Arm classes in the output: [P] positive — the defect shape, RED on a binary without per-row attribution; [N] near miss — +# a shape the new code could plausibly mis-handle (a one-group answer that must stay plain, an overload or a namesake that +# must not be over-attributed); it is red on such a binary only where it needs a hedge that binary cannot print; [C] control +# — holds on every binary. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/attribmultideffix" +DIR="$( mktemp -d )"; trap 'rm -rf "$DIR"' EXIT +fail=0 +# every verdict is an if/else: a failed write of a PASS line must never print FAIL for an arm that passed (gateexitcheck G2) +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; } +# say RC PASSMSG FAILMSG — the verdict of the test just run ($?), as an if/else (gateexitcheck G2) +say(){ if [ "$1" = 0 ]; then ok "$2"; else no "$3"; fi; } + +echo "attribmultidefcheck: BIN=$BIN" +[ -x "$BIN" ] || { echo " FAIL no ripwire binary at $BIN"; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo " FAIL python3 is required (the answers are parsed, not grepped)"; exit 1; } +[ -d "$FIX/cpp" ] || { echo " FAIL corpus $FIX missing"; exit 1; } + +# ── the answer reader: one canonical line per row, so every arm compares strings ───────────────────────────────────── +# amd.py FILE FORM ROOTTAG ROWKEY +# FORM xml : ROOTTAG = the root element (callers|callees|impact|safe-delete), ROWKEY = the row element (s|c|rf) +# FORM json: ROOTTAG unused, ROWKEY = the row array key (callers|callees|calledBy|calls|reach…); FILE may be an MCP +# JSON-RPC line (its content[0].text is read) +# prints: ROOT ok defs=N groups=G (G = rows; -1 when the root is absent) +# DEF k p bodyless sc +# ROW namecanonflags canon = the of_def tokens mapped to their group's p (sorted), "-" when the row has +# no of_def, "!K" for a token whose K names no ; flags = via=name / prov=split +cat >"$DIR/amd.py" <<'PY' +import json, re, sys +path, form, roottag, rowkey = sys.argv[1:5] +raw = open(path, encoding='utf-8', errors='replace').read() +def attrs(tag): + return dict(re.findall(r'\s([A-Za-z_]+)="([^"]*)"', tag)) +def canon(tokens, table): + out = [] + for t in tokens: + k, _, mark = t.partition('~') + if k == '' and mark: + out.append('~' + mark); continue # a cut table's bare ~n (its by-name ties, not enumerated) + p = table.get(k) + out.append(('!' + k) if p is None else (p + ('~' + mark if mark else ''))) + return ' '.join(sorted(out)) +if form == 'mcp': + try: + msg = json.loads(raw.strip().splitlines()[-1]) + raw = msg['result']['content'][0]['text'] + except Exception as e: + print('ROOT missing mcp-unreadable %s' % e); sys.exit(0) + form = 'json' if raw.lstrip().startswith('{') else 'xml' +if form == 'xml': + m = re.search(r'<%s\s[^>]*>' % re.escape(roottag), raw) + if not m: + print('ROOT missing'); sys.exit(0) + root = attrs(m.group(0)) + body = raw[m.end():] + end = body.find('' % roottag) + body = body if end < 0 else body[:end] + table = {} + defs = re.findall(r']*?/?>', body) + for d in defs: + a = attrs(d) + table[a.get('ix', '?')] = a.get('p', '?') + print('DEF\t%s\t%s\t%s\t%s' % (a.get('ix', '?'), a.get('p', '?'), a.get('bodyless', '0'), a.get('sc', ''))) + print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=%s\tfrom_p=%s\tcount=%s\tdefs_listed=%s\tnext_offset=%s' % ( + root.get('defs', root.get('to_defs', '?')), len(defs), root.get('to_p', '-'), root.get('from_p', '-'), + root.get('count', root.get('reaches', root.get('callers', '?'))), root.get('defs_listed', '-'), root.get('next_offset', '-'))) + for r in re.findall(r'<%s\s[^>]*?/?>' % re.escape(rowkey), body): + a = attrs(r) + name = a.get('n', a.get('p', '?')) + tok = a.get('of_def') + flags = ' '.join(x for x in (('via=name' if a.get('via') == 'name' else ''), ('prov=split' if a.get('prov') == 'split' else '')) if x) + print('ROW\t%s\t%s\t%s' % (name, '-' if tok is None else canon(tok.split(), table), flags)) +else: + try: + j = json.loads(raw) + except Exception as e: + print('ROOT missing json-unreadable %s' % e); sys.exit(0) + tab = j.get('def_table') or [] + table = {} + for d in tab: + p = d.get('p') or ('%s:%s' % (d.get('file', '?'), d.get('line', '?'))) + table[str(d.get('ix'))] = p + print('DEF\t%s\t%s\t%s\t%s' % (d.get('ix'), p, d.get('bodyless', 0), d.get('sc', d.get('scope', '')))) + print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=%s\tfrom_p=%s\tcount=%s\tdefs_listed=%s\tnext_offset=%s' % ( + j.get('defs', j.get('to_defs', '?')), len(tab), j.get('to_p', '-'), j.get('from_p', '-'), + j.get('count', j.get('reaches', '?')), j.get('defs_listed', '-'), j.get('next_offset', '-'))) + for r in j.get(rowkey) or []: + name = r.get('n', r.get('name', '?')) + tok = r.get('of_def') + flags = ' '.join(x for x in (('via=name' if r.get('via') == 'name' else ''), ('prov=split' if r.get('prov') == 'split' else '')) if x) + print('ROW\t%s\t%s\t%s' % (name, '-' if tok is None else canon([str(t) for t in tok], table), flags)) +PY + +# ans TAG LANG ROOTTAG ROWKEY FORM -- ARGS… : run BIN on the corpus, read the answer into $DIR/TAG.rows; FAIL (and return 1) +# when the run failed or printed no root, so no absence arm ever reads a crashed or empty run as "not attributed". +ans(){ + local tag="$1" lang="$2" roottag="$3" rowkey="$4" form="$5"; shift 6 + if ! ( cd "$FIX/$lang" && perl -e 'alarm 120; exec @ARGV' "$BIN" . --no-cache "$@" ) >"$DIR/$tag.out" 2>"$DIR/$tag.err"; then + no "$tag: '$*' on $lang exited non-zero: $( head -2 "$DIR/$tag.err" )"; : >"$DIR/$tag.rows"; return 1 + fi + python3 "$DIR/amd.py" "$DIR/$tag.out" "$form" "$roottag" "$rowkey" >"$DIR/$tag.rows" + if ! grep -q '^ROOT ok' "$DIR/$tag.rows"; then no "$tag: '$*' on $lang printed no <$roottag> root"; return 1; fi + return 0 +} +# mcp TAG LANG ROWKEY TOOL ARGSJSON : the same, through the MCP server (initialize + one tools/call) +mcp(){ + local tag="$1" lang="$2" rowkey="$3" tool="$4" args="$5" + printf '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s",%s}}}\n' \ + "$tool" "$FIX/$lang" "$args" | perl -e 'alarm 120; exec @ARGV' "$BIN" --mcp 2>"$DIR/$tag.err" | tail -1 >"$DIR/$tag.out" + python3 "$DIR/amd.py" "$DIR/$tag.out" mcp "$( [ "$tool" = impact ] && echo impact || echo path )" "$rowkey" >"$DIR/$tag.rows" + if ! grep -q '^ROOT ok' "$DIR/$tag.rows"; then no "$tag: MCP $tool on $lang gave no answer: $( head -c 200 "$DIR/$tag.out" )"; return 1; fi + return 0 +} +row(){ awk -F'\t' -v n="$2" '$1=="ROW" && $2==n { print $3; exit }' "$DIR/$1.rows"; } +rowflags(){ awk -F'\t' -v n="$2" '$1=="ROW" && $2==n { print $4; exit }' "$DIR/$1.rows"; } +groups(){ awk -F'\t' '$1=="ROOT" { sub("groups=","",$4); print $4; exit }' "$DIR/$1.rows"; } +rootf(){ awk -F'\t' -v f="$2" '$1=="ROOT" { for(i=3;i<=NF;i++) if(index($i,f"=")==1){ print substr($i,length(f)+2); exit } }' "$DIR/$1.rows"; } +nrows(){ grep -c '^ROW' "$DIR/$1.rows"; } + +# expect CLASS TAG NAME WANT : the row's canonical attribution equals WANT exactly +expect(){ + local cls="$1" tag="$2" n="$3" want="$4" got + got="$( row "$tag" "$n" )" + if [ -z "$got" ]; then no "[$cls] $tag: no row n=\"$n\" (rows: $( grep '^ROW' "$DIR/$tag.rows" | cut -f2 | tr '\n' ' ' ))" + elif [ "$got" = "$want" ]; then ok "[$cls] $tag: $n -> $want" + else no "[$cls] $tag: $n attributed '$got', want '$want'"; fi +} +# hedged CLASS TAG NAME ALLOWED : the row has of_def, every token is marked (~n/~s) and names a group in ALLOWED +hedged(){ + local cls="$1" tag="$2" n="$3" allowed="$4" got t bad="" + got="$( row "$tag" "$n" )" + if [ -z "$got" ] || [ "$got" = "-" ]; then no "[$cls] $tag: $n carries no of_def ('$got'); a multi-definition row must name or hedge its definition"; return; fi + for t in $got; do + case "$t" in *~n|*~s) ;; *) bad="$bad $t(unmarked)" ;; esac + case " $allowed " in *" ${t%%~*} "*) ;; *) bad="$bad $t(not in: $allowed)" ;; esac + done + [ -z "$bad" ]; say $? "[$cls] $tag: $n hedged ($got)" \ + "[$cls] $tag: $n tokens '$got' —$bad" +} +# plain CLASS TAG [split] : one-group answer — no table and no of_def on any row (a read of a successful run); and, +# unless `split` is given (an answer whose split arms must carry it), no prov="split" on any row either +plain(){ + local cls="$1" tag="$2" allow="${3:-}" + if [ "$( groups "$tag" )" = 0 ] && ! awk -F'\t' '$1=="ROW" && $3!="-" { f=1 } END { exit !f }' "$DIR/$tag.rows" \ + && { [ -n "$allow" ] || ! awk -F'\t' '$1=="ROW" && $4 ~ /prov=split/ { f=1 } END { exit !f }' "$DIR/$tag.rows"; } \ + && [ "$( nrows "$tag" )" -gt 0 ]; then ok "[$cls] $tag: one definition group — no table, no of_def$( [ -n "$allow" ] || printf ', no prov' ) ($( nrows "$tag" ) rows)" + else no "[$cls] $tag: a one-group answer grew attribution: groups=$( groups "$tag" ), rows: $( grep '^ROW' "$DIR/$tag.rows" | cut -f2,3,4 | tr '\t\n' '= ' )"; fi +} +# rowset CLASS TAG COUNT NAMES : attribution adds attributes and never removes a row — the root count and the sorted row-name +# multiset equal the base binary's (pinned from base d4325a8c) +rowset(){ + local cls="$1" tag="$2" want_n="$3" want="$4" got got_n + got="$( awk -F'\t' '$1=="ROW" { print $2 }' "$DIR/$tag.rows" | sort | tr '\n' ' ' | sed 's/ $//' )" + got_n="$( rootf "$tag" count )" + { [ "$got" = "$want" ] && [ "$got_n" = "$want_n" ]; }; say $? "[C] $tag: rows preserved (count=$got_n; $got)" \ + "[C] $tag: rows changed: count='$got_n' want '$want_n'; rows '$got' want '$want'" +} +# defbody CLASS TAG P BODYLESS : the row with p=P exists and carries bodyless=BODYLESS (1 = a declaration tied to no +# listed definition, its own group) +defbody(){ + local cls="$1" tag="$2" p="$3" want="$4" got + got="$( awk -F'\t' -v p="$p" '$1=="DEF" && $3==p { print $4; exit }' "$DIR/$tag.rows" )" + [ "$got" = "$want" ]; say $? "[$cls] $tag: bodyless=$want" \ + "[$cls] $tag: bodyless='$got', want '$want' (defs: $( awk -F'\t' '$1=="DEF" { printf "%s/%s ", $3, $4 }' "$DIR/$tag.rows" ))" +} +# invariant CLASS TAG : every row has of_def, every K resolves to a , and a row with no unmarked token is hedged +invariant(){ + local cls="$1" tag="$2" bad + bad="$( awk -F'\t' '$1=="ROW" { if ($3=="-") print $2": no of_def"; else if ($3 ~ /!/) print $2": token names no ("$3")" }' "$DIR/$tag.rows" )" + if [ "$( nrows "$tag" )" -gt 0 ] && [ -z "$bad" ]; then ok "[$cls] $tag: invariant — all $( nrows "$tag" ) rows attributed or hedged, every K in the table" + else no "[$cls] $tag: invariant broken: $( printf '%s' "$bad" | tr '\n' ';' | cut -c1-300 )"; fi +} + +SP=string_piece.h:6; EV=eval_env.h:5 + +# ── (A) ninja-07 shape, C++ callers: the union names its definition per row ─────────────────────────────────────────── +echo "(A) C++ --callers=empty (StringPiece::empty vs EvalString::empty)" +if ans a_cpp cpp callers s xml -- --callers=empty; then + [ "$( groups a_cpp )" = 2 ]; say $? "[P] a_cpp: two groups listed" \ + "[P] a_cpp: rows=$( groups a_cpp ), want 2" + expect P a_cpp UsesPiece "$SP" + expect P a_cpp UsesEval "$EV" + expect P a_cpp UsesBoth "$EV $SP" + expect P a_cpp ParseDefault "$EV" + invariant P a_cpp + rowset C a_cpp 4 "ParseDefault UsesBoth UsesEval UsesPiece" + # the std::string::empty confounder: the C++ standard-member gate declines it (no row), or the row names no group proven + g="$( row a_cpp UsesStd )" + case "$g" in + "") ok "[N] a_cpp: UsesStd (std::string::empty) is no row" ;; + *) bad=""; for t in $g; do case "$t" in *~n|*~s|*~t) ;; *) bad="$bad $t" ;; esac; done + [ -z "$bad" ]; say $? "[N] a_cpp: UsesStd row hedged ($g)" "[N] a_cpp: UsesStd (a std::string member call) carries proven token(s):$bad" ;; + esac + sed 's/-->.*//' "$DIR/a_cpp.out" | grep -q 'of_def='; say $? "[P] a_cpp: the legend defines of_def= (present-only clause)" \ + "[P] a_cpp: of_def= is not defined in the answer's legend" +fi +if ans a_cppj cpp - callers json -- --callers=empty --json; then + expect P a_cppj UsesPiece "$SP"; expect P a_cppj UsesBoth "$EV $SP" + rowset C a_cppj 4 "ParseDefault UsesBoth UsesEval UsesPiece" +fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty --format=columnar ) >"$DIR/a_col.out" 2>/dev/null && grep -q ' table and an of_def column" \ + "[P] a_col: columnar lacks rows or the of_def column: $( grep -oE 'fields="[^"]*"' "$DIR/a_col.out" )" +else no "a_col: columnar --callers=empty failed"; fi +if ans a_sd cpp safe-delete c xml -- --safe-delete=empty; then + expect P a_sd UsesPiece "$SP"; expect P a_sd UsesEval "$EV"; expect P a_sd UsesBoth "$EV $SP"; expect P a_sd ParseDefault "$EV" + invariant P a_sd + rowset C a_sd 4 "ParseDefault UsesBoth UsesEval UsesPiece" +fi +if mcp a_mcp cpp calledBy find_referencing_symbols '"symbol":"empty"'; then + expect P a_mcp UsesPiece "$SP"; expect P a_mcp UsesBoth "$EV $SP"; invariant P a_mcp + rowset C a_mcp 4 "ParseDefault UsesBoth UsesEval UsesPiece" +fi +if mcp a_mcps cpp calledBy find_symbol '"symbol":"empty"'; then + expect P a_mcps UsesEval "$EV" + [ "$( groups a_mcps )" = 2 ]; say $? "[P] a_mcps: find_symbol's root symbol object rides beside a def_table of 2" \ + "[P] a_mcps: def_table rows=$( groups a_mcps ), want 2" +fi + +# ── (B) ninja-26 shape, C++ callees: a declaration + its definition = ONE group; the split overload arms are hedged ── +echo "(B) C++ --callees=ParseDefault (decl + def; CanonicalizePath overload split)" +if ans b_cpp cpp callees s xml -- --callees=ParseDefault; then + plain N b_cpp split + for n in CanonicalizePath; do + n_split="$( awk -F'\t' -v n="$n" '$1=="ROW" && $2==n && $4 ~ /prov=split/' "$DIR/b_cpp.rows" | wc -l | tr -d ' ' )" + [ "$n_split" = 2 ]; say $? "[P] b_cpp: both CanonicalizePath arms carry prov=\"split\"" \ + "[P] b_cpp: $n_split of 2 CanonicalizePath rows carry prov=\"split\"" + done + for n in empty ReadPath; do + [ -z "$( rowflags b_cpp "$n" )" ]; say $? "[N] b_cpp: proven callee $n carries no hedge" \ + "[N] b_cpp: proven callee $n hedged '$( rowflags b_cpp "$n" )'" + done +fi +if ans b_cpp2 cpp callers s xml -- --callers=CanonicalizePath; then + # two overloads, each declared in util.h and defined in util.cc: the declarations fold into their definitions + [ "$( groups b_cpp2 )" = 2 ]; say $? "[N] b_cpp2: two groups (each util.h declaration folds into its util.cc definition)" \ + "[N] b_cpp2: rows=$( groups b_cpp2 ), want 2" + hedged P b_cpp2 ParseDefault "util.cc:2 util.cc:6" + # the string* overload's own call passes three arguments: the resolver binds it to the 3-parameter overload, proven + expect N b_cpp2 CanonicalizePath util.cc:6 +fi +if ans b_rp cpp callers s xml -- --callers=ReadPath; then plain N b_rp; fi +# UsesPiece is declared in piece_user.cc with no #include that ties it to parser.cc's definition: the identity rule answers +# Unproven (may declare it, cannot prove it), and no row ties to that declaration (DrivePiece's call binds the definition), so +# the declaration is silent and the answer stays one group — the tmux/ninja prototype shape (a variadic or template-qualified +# declarator is Unproven the same way). A FOREIGN declaration (c_tal, c_hlp below: provably another function) is always listed. +if ans b_up cpp callers s xml -- --callers=UsesPiece; then plain N b_up; fi +if ans b_q cpp callers s xml -- --callers=string_piece.h:empty; then plain N b_q; fi +# Q1 (orchestrator ruling): the split-arm hedge rides single-definition answers too (by-design byte change on them) +if ans b_one cpp callees s xml -- --callees=PutInt; then + plain N b_one split + n_split="$( awk -F'\t' '$1=="ROW" && $2=="put" && $4 ~ /prov=split/' "$DIR/b_one.rows" | wc -l | tr -d ' ' )" + [ "$n_split" = 2 ]; say $? "[P] b_one: a single-definition --callees=PutInt marks both put arms prov=\"split\"" \ + "[P] b_one: $n_split of 2 put rows carry prov=\"split\" on the single-definition answer" +fi + +# ── (C) near misses in one C++ file pair, the foreign declarations (C++ and C), the TS overload signatures ────────────── +echo "(C) near misses: overloads in one class, nested namespace, template specialisation, foreign declarations, TS overloads" +if ans c_put cpp callers s xml -- --callers=put; then + hedged N c_put PutInt "fmt.cc:2 fmt.cc:3" + hedged N c_put PutStr "fmt.cc:2 fmt.cc:3" +fi +if ans c_run cpp callers s xml -- --callers=run; then + [ "$( groups c_run )" = 2 ]; say $? "[N] c_run: two groups (each fmt.h declaration folds into its own fmt.cc definition)" \ + "[N] c_run: rows=$( groups c_run ), want 2" + expect N c_run RunOuter fmt.cc:4 + expect N c_run RunInner fmt.cc:5 + sc4="$( awk -F'\t' '$1=="DEF" && $3=="fmt.cc:4" { print $5 }' "$DIR/c_run.rows" )" + sc5="$( awk -F'\t' '$1=="DEF" && $3=="fmt.cc:5" { print $5 }' "$DIR/c_run.rows" )" + { [ -n "$sc4" ] && [ -n "$sc5" ] && [ "$sc4" != "$sc5" ]; }; say $? "[N] c_run: the two run groups carry distinct sc= ($sc4 / $sc5)" \ + "[N] c_run: nested-namespace groups not told apart by sc= ('$sc4' / '$sc5')" +fi +# Box::get (fmt.h:14) vs the Box specialisation's get (fmt.h:18). The resolver binds GetInt(const Box&) to the +# PRIMARY today (premise arm below), so a token naming the primary is a claim no proof backs: it must carry the ~t mark +# (bound to a primary template whose specialisation is listed), never stand unmarked, and never pose as a split (~s: the +# resolver split nothing). The specialisation itself, proven, is accepted (a future resolver fix). +gettok(){ + local tag="$1" n="$2" g t bad="" + g="$( row "$tag" "$n" )" + if [ -z "$g" ] || [ "$g" = "-" ]; then no "[N] $tag: $n carries no of_def ('$g')"; return; fi + for t in $g; do case "$t" in "fmt.h:14~t"|"fmt.h:18") ;; *) bad="$bad $t" ;; esac; done + [ -z "$bad" ]; say $? "[N] $tag: $n tokens '$g' (the primary only as ~t)" \ + "[N] $tag: $n tokens '$g' — not allowed:$bad (want fmt.h:14~t or fmt.h:18)" +} +if ans c_get cpp callers s xml -- --callers=get; then + gettok c_get GetInt + gettok c_get GetDouble +fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=get --legend=full ) >"$DIR/c_getl.out" 2>/dev/null; then + sed 's/-->.*//' "$DIR/c_getl.out" | grep -q 'K~t'; say $? "[P] c_getl: the legend reads the ~t mark (K~t)" \ + "[P] c_getl: no K~t reading in the --legend=full answer" +else no "c_getl: --callers=get --legend=full failed"; fi +# S4 premise: the resolver's binding this lane hedges and does not fix (the resolver card flips this arm when it lands) +if ans c_geti cpp callees s xml -- --callees=GetInt; then + grep -q 'n="get" p="fmt.h:14"' "$DIR/c_geti.out"; say $? "[C] c_geti: premise — the resolver binds GetInt's b.get() to the primary (fmt.h:14)" \ + "[C] c_geti: premise moved — GetInt's get is no longer fmt.h:14 (resolver card landed? re-anchor c_get): $( grep -o ']*>' "$DIR/c_geti.out" | head -2 | tr '\n' ' ' )" +fi +if ans c_tal cpp callers s xml -- --callers=tally; then + [ "$( groups c_tal )" = 2 ]; say $? "[N] c_tal: a::detail::tally (declared) and b::detail::tally (defined) are two groups" \ + "[N] c_tal: rows=$( groups c_tal ), want 2 (same leaf scope, another chain: never one group)" + defbody N c_tal detail.h:3 1 + defbody N c_tal detail.cc:2 0 + invariant N c_tal +fi +if ans c_hlp c callers s xml -- --callers=helper; then + [ "$( groups c_hlp )" = 2 ]; say $? "[N] c_hlp: C static helper (helper_a.c) and the prototype in helper_b.c are two groups" \ + "[N] c_hlp: rows=$( groups c_hlp ), want 2 (internal linkage in another file: never one group)" + defbody N c_hlp helper_b.c:2 1 + defbody N c_hlp helper_a.c:2 0 + invariant N c_hlp +fi +if ans c_lost c callers s xml -- --callers=lost; then plain N c_lost; fi # C prototype + definition: one group +if ans c_pick ts callers s xml -- --callers=pick; then plain N c_pick; fi # TS overload signatures + implementation +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=empty ) >"$DIR/c_for.out" 2>/dev/null; then + n_d="$( grep -oE ']*n="empty"[^>]*>' "$DIR/c_for.out" | grep -c 'sc="' )" + [ "$n_d" -ge 2 ]; say $? "[C] c_for: --for=empty serves each definition as its own row with sc= ($n_d)" \ + "[C] c_for: --for=empty rows with sc=: $n_d" +else no "c_for: --for=empty failed"; fi + +# ── (D) impact: per-row attribution through the transitive reach, and the file rollup ─────────────────────────────── +echo "(D) C++ --impact=empty" +if ans d_imp cpp impact s xml -- --impact=empty; then + expect P d_imp UsesPiece "$SP"; expect P d_imp UsesEval "$EV"; expect P d_imp UsesBoth "$EV $SP" + expect P d_imp DrivePiece "$SP"; expect P d_imp DriveEval "$EV"; expect P d_imp ParseDefault "$EV" + invariant P d_imp + rowset C d_imp 6 "DriveEval DrivePiece ParseDefault UsesBoth UsesEval UsesPiece" +fi +if ans d_rf cpp impact rf xml -- --impact=empty --limit=2; then + expect P d_rf parser.cc "$EV $SP" + expect P d_rf eval_user.cc "$EV" + rowset C d_rf 6 "eval_user.cc parser.cc" +fi +if ans d_impj cpp - reach json -- --impact=empty --json; then + # the row array key of the JSON dialect is read off the answer, never assumed + key="$( python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); print(next((k for k,v in j.items() if isinstance(v,list) and v and isinstance(v[0],dict) and "n" in v[0]), "-"))' "$DIR/d_impj.out" 2>/dev/null )" + python3 "$DIR/amd.py" "$DIR/d_impj.out" json - "$key" >"$DIR/d_impj.rows" + expect P d_impj DrivePiece "$SP" +fi +if mcp d_mcp cpp s impact '"symbol":"empty"'; then expect P d_mcp DriveEval "$EV"; fi +if ans d_one cpp impact s xml -- --impact=string_piece.h:empty; then plain N d_one; fi + +# ── (E) --path endpoints ────────────────────────────────────────────────────────────────────────────────────────────── +echo "(E) C++ --path endpoints" +if ans e_un cpp path s xml -- --path=Clear,empty; then + [ "$( rootf e_un to_p )" = "-" ]; say $? "[P] e_un: unreached multi-definition to= carries no to_p" \ + "[P] e_un: reachable=0 with to_defs=2 still claims to_p=\"$( rootf e_un to_p )\"" + [ "$( rootf e_un from_p )" = "eval_env.h:4" ]; say $? "[N] e_un: the single-definition from= keeps from_p" \ + "[N] e_un: from_p='$( rootf e_un from_p )'" +fi +if ans e_re cpp path s xml -- --path=DrivePiece,empty; then + [ "$( rootf e_re to_p )" = "$SP" ]; say $? "[C] e_re: a reached multi-definition to= names the bound one ($SP)" \ + "[C] e_re: to_p='$( rootf e_re to_p )'" +fi +if ans e_sp cpp path s xml -- --path=PutInt,put; then + # the hop into put is one arm of a split call (Fmt::put(int) vs Fmt::put(const char*)): the row that ends the path says + # so, and the root does not claim the arm the BFS happened to take as the definition put was bound to + case "$( rowflags e_sp put )" in *prov=split*) ok "[P] e_sp: the split hop into a multi-definition to= carries prov=\"split\"" ;; + *) no "[P] e_sp: the hop into put is a split arm but carries '$( rowflags e_sp put )'" ;; esac + [ "$( rootf e_sp to_p )" = "-" ]; say $? "[P] e_sp: a to= bound only by a split hop carries no to_p" \ + "[P] e_sp: to_p=\"$( rootf e_sp to_p )\" claimed for a to= the path reached by a split arm" + [ "$( rootf e_sp from_p )" = "fmt.cc:6" ]; say $? "[N] e_sp: the single-definition from= keeps from_p" \ + "[N] e_sp: from_p='$( rootf e_sp from_p )'" +fi +if mcp e_mcp cpp s path_between '"from":"Clear","to":"empty"'; then + [ "$( rootf e_mcp to_p )" = "-" ]; say $? "[P] e_mcp: MCP path_between drops the unbound to_p too" \ + "[P] e_mcp: MCP to_p='$( rootf e_mcp to_p )'" +fi +if mcp e_mcps cpp s path_between '"from":"PutInt","to":"put"'; then + [ "$( rootf e_mcps to_p )" = "-" ]; say $? "[P] e_mcps: MCP path_between drops a to_p bound by a split hop" \ + "[P] e_mcps: MCP to_p='$( rootf e_mcps to_p )'" +fi + +# ── (F) the other languages: nfpm-06 (Go), entity identity in one file (Python, TypeScript, Java), JS, Ruby, hedged (Rust, ObjC) +echo "(F) Go / Python / TypeScript / Java / JavaScript / Ruby / Rust / ObjC" +if ans f_go go callers s xml -- --callers=Get; then + expect P f_go Package "deb/deb.go:7 files/files.go:4" + expect P f_go Describe "deb/deb.go:7" + expect P f_go ListOnly "files/files.go:4" + expect P f_go Stamp "modtime.go:4" + [ "$( groups f_go )" = 3 ]; say $? "[P] f_go: three groups" \ + "[P] f_go: rows=$( groups f_go ), want 3" + rowset C f_go 4 "Describe ListOnly Package Stamp" +fi +if mcp f_gomcp go calledBy find_referencing_symbols '"symbol":"Get"'; then + expect P f_gomcp Describe "deb/deb.go:7" + rowset C f_gomcp 4 "Describe ListOnly Package Stamp" +fi +if ans f_py py callers s xml -- --callers=render; then + expect P f_py draw_widget widgets.py:2 + expect P f_py draw_canvas widgets.py:6 + expect P f_py draw_both "widgets.py:2 widgets.py:6" + # draw_mixed calls Widget.render twice: w.render() (proven) and thing.render() (split between the two by name). The graph keeps + # ONE provenance per edge (resolve.h edgeProvenance: split outranks plain), so the Widget edge reads ~s — a hedge, never a claim + expect P f_py draw_mixed "widgets.py:2~s widgets.py:6~n" + rowset C f_py 4 "draw_both draw_canvas draw_mixed draw_widget" +fi +if ans f_ts ts callers s xml -- --callers=area; then + expect P f_ts circleArea shapes.ts:2; expect P f_ts squareArea shapes.ts:5; expect P f_ts bothAreas "shapes.ts:2 shapes.ts:5" + rowset C f_ts 3 "bothAreas circleArea squareArea" +fi +if ans f_java java callers s xml -- --callers=lookup; then + expect P f_java fromCache Store.java:2; expect P f_java fromRegistry Store.java:5; expect P f_java fromBoth "Store.java:2 Store.java:5" + rowset C f_java 3 "fromBoth fromCache fromRegistry" +fi +if ans f_js js callers s xml -- --callers=area; then + expect P f_js circleArea shapes.js:1; expect P f_js squareArea shapes.js:2 + rowset C f_js 2 "circleArea squareArea" +fi +if ans f_rb rb callers s xml -- --callers=area; then + expect P f_rb circle_area shapes.rb:2; expect P f_rb square_area shapes.rb:7 + rowset C f_rb 2 "circle_area square_area" +fi +if ans f_rs rs callers s xml -- --callers=iter; then + hedged P f_rs count_items "src/lib.rs:3 src/lib.rs:7" + hedged P f_rs count_books "src/lib.rs:3 src/lib.rs:7" + case "$( rowflags f_rs count_items )" in *via=name*) ok "[N] f_rs: the existing via=\"name\" row hedge stays" ;; *) no "[N] f_rs: via=\"name\" lost on count_items" ;; esac + rowset C f_rs 2 "count_books count_items" +fi +if ans f_objc objc callers s xml -- --callers=area; then + hedged P f_objc circleArea "Shapes.m:6 Shapes.m:12" + hedged P f_objc squareArea "Shapes.m:6 Shapes.m:12" + rowset C f_objc 2 "circleArea squareArea" +fi +if ans f_py1 py callers s xml -- --callers=draw_widget; then + # a single definition with no caller: the root, no rows — read the table count only + [ "$( groups f_py1 )" = 0 ]; say $? "[N] f_py1: single definition, no table" \ + "[N] f_py1: rows on a single definition" +fi + +# ── (H) the definition-table cap: 20 same-named definitions, 12 callers — the table lists what the page names ───────── +echo "(H) the definition-table cap (16): a generated 20-definition selector" +mkdir -p "$DIR/capfix" +python3 - "$DIR/capfix/caps.py" <<'PY' +import sys +out = [] +for k in range(20): + out.append('class C%02d:\n def go(self):\n return %d\n' % (k, k)) +for k in range(12): + out.append('def use_%02d():\n x = C%02d()\n return x.go()\n' % (k, k)) +open(sys.argv[1], 'w').write('\n'.join(out)) +PY +FIXSAVE="$FIX"; FIX="$DIR" +if ans h_all capfix callers s xml -- --callers=go; then + [ "$( groups h_all )" = 12 ] && [ "$( rootf h_all defs_listed )" = 12 ]; say $? "[P] h_all: 20 groups, 12 rows: the table lists the 12 the rows name, defs_listed=\"12\"" \ + "[P] h_all: rows=$( groups h_all ) defs_listed='$( rootf h_all defs_listed )', want 12 / 12" + invariant P h_all +fi +if ans h_p1 capfix callers s xml -- --callers=go --limit=5; then + [ "$( groups h_p1 )" = 5 ] && [ "$( rootf h_p1 defs_listed )" = 5 ]; say $? "[P] h_p1: page 1 (--limit=5) lists its 5 groups, defs_listed=\"5\"" \ + "[P] h_p1: rows=$( groups h_p1 ) defs_listed='$( rootf h_p1 defs_listed )', want 5 / 5" + invariant P h_p1 + nxt="$( rootf h_p1 next_offset )" + if [ "$nxt" = 5 ] && ans h_p2 capfix callers s xml -- --callers=go --limit=5 --offset="$nxt"; then + invariant P h_p2 + both="$( { awk -F'\t' '$1=="ROW" { print $2 }' "$DIR/h_p1.rows"; awk -F'\t' '$1=="ROW" { print $2 }' "$DIR/h_p2.rows"; } | sort | uniq -d | tr '\n' ' ' )" + [ -z "$both" ] && [ "$( nrows h_p2 )" = 5 ]; say $? "[C] h_p2: next_offset=$nxt pages on to 5 new rows, each attributed in its own table" \ + "[C] h_p2: page 2 rows=$( nrows h_p2 ), repeated from page 1: '$both'" + else no "[C] h_p1: next_offset='$nxt' (want 5) or the page-2 call failed"; fi +fi +# R2 (final review): above the cap, by-name ties are not enumerated. use_any reaches go by name alone: no of_def, via="name" +# is its hedge; use_mix holds a split arm into C05.go plus by-name ties: its split token and ONE bare ~n. The cut table then +# lists only the groups a printed token names (the three proven callers' and C05's). +python3 - "$DIR/capfix2/caps.py" <<'PY' +import os, sys +os.makedirs(os.path.dirname(sys.argv[1]), exist_ok=True) +out = [] +for k in range(20): + out.append('class C%02d:\n def go(self):\n return %d\n' % (k, k)) +for k in range(3): + out.append('def use_%02d():\n x = C%02d()\n return x.go()\n' % (k, k)) +out.append('def use_any(o):\n return o.go()\n') +out.append('def use_mix(o):\n y = C05()\n y.go()\n return o.go()\n') +open(sys.argv[1], 'w').write('\n'.join(out)) +PY +# cutinv TAG : on a cut table every row has of_def or is via="name", and every K resolves to a listed +cutinv(){ + local tag="$1" bad + bad="$( awk -F'\t' '$1=="ROW" { if ($3=="-" && $4 !~ /via=name/) print $2": no of_def and no via=name"; else if ($3 ~ /!/) print $2": token names no ("$3")" }' "$DIR/$tag.rows" )" + if [ "$( nrows "$tag" )" -gt 0 ] && [ -z "$bad" ]; then ok "[P] $tag: cut invariant — every row attributed or via=\"name\", every K in the table" + else no "[P] $tag: cut invariant broken: $( printf '%s' "$bad" | tr '\n' ';' | cut -c1-300 )"; fi +} +for form in xml json; do + t=h2_$form + if [ "$form" = json ]; then ans "$t" capfix2 callers callers json -- --callers=go --json; else ans "$t" capfix2 callers s xml -- --callers=go; fi || continue + expect P "$t" use_any - + case "$( rowflags "$t" use_any )" in *via=name*) ok "[P] $t: use_any (by name alone) keeps via=\"name\"" ;; *) no "[P] $t: use_any lost via=\"name\" ('$( rowflags "$t" use_any )')" ;; esac + expect P "$t" use_mix "caps.py:22~s ~n" + expect C "$t" use_00 caps.py:2 + { [ "$( groups "$t" )" = 4 ] && [ "$( rootf "$t" defs_listed )" = 4 ]; }; say $? "[P] $t: the cut table lists the 4 groups a printed token names, defs_listed=4" \ + "[P] $t: rows=$( groups "$t" ) defs_listed='$( rootf "$t" defs_listed )', want 4 / 4" + cutinv "$t" +done +if ans h2_imp capfix2 impact s xml -- --impact=go; then + expect P h2_imp use_any - + expect P h2_imp use_mix "caps.py:22~s ~n" + { [ "$( groups h2_imp )" = 4 ] && [ "$( rootf h2_imp defs_listed )" = 4 ]; }; say $? "[P] h2_imp: impact's cut table lists 4 groups, defs_listed=4" \ + "[P] h2_imp: rows=$( groups h2_imp ) defs_listed='$( rootf h2_imp defs_listed )', want 4 / 4" + cutinv h2_imp +fi +if ( cd "$DIR/capfix2" && "$BIN" . --no-cache --callers=go --legend=full ) >"$DIR/h2_full.out" 2>/dev/null; then + sed -n '1,/-->/p' "$DIR/h2_full.out" | grep -q 'bare ~n'; say $? "[P] h2_full: the full legend reads the cut table's bare ~n" \ + "[P] h2_full: no legend reading of the bare ~n token" + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$DIR/h2_full.out" 2>/dev/null; say $? "[C] h2_full: --legend=full well-formed" "[C] h2_full: --legend=full malformed" + fi + ( cd "$DIR/capfix2" && "$BIN" . --no-cache --callers=go ) 2>/dev/null | sed -n '1,/-->/p' | grep -q 'bare ~n'; say $? "[P] h2_cmp: the compact legend reads the bare ~n" \ + "[P] h2_cmp: the compact legend does not read the bare ~n" +else no "h2_full: --callers=go --legend=full failed"; fi +FIX="$FIXSAVE" + +# ── (J) R1 (final review): an untied declaration is dropped only when that leaves ONE group; a printed table lists it ────── +echo "(J) untied declarations on a printed table" +if ans j_u2 u2 callers s xml -- --callers=scan_tok; then + [ "$( groups j_u2 )" = 3 ]; say $? "[P] j_u2: three groups listed (definition, untied prototype, static namesake)" \ + "[P] j_u2: rows=$( groups j_u2 ), want 3 (an untied prototype vanished from a printed table)" + defbody P j_u2 prog2/main.c:2 1 + defbody C j_u2 prog1/util.c:2 0 + defbody C j_u2 prog3/other.c:2 0 + expect C j_u2 use_tok1 prog1/util.c:2 + expect C j_u2 use_tok3 prog3/other.c:2 +fi +if ans j_u2i u2 impact s xml -- --impact=scan_tok; then + [ "$( groups j_u2i )" = 3 ]; say $? "[P] j_u2i: impact lists the three groups" "[P] j_u2i: rows=$( groups j_u2i ), want 3" +fi +# the one-group sibling (u1 = prog1 + prog2): dropping the untied prototype leaves one group, so the answer stays plain +mkdir -p "$DIR/u1/prog1" "$DIR/u1/prog2"; cp "$FIX/u2/prog1/util.c" "$DIR/u1/prog1/"; cp "$FIX/u2/prog2/main.c" "$DIR/u1/prog2/" +FIXSAVE="$FIX"; FIX="$DIR" +if ans j_u1 u1 callers s xml -- --callers=scan_tok; then plain C j_u1; fi +FIX="$FIXSAVE" + +# ── (I) Q1 on --for: a body's row (and a hop's) whose edge is a resolver split arm carries prov="split", defined in the +# bundle's first-screen legend; a proven callee row does not ────────────────────────────────────────────────────────────────── +echo "(I) Q1: --for rows of a split call" +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=PutInt ) >"$DIR/i_for.out" 2>/dev/null && grep -q ']* prov="split"' "$DIR/i_for.out" | wc -l | tr -d ' ' )" + [ "$n_split" = 2 ]; say $? "[P] i_for: both put arms in PutInt's carry prov=\"split\"" \ + "[P] i_for: $n_split of 2 put rows carry prov=\"split\"" + sed 's/-->.*//' "$DIR/i_for.out" | grep -q 'prov=split'; say $? "[P] i_for: the first-screen legend defines prov=split" \ + "[P] i_for: prov=split is not defined in the --for header" +else no "i_for: --for=PutInt failed"; fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=UsesBoth ) >"$DIR/i_for2.out" 2>/dev/null && grep -q '/dev/null 2>&1; then + ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=PutInt --legend=full ) >"$DIR/i_forf.out" 2>/dev/null && xmllint --noout "$DIR/i_forf.out" 2>/dev/null + say $? "[C] i_forf: --for=PutInt --legend=full well-formed (the prov=split clause rides)" "[C] i_forf: --for=PutInt --legend=full malformed or failed" +fi + +# ── (G) well-formedness and determinism ─────────────────────────────────────────────────────────────────────────────── +echo "(G) well-formedness, determinism" +if command -v xmllint >/dev/null 2>&1; then + for t in a_cpp a_col a_sd b_cpp c_get d_imp d_rf e_un e_sp f_go h_p1; do + [ -s "$DIR/$t.out" ] || continue + xmllint --noout "$DIR/$t.out" 2>/dev/null; say $? "[C] $t: well-formed XML" \ + "[C] $t: malformed XML" + done + for v in "--callers=empty" "--callees=ParseDefault" "--impact=empty" "--safe-delete=empty" "--path=PutInt,put" "--callers=get" "--callers=empty --format=columnar"; do + # shellcheck disable=SC2086 + ( cd "$FIX/cpp" && "$BIN" . --no-cache $v --legend=full ) >"$DIR/g_full.out" 2>/dev/null && xmllint --noout "$DIR/g_full.out" 2>/dev/null; say $? "[C] g_full: $v --legend=full well-formed" \ + "[C] g_full: $v --legend=full malformed or failed" + done +else + printf ' SKIP xml well-formedness (no xmllint)\n' +fi +( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty ) >"$DIR/g_det.out" 2>/dev/null +cmp -s "$DIR/g_det.out" "$DIR/a_cpp.out"; say $? "[C] determinism: --callers=empty byte-identical run to run" \ + "[C] --callers=empty differs run to run" + +# [S] train 28 (for-body-slot x attrib-multidef): the --for body slot's rows label a split arm prov="split" as every +# other --for body does. "how does ManifestParser ParseDefault work" picks ParseDefault as the slot owner (pick="name"), and +# its CanonicalizePath call is the fixture's split call: the hop row already carried prov="split"; the slot body's two +# rows did not before the fix (the same arm read as a plain callee there). +( cd "$FIX/cpp" && "$BIN" . --no-cache --for="how does ManifestParser ParseDefault work" ) >"$DIR/s_slot.out" 2>/dev/null; s_rc=$? +s_counts="$( python3 -c ' +import re, sys +d = open( sys.argv[ 1 ], encoding="utf-8", errors="replace" ).read() +m = re.search( r"]*pick=\"name\"[^>]*>(.*?)", d, re.S ) +if not m: print( "none" ); sys.exit( 0 ) +rows = re.findall( r"]*>", m.group( 1 ) ) +legend = d.split( " row" in legend ) ) +' "$DIR/s_slot.out" )" +if [ "$s_rc" -ne 0 ] || [ "$s_counts" = none ] || [ -z "$s_counts" ]; then + no "[S] premise: the slot did not ride (rc=$s_rc, no )" +else + read -r s_all s_split s_leg <<<"$s_counts" + if [ "$s_all" -ge 1 ] && [ "$s_split" = "$s_all" ]; then ok "[S] the body slot labels its $s_all CanonicalizePath split-arm row(s) prov=\"split\"" + else no "[S] the body slot's CanonicalizePath rows: $s_split of $s_all carry prov=\"split\""; fi + if [ "$s_leg" = 1 ]; then ok "[S] the header defines the split reading the slot's rows use" + else no "[S] no split reading in the header of an answer that labels a split arm"; fi +fi + +# [S] MCP twin (CR pre-pass A): the MCP for tool's slot body labels the same two rows, and defines the reading once beside +# them (its header never does: splitLegendInHead is false there) — a duplicate or a missing definition goes red. +printf '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"how does ManifestParser ParseDefault work"}}}\n' \ + "$FIX/cpp" | perl -e 'alarm 120; exec @ARGV' "$BIN" --mcp 2>"$DIR/s_mcp.err" | tail -1 >"$DIR/s_mcp.out" +s_mcp="$( python3 -c ' +import json, re, sys +try: t = json.loads( open( sys.argv[ 1 ] ).read() )[ "result" ][ "content" ][ 0 ][ "text" ] +except Exception: print( "none" ); sys.exit( 0 ) +m = re.search( r"]*pick=\"name\"[^>]*>(.*?)", t, re.S ) +if not m: print( "none" ); sys.exit( 0 ) +rows = re.findall( r"]*>", m.group( 1 ) ) +print( len( rows ), sum( "prov=\"split\"" in r for r in rows ), t.count( "prov=\"split\" on a row" ) ) +' "$DIR/s_mcp.out" )" +if [ "$s_mcp" = none ] || [ -z "$s_mcp" ]; then no "[S] MCP premise: no slot body in the MCP for answer" +else + read -r m_all m_split m_def <<<"$s_mcp" + if [ "$m_all" -ge 1 ] && [ "$m_split" = "$m_all" ] && [ "$m_def" = 1 ]; then ok "[S] MCP for: the slot's $m_all CanonicalizePath rows carry prov=\"split\", the reading defined once" + else no "[S] MCP for: $m_split of $m_all rows carry prov=\"split\", the reading defined $m_def time(s) (want all, once)"; fi +fi +# [S2] (CR pre-pass A): a spent soft target serves the hops cut marker and no row carries prov="split", so the present-only +# split reading leaves the header with them (the lane cleared it on the spent-ceiling switch forbudget-soft retired); at 500 +# the hop row rides and so does the reading (the control). +for lg in compact full; do + for n in 100 500; do + ( cd "$FIX/cpp" && "$BIN" . --no-cache --for="how does ManifestParser ParseDefault work" --token-budget=$n --legend=$lg ) >"$DIR/s2_${n}_$lg.out" 2>/dev/null; s2_rc=$? + s2="$( python3 -c ' +import re, sys +d = open( sys.argv[ 1 ], encoding="utf-8", errors="replace" ).read() +head = d.split( " row" in head ) +rows = len( re.findall( r"]*prov=\"split\"", d ) ) +marker = int( bool( re.search( r"]*capped=\"1\"", d ) ) ) +print( rows, clause, marker )' "$DIR/s2_${n}_$lg.out" )" + read -r r2 c2 k2 <<<"$s2" + if [ "$s2_rc" -ne 0 ] || [ -z "$s2" ]; then no "[S2] $lg @$n: no answer (rc=$s2_rc)" + elif [ "$n" = 100 ]; then + if [ "$k2" = 1 ] && [ "$r2" = 0 ] && [ "$c2" = 0 ]; then ok "[S2] $lg @100: the hops cut marker, no split row, no split reading" + else no "[S2] $lg @100: marker=$k2 split rows=$r2 split reading=$c2 (want 1 0 0)"; fi + else + if [ "$r2" -ge 1 ] && [ "$c2" = 1 ]; then ok "[S2] $lg @500 control: $r2 split row(s) and the reading ride" + else no "[S2] $lg @500 control: split rows=$r2 reading=$c2 (want >=1 and 1)"; fi + fi + done +done + +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit $fail diff --git a/test/attribmultideffix/c/client.c b/test/attribmultideffix/c/client.c new file mode 100644 index 000000000..c4edb6bf8 --- /dev/null +++ b/test/attribmultideffix/c/client.c @@ -0,0 +1,2 @@ +#include "proto.h" +int LoseIt(void) { return lost(3); } diff --git a/test/attribmultideffix/c/helper_a.c b/test/attribmultideffix/c/helper_a.c new file mode 100644 index 000000000..134e76eec --- /dev/null +++ b/test/attribmultideffix/c/helper_a.c @@ -0,0 +1,3 @@ +/* a static (internal-linkage) definition: no prototype in another file declares it */ +static int helper(int x) { return x + 1; } +int UseA(void) { return helper(1); } diff --git a/test/attribmultideffix/c/helper_b.c b/test/attribmultideffix/c/helper_b.c new file mode 100644 index 000000000..17332e1bb --- /dev/null +++ b/test/attribmultideffix/c/helper_b.c @@ -0,0 +1,3 @@ +/* a same-named prototype in another file: a foreign declaration of the static helper in helper_a.c */ +int helper(int x); +int UseB(void) { return helper(2); } diff --git a/test/attribmultideffix/c/proto.c b/test/attribmultideffix/c/proto.c new file mode 100644 index 000000000..bbbbb01a6 --- /dev/null +++ b/test/attribmultideffix/c/proto.c @@ -0,0 +1,2 @@ +#include "proto.h" +int lost(int c) { return c - 1; } diff --git a/test/attribmultideffix/c/proto.h b/test/attribmultideffix/c/proto.h new file mode 100644 index 000000000..e27722f87 --- /dev/null +++ b/test/attribmultideffix/c/proto.h @@ -0,0 +1,4 @@ +#ifndef PROTO_H +#define PROTO_H +int lost(int c); +#endif diff --git a/test/attribmultideffix/cpp/detail.cc b/test/attribmultideffix/cpp/detail.cc new file mode 100644 index 000000000..3f26700be --- /dev/null +++ b/test/attribmultideffix/cpp/detail.cc @@ -0,0 +1,4 @@ +#include "detail.h" +namespace b { namespace detail { int tally(int x) { return x + 1; } } } +int CountA() { return a::detail::tally(1); } +int CountB() { return b::detail::tally(2); } diff --git a/test/attribmultideffix/cpp/detail.h b/test/attribmultideffix/cpp/detail.h new file mode 100644 index 000000000..4d64413d0 --- /dev/null +++ b/test/attribmultideffix/cpp/detail.h @@ -0,0 +1,3 @@ +#pragma once +// a foreign declaration: same name, same leaf scope `detail`, same parameter types — another scope chain +namespace a { namespace detail { int tally(int x); } } diff --git a/test/attribmultideffix/cpp/eval_env.h b/test/attribmultideffix/cpp/eval_env.h new file mode 100644 index 000000000..72c2313da --- /dev/null +++ b/test/attribmultideffix/cpp/eval_env.h @@ -0,0 +1,6 @@ +#pragma once +struct EvalString { + int parsed_; + void Clear() { parsed_ = 0; } + bool empty() const { return parsed_ == 0; } +}; diff --git a/test/attribmultideffix/cpp/eval_user.cc b/test/attribmultideffix/cpp/eval_user.cc new file mode 100644 index 000000000..43444e94e --- /dev/null +++ b/test/attribmultideffix/cpp/eval_user.cc @@ -0,0 +1,7 @@ +// impact depth 2: reaches EvalString::empty only, through UsesEval +#include "eval_env.h" +bool UsesEval(EvalString ev); +bool DriveEval() { + EvalString ev{0}; + return UsesEval(ev); +} diff --git a/test/attribmultideffix/cpp/fmt.cc b/test/attribmultideffix/cpp/fmt.cc new file mode 100644 index 000000000..74caa2c28 --- /dev/null +++ b/test/attribmultideffix/cpp/fmt.cc @@ -0,0 +1,11 @@ +#include "fmt.h" +void Fmt::put(int v) { (void)v; } +void Fmt::put(const char* s) { (void)s; } +void outer::run() {} +void outer::inner::run() {} +void PutInt(Fmt& f) { f.put(1); } +void PutStr(Fmt& f) { f.put("x"); } +void RunOuter() { outer::run(); } +void RunInner() { outer::inner::run(); } +int GetInt(const Box& b) { return b.get(); } +double GetDouble(const Box& b) { return b.get(); } diff --git a/test/attribmultideffix/cpp/fmt.h b/test/attribmultideffix/cpp/fmt.h new file mode 100644 index 000000000..89630d6aa --- /dev/null +++ b/test/attribmultideffix/cpp/fmt.h @@ -0,0 +1,19 @@ +#pragma once +struct Fmt { + void put(int v); + void put(const char* s); +}; +namespace outer { +void run(); +namespace inner { +void run(); +} +} +template struct Box { + T value; + T get() const { return value; } +}; +template <> struct Box { + int value; + int get() const { return value + 1; } +}; diff --git a/test/attribmultideffix/cpp/lexer.cc b/test/attribmultideffix/cpp/lexer.cc new file mode 100644 index 000000000..d390a0eab --- /dev/null +++ b/test/attribmultideffix/cpp/lexer.cc @@ -0,0 +1,2 @@ +#include "lexer.h" +bool Lexer::ReadPath(EvalString* out) { out->Clear(); return true; } diff --git a/test/attribmultideffix/cpp/lexer.h b/test/attribmultideffix/cpp/lexer.h new file mode 100644 index 000000000..b3d88953b --- /dev/null +++ b/test/attribmultideffix/cpp/lexer.h @@ -0,0 +1,5 @@ +#pragma once +#include "eval_env.h" +struct Lexer { + bool ReadPath(EvalString* out); +}; diff --git a/test/attribmultideffix/cpp/parser.cc b/test/attribmultideffix/cpp/parser.cc new file mode 100644 index 000000000..a2ac5f8f4 --- /dev/null +++ b/test/attribmultideffix/cpp/parser.cc @@ -0,0 +1,21 @@ +#include "parser.h" +#include "util.h" +#include "string_piece.h" +bool ManifestParser::ParseDefault(std::string* err) { + EvalString eval; + lexer_.ReadPath(&eval); + if (eval.empty()) return false; + std::string path = "x"; + unsigned long bits; + CanonicalizePath(&path, &bits); + return true; +} +bool UsesPiece(StringPiece sp) { + return sp.empty(); +} +bool UsesEval(EvalString ev) { + return ev.empty(); +} +bool UsesBoth(StringPiece sp, EvalString ev) { + return sp.empty() && ev.empty(); +} diff --git a/test/attribmultideffix/cpp/parser.h b/test/attribmultideffix/cpp/parser.h new file mode 100644 index 000000000..450629098 --- /dev/null +++ b/test/attribmultideffix/cpp/parser.h @@ -0,0 +1,6 @@ +#pragma once +#include "lexer.h" +struct ManifestParser { + Lexer lexer_; + bool ParseDefault(std::string* err); +}; diff --git a/test/attribmultideffix/cpp/piece_user.cc b/test/attribmultideffix/cpp/piece_user.cc new file mode 100644 index 000000000..ae40daf74 --- /dev/null +++ b/test/attribmultideffix/cpp/piece_user.cc @@ -0,0 +1,7 @@ +// impact depth 2: reaches StringPiece::empty only, through UsesPiece +#include "string_piece.h" +bool UsesPiece(StringPiece sp); +bool DrivePiece() { + StringPiece sp{"", 0}; + return UsesPiece(sp); +} diff --git a/test/attribmultideffix/cpp/std_user.cc b/test/attribmultideffix/cpp/std_user.cc new file mode 100644 index 000000000..6bc1aea2c --- /dev/null +++ b/test/attribmultideffix/cpp/std_user.cc @@ -0,0 +1,3 @@ +// ninja-07 confounder: a std::string member call named like the selector — never a proven row of either empty +#include +bool UsesStd(const std::string& s) { return s.empty(); } diff --git a/test/attribmultideffix/cpp/string_piece.h b/test/attribmultideffix/cpp/string_piece.h new file mode 100644 index 000000000..3947a7326 --- /dev/null +++ b/test/attribmultideffix/cpp/string_piece.h @@ -0,0 +1,7 @@ +#pragma once +#include +struct StringPiece { + const char* str_; + std::size_t len_; + bool empty() const { return len_ == 0; } +}; diff --git a/test/attribmultideffix/cpp/util.cc b/test/attribmultideffix/cpp/util.cc new file mode 100644 index 000000000..b30389320 --- /dev/null +++ b/test/attribmultideffix/cpp/util.cc @@ -0,0 +1,8 @@ +#include "util.h" +void CanonicalizePath(std::string* path, unsigned long* bits) { + unsigned long len = path->size(); + CanonicalizePath(&(*path)[0], &len, bits); +} +void CanonicalizePath(char* path, unsigned long* len, unsigned long* bits) { + *len = 0; *bits = 0; (void)path; +} diff --git a/test/attribmultideffix/cpp/util.h b/test/attribmultideffix/cpp/util.h new file mode 100644 index 000000000..f23e64abd --- /dev/null +++ b/test/attribmultideffix/cpp/util.h @@ -0,0 +1,4 @@ +#pragma once +#include +void CanonicalizePath(std::string* path, unsigned long* bits); +void CanonicalizePath(char* path, unsigned long* len, unsigned long* bits); diff --git a/test/attribmultideffix/go/deb/deb.go b/test/attribmultideffix/go/deb/deb.go new file mode 100644 index 000000000..5863ce496 --- /dev/null +++ b/test/attribmultideffix/go/deb/deb.go @@ -0,0 +1,16 @@ +package deb + +import "example.com/pk/files" + +type Info struct{ Name string } + +func (i *Info) Get(key string) string { return i.Name + key } + +func Package(i *Info) []string { + _ = i.Get("x") + return files.Get(i.Name) +} + +func Describe(i *Info) string { return i.Get("d") } + +func ListOnly(n string) []string { return files.Get(n) } diff --git a/test/attribmultideffix/go/files/files.go b/test/attribmultideffix/go/files/files.go new file mode 100644 index 000000000..b0b22c9e4 --- /dev/null +++ b/test/attribmultideffix/go/files/files.go @@ -0,0 +1,4 @@ +package files + +// Get returns the content list. +func Get(name string) []string { return []string{name} } diff --git a/test/attribmultideffix/go/go.mod b/test/attribmultideffix/go/go.mod new file mode 100644 index 000000000..eba449962 --- /dev/null +++ b/test/attribmultideffix/go/go.mod @@ -0,0 +1,2 @@ +module example.com/pk +go 1.21 diff --git a/test/attribmultideffix/go/modtime.go b/test/attribmultideffix/go/modtime.go new file mode 100644 index 000000000..f7e4bf519 --- /dev/null +++ b/test/attribmultideffix/go/modtime.go @@ -0,0 +1,6 @@ +package pk + +// Get returns a timestamp. +func Get() int64 { return 0 } + +func Stamp() int64 { return Get() } diff --git a/test/attribmultideffix/java/Store.java b/test/attribmultideffix/java/Store.java new file mode 100644 index 000000000..e8eea7faa --- /dev/null +++ b/test/attribmultideffix/java/Store.java @@ -0,0 +1,11 @@ +class Cache { + String lookup(String k) { return k; } +} +class Registry { + String lookup(String k) { return k + "!"; } +} +class Store { + String fromCache(Cache c) { return c.lookup("a"); } + String fromRegistry(Registry r) { return r.lookup("b"); } + String fromBoth(Cache c, Registry r) { return c.lookup("a") + r.lookup("b"); } +} diff --git a/test/attribmultideffix/js/shapes.js b/test/attribmultideffix/js/shapes.js new file mode 100644 index 000000000..ceec76ba9 --- /dev/null +++ b/test/attribmultideffix/js/shapes.js @@ -0,0 +1,5 @@ +class Circle { area() { return 3; } } +class Square { area() { return 4; } } +function circleArea() { const c = new Circle(); return c.area(); } +function squareArea() { const s = new Square(); return s.area(); } +module.exports = { circleArea, squareArea }; diff --git a/test/attribmultideffix/objc/Shapes.m b/test/attribmultideffix/objc/Shapes.m new file mode 100644 index 000000000..b712f5d39 --- /dev/null +++ b/test/attribmultideffix/objc/Shapes.m @@ -0,0 +1,15 @@ +#import +@interface Circle : NSObject +- (int)area; +@end +@implementation Circle +- (int)area { return 3; } +@end +@interface Square : NSObject +- (int)area; +@end +@implementation Square +- (int)area { return 4; } +@end +int circleArea(Circle* c) { return [c area]; } +int squareArea(Square* s) { return [s area]; } diff --git a/test/attribmultideffix/py/widgets.py b/test/attribmultideffix/py/widgets.py new file mode 100644 index 000000000..7cc038a26 --- /dev/null +++ b/test/attribmultideffix/py/widgets.py @@ -0,0 +1,25 @@ +class Widget: + def render(self): + return "w" + +class Canvas: + def render(self): + return "c" + +def draw_widget(): + w = Widget() + return w.render() + +def draw_canvas(): + c = Canvas() + return c.render() + +def draw_both(): + w = Widget() + c = Canvas() + return w.render() + c.render() + +def draw_mixed(thing): + w = Widget() + w.render() + return thing.render() diff --git a/test/attribmultideffix/rb/shapes.rb b/test/attribmultideffix/rb/shapes.rb new file mode 100644 index 000000000..36e240a96 --- /dev/null +++ b/test/attribmultideffix/rb/shapes.rb @@ -0,0 +1,18 @@ +class Circle + def area + 3 + end +end +class Square + def area + 4 + end +end +def circle_area + c = Circle.new + c.area +end +def square_area + s = Square.new + s.area +end diff --git a/test/attribmultideffix/rs/Cargo.toml b/test/attribmultideffix/rs/Cargo.toml new file mode 100644 index 000000000..89a1df393 --- /dev/null +++ b/test/attribmultideffix/rs/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bk" +version = "0.1.0" +edition = "2021" diff --git a/test/attribmultideffix/rs/src/lib.rs b/test/attribmultideffix/rs/src/lib.rs new file mode 100644 index 000000000..bfcbc88e1 --- /dev/null +++ b/test/attribmultideffix/rs/src/lib.rs @@ -0,0 +1,10 @@ +pub struct Book { pub items: Vec } +impl Book { + pub fn iter(&self) -> std::slice::Iter<'_, u32> { self.items.iter() } +} +pub struct Shelf { pub books: Vec } +impl Shelf { + pub fn iter(&self) -> std::slice::Iter<'_, Book> { self.books.iter() } +} +pub fn count_items(b: &Book) -> usize { b.iter().count() } +pub fn count_books(s: &Shelf) -> usize { s.iter().count() } diff --git a/test/attribmultideffix/ts/overloads.ts b/test/attribmultideffix/ts/overloads.ts new file mode 100644 index 000000000..a6385fc0b --- /dev/null +++ b/test/attribmultideffix/ts/overloads.ts @@ -0,0 +1,5 @@ +// overload signatures of DIFFERING arity before one implementation: one function, one definition group +export function pick(a: string): string; +export function pick(a: string, b: number): string; +export function pick(a: string, b?: number): string { return b === undefined ? a : a + b; } +export function usePick(): string { return pick("x") + pick("y", 2); } diff --git a/test/attribmultideffix/ts/shapes.ts b/test/attribmultideffix/ts/shapes.ts new file mode 100644 index 000000000..6ceac6cb1 --- /dev/null +++ b/test/attribmultideffix/ts/shapes.ts @@ -0,0 +1,19 @@ +export class Circle { + area(): number { return 3; } +} +export class Square { + area(): number { return 4; } +} +export function circleArea(): number { + const c = new Circle(); + return c.area(); +} +export function squareArea(): number { + const s = new Square(); + return s.area(); +} +export function bothAreas(): number { + const c = new Circle(); + const s = new Square(); + return c.area() + s.area(); +} diff --git a/test/attribmultideffix/u2/prog1/util.c b/test/attribmultideffix/u2/prog1/util.c new file mode 100644 index 000000000..0e3d88e72 --- /dev/null +++ b/test/attribmultideffix/u2/prog1/util.c @@ -0,0 +1,3 @@ +/* R1 (final review): a definition, an untied prototype in another program, a static namesake in a third */ +int scan_tok(const char *s) { return s[0]; } +int use_tok1(void) { return scan_tok("a"); } diff --git a/test/attribmultideffix/u2/prog2/main.c b/test/attribmultideffix/u2/prog2/main.c new file mode 100644 index 000000000..0e0625d6a --- /dev/null +++ b/test/attribmultideffix/u2/prog2/main.c @@ -0,0 +1,3 @@ +/* a prototype with no #include that ties it to prog1/util.c: the identity rule can only say Unproven */ +int scan_tok(const char *s); +int use_tok2(void) { return scan_tok("b"); } diff --git a/test/attribmultideffix/u2/prog3/other.c b/test/attribmultideffix/u2/prog3/other.c new file mode 100644 index 000000000..5f03c505d --- /dev/null +++ b/test/attribmultideffix/u2/prog3/other.c @@ -0,0 +1,3 @@ +/* a static (internal-linkage) namesake: another function */ +static int scan_tok(const char *s) { return 7; } +int use_tok3(void) { return scan_tok("c"); } diff --git a/test/budgetpolicycheck.sh b/test/budgetpolicycheck.sh index 81c0c46af..8efa5a752 100755 --- a/test/budgetpolicycheck.sh +++ b/test/budgetpolicycheck.sh @@ -274,7 +274,7 @@ echo "=== (D) over_ceiling= is 1 exactly when est_tokens exceeds a ceiling named # named on that root, and whether it says over_ceiling. Comments are stripped: a legend DEFINES these names # and a definition is not a measurement. Prints nothing when the document names no ceiling at all. root_nums() { - python3 - "$1" <<'PY_EOF' + python3 - "$1" "${2:-}" <<'PY_EOF' import re, sys t = re.sub( r"", "", open( sys.argv[1], encoding = "utf-8", errors = "replace" ).read(), flags = re.S ) # XML dialects put every root attribute in the first start-tag; --recall's is a PROSE header line, and its @@ -289,24 +289,40 @@ def num( name ): g = re.search( r'\b%s="?(\d+)"?' % name, root ) return int( g.group( 1 ) ) if g else None est = num( "est_tokens" ) -ceils = [ c for c in ( num( "max_tokens" ), num( "budget_tokens" ), num( "budget" ) ) if c ] +# forbudget-soft: on a SOFT lens (argv[2] == "soft": an explicit budget on --for / --pack-task / --from-trace) budget_tokens= is a +# target, not a ceiling — only max_tokens= is one there. The target must still be NAMED (arm (B)'s echo): a missing one is a miss. +soft = len( sys.argv ) > 2 and sys.argv[ 2 ] == "soft" +if soft and num( "budget_tokens" ) is None: + print( "NOBUDGET" ); raise SystemExit +ceils = [ c for c in ( num( "max_tokens" ), None if soft else num( "budget_tokens" ), None if soft else num( "budget" ) ) if c ] +if soft and est is not None and not ceils: + print( "%d 0 %d" % ( est, 1 if re.search( r'\bover_ceiling="?1"?', root ) else 0 ) ); raise SystemExit over = 1 if re.search( r'\bover_ceiling="?1"?', root ) else 0 if est is None or not ceils: raise SystemExit print( "%d %d %d" % ( est, min( ceils ), over ) ) PY_EOF } -label_probe() { # label_probe LABEL ARGS... +label_probe() { # label_probe LABEL ARGS... (LABEL starting "soft:" = a soft-target lens, see root_nums) local label="$1"; shift + local mode="" + case "$label" in soft:*) mode="soft"; label="${label#soft:}";; esac "$BIN" . "$@" >"$TMP/lab.out" 2>/dev/null - local nums; nums="$( root_nums "$TMP/lab.out" )" - if [ -z "$nums" ]; then - no "(D) $label: the root names no ceiling and no price — arm (B)'s subject vanished, fix this probe" + local nums; nums="$( root_nums "$TMP/lab.out" $mode )" + if [ -z "$nums" ] || [ "$nums" = "NOBUDGET" ]; then + no "(D) $label: the root names no ceiling/target and no price — arm (B)'s subject vanished, fix this probe" return fi # shellcheck disable=SC2086 set -- $nums local est="$1" ceil="$2" over="$3" + if [ "$mode" = soft ] && [ "$ceil" = 0 ]; then + # forbudget-soft (rule 4, the replacement for the by-design break): a soft target is never "over" — no label, + # whatever the price; budget_tokens= still names it (checked in root_nums) + if [ "$over" = 0 ]; then ok "(D) $label: soft target, est=$est — no over_ceiling (budget_tokens= names a target, not a ceiling)" + else no "(D) $label: over_ceiling=\"1\" on a soft target (est=$est)"; fi + return + fi if [ "$est" -gt "$ceil" ] && [ "$over" = 0 ]; then no "(D) $label: root prices $est tokens against a ceiling of $ceil and says NOTHING (over_ceiling absent)" elif [ "$est" -le "$ceil" ] && [ "$over" = 1 ]; then @@ -317,13 +333,15 @@ label_probe() { # label_probe LABEL ARGS... } # Every budgeted verb, through BOTH front doors where it has two, at a ceiling that BINDS and one that does # not — the pair is the point: a verb that labels everything passes the first half and fails the second. -label_probe "--for --token-budget=600" '--for=pagerank power iteration' --token-budget=600 -label_probe "--for --token-budget=1600" '--for=pagerank power iteration' --token-budget=1600 -label_probe "--for --token-budget=8000" '--for=pagerank power iteration' --token-budget=8000 -label_probe "--pack-task --token-budget=600" '--pack-task=pagerank power iteration' --token-budget=600 -label_probe "--pack-task --token-budget=8000" '--pack-task=pagerank power iteration' --token-budget=8000 -label_probe "--from-trace --token-budget=200" "--from-trace=$TMP/trace.txt" --token-budget=200 -label_probe "--from-trace --token-budget=8000" "--from-trace=$TMP/trace.txt" --token-budget=8000 +# forbudget-soft (owner decision 2026-10-14): the task lenses' explicit --token-budget is a SOFT target ("soft:" probes: +# never labelled, still named); the family law above holds for every hard ceiling (max_tokens=, --handoff, --recall). +label_probe "soft:--for --token-budget=600" '--for=pagerank power iteration' --token-budget=600 +label_probe "soft:--for --token-budget=1600" '--for=pagerank power iteration' --token-budget=1600 +label_probe "soft:--for --token-budget=8000" '--for=pagerank power iteration' --token-budget=8000 +label_probe "soft:--pack-task --token-budget=600" '--pack-task=pagerank power iteration' --token-budget=600 +label_probe "soft:--pack-task --token-budget=8000" '--pack-task=pagerank power iteration' --token-budget=8000 +label_probe "soft:--from-trace --token-budget=200" "--from-trace=$TMP/trace.txt" --token-budget=200 +label_probe "soft:--from-trace --token-budget=8000" "--from-trace=$TMP/trace.txt" --token-budget=8000 label_probe "--from-trace --max-tokens=200" "--from-trace=$TMP/trace.txt" --max-tokens=200 label_probe "--from-trace --max-tokens=8000" "--from-trace=$TMP/trace.txt" --max-tokens=8000 label_probe "--handoff --token-budget=100" --handoff --token-budget=100 diff --git a/test/builtinbindcheck.sh b/test/builtinbindcheck.sh index 3c504faa4..fcc277bd6 100755 --- a/test/builtinbindcheck.sh +++ b/test/builtinbindcheck.sh @@ -202,11 +202,21 @@ for spec in "edit-check|--edit-check=mono/py/e/pool.py:popitem" "safe-delete|--s no "(R) $flag: no declined_calls=\"1\" or no definition: ${R:-no <$tag> root}" fi done +# Re-anchored by design (lane declined-sites, gates review R11): this arm pinned risk="none-found" beside declined_calls=, +# qualified by a legend sentence. A definition with no caller and no use that a declined call could have meant now reads +# risk="possible-callers" and the declined call is listed as a row; the qualifying-sentence assert is kept as the twin: +# the risk value's own sentence still says it is not a safety reading. rw --safe-delete=mono/py/e/pool.py:popitem --legend=full >"$TMP/sd.xml" -if [ "$( attr "$( root_tag "$TMP/sd.xml" safe-delete )" risk )" = none-found ] && legend_of "$TMP/sd.xml" | grep -q 'none-found beside declined_calls= is not a safety reading'; then - ok "(R) --safe-delete: risk=none-found is qualified by the declined_calls= sentence" +SDR="$( root_tag "$TMP/sd.xml" safe-delete )" +if [ "$( attr "$SDR" risk )" = possible-callers ] && [ "$( attr "$SDR" declined_calls )" = 1 ] && grep -q ' row" else - no "(R) --safe-delete: risk=none-found stands unqualified beside declined_calls=" + no "(R) --safe-delete: a definition only a declined call could mean should read risk=possible-callers with its row: ${SDR:-no root}" +fi +if legend_of "$TMP/sd.xml" | grep -q 'possible-callers: .*not a safety reading'; then + ok "(R) --safe-delete: risk=possible-callers is qualified in the legend (not a safety reading)" +else + no "(R) --safe-delete: risk=possible-callers stands unqualified in the legend" fi QD="$TMP/qd"; rm -rf "$QD"; mkdir -p "$QD"; cp -R mono "$QD/" 2>/dev/null ( cd "$QD" && git init -q && git -c user.name=t -c user.email=t@t add -A && git -c user.name=t -c user.email=t@t commit -qm base ) >/dev/null 2>&1 diff --git a/test/bundleidcheck.sh b/test/bundleidcheck.sh index b99fb460e..f9ad2a17c 100755 --- a/test/bundleidcheck.sh +++ b/test/bundleidcheck.sh @@ -15,8 +15,10 @@ # for the same symbol (one canonical id form across lenses — the --plan-lanes prerequisite). # * --for and --pack-task agree on in= for every symbol both bundles name (never 0-vs-real). # * no bundle asserts in="0" without a fan-in source: --from-trace OMITS the attribute instead. -# * the added attributes are ACCOUNTED for, not overflowed — a --token-budget=N run still fits its own -# reported byte budget, and --for's est_tokens still matches the bytes actually delivered. +# * the added attributes are ACCOUNTED for, not overflowed — a --token-budget=N run's SECTIONS still fit its own +# reported ceiling (forbudget-soft: an explicit --pack-task --token-budget is a soft target — the header is never +# charged and never labelled over_ceiling; the default --pack-task keeps the hard fit-or-label rule), and --for's +# est_tokens still matches the bytes actually delivered. # * determinism (run twice → byte-identical) + well-formed XML on all three verbs. # Mutation-tested: the n=-presence assertion is checked to actually FAIL on a row stripped of n=. # Exits non-zero on any failure. @@ -198,16 +200,20 @@ for B in 600 1200 6000; do # label is the ladder's colon note (or the root attribute), never the bare word. OVERLBL="$( sed -n 's/.*.*/\1/p' "$TMP/b.$B" | grep -cE 'over_ceiling(="1"|:)' )" [ "${OVERLBL:-0}" -gt 0 ] && OVERLBL=1 || OVERLBL=0 - [ "$BYTES" -gt "$CEILTOL" ] && OVERREAL=1 || OVERREAL=0 - if [ -z "$BUDGET" ] || [ -z "$CEIL" ]; then - no "--token-budget=$B: header reported no budget/ceiling — the ledger is unreadable" - elif [ "$OVERREAL" = "$OVERLBL" ]; then - if [ "$OVERREAL" = 0 ]; then ok "--token-budget=$B: delivered $BYTES B <= ceiling $CEIL B * 1.15 first-entry tolerance (budget $BUDGET B), no over_ceiling claimed" - else ok "--token-budget=$B: delivered $BYTES B past ceiling $CEIL B * 1.15 and SAYS over_ceiling (the ladder's honest rung)"; fi - elif [ "$OVERREAL" = 1 ]; then - no "--token-budget=$B: delivered $BYTES B OVERFLOWS ceiling $CEIL B even at the 1.15 single-entry tolerance, with NO over_ceiling label" + grep -qE '^]* over_ceiling=' "$TMP/b.$B" && OVERLBL=1 + # forbudget-soft (rule 4): an EXPLICIT --pack-task --token-budget is a SOFT target. The biconditional above (whole document + # <= ceiling*1.15 iff no over_ceiling) priced the header against a hard ceiling; it is replaced by NOLBL (never over_ceiling= + # / an "over_ceiling:" note on an explicit budget) + ST (the SECTIONS — everything past the root and its header comments — + # fit the ledger's ceiling*1.15: the quota cascade is unchanged, the header is never charged) + the legend names the soft + # target. RED on base (d4325a8c): 600/1200 carry over_ceiling="1" and a legend without "soft token target". The hard twin + # (the DEFAULT --pack-task keeps fit-or-label) follows the loop. + PAY="$( python3 -c 'import re,sys; s=open(sys.argv[1],"rb").read(); m=re.match(rb"]*>(?:)*",s,re.S); print(len(s)-m.end() if m else "")' "$TMP/b.$B" 2>/dev/null )" + if [ -z "$BUDGET" ] || [ -z "$CEIL" ] || [ -z "$PAY" ]; then + no "--token-budget=$B: header reported no budget/ceiling (or no root to split the sections off) — the ledger is unreadable" + elif [ "$OVERLBL" = 0 ] && [ "$PAY" -le "$CEILTOL" ] && grep -q 'soft token target' "$TMP/b.$B"; then + ok "--token-budget=$B: soft target — sections $PAY B <= ceiling $CEIL B * 1.15 (document $BYTES B, header uncharged), no over_ceiling, legend names the soft target" else - no "--token-budget=$B: delivered $BYTES B FITS ceiling $CEIL B * 1.15 yet the header claims over_ceiling" + no "--token-budget=$B: soft target broken — over_ceiling=$OVERLBL (must be 0), sections $PAY B vs ceiling*1.15 $CEILTOL B, soft-target legend $( grep -c 'soft token target' "$TMP/b.$B" )" fi # the truncation ledger must still reconcile: every "kept X of Y" has X <= Y python3 - "$TMP/b.$B" <<'PY' || no "--token-budget=$B: truncation ledger does not reconcile" @@ -218,6 +224,19 @@ sys.exit( 1 if bad else 0 ) PY done [ $fail -eq 0 ] && ok "truncation ledgers reconcile (kept <= total on every section)" +# (twin) the DEFAULT --pack-task (no --token-budget) keeps its HARD ceiling: the biconditional this arm used to state for every +# budget — the document fits ceiling*1.15 iff it carries no over_ceiling label (a fitting answer that claims over_ceiling fails too). +"$BIN" "$FIX" --no-cache --pack-task="$TASK" --legend=full >"$TMP/b.def" 2>/dev/null +D_BYTES="$( wc -c < "$TMP/b.def" | tr -d ' ' )"; D_CEIL="$( grep -o 'ceiling [0-9]*' "$TMP/b.def" | head -1 | tr -dc '0-9' )" +D_LBL="$( sed -n 's/.*.*/\1/p' "$TMP/b.def" | grep -cE 'over_ceiling(="1"|:)' )" +grep -qE '^]* over_ceiling="1"' "$TMP/b.def" && D_LBL=1 +if [ -z "$D_CEIL" ] || [ -z "$D_BYTES" ]; then + no "(twin) default --pack-task: header reported no ceiling — the hard ledger is unreadable" +elif { [ "$D_BYTES" -le $(( D_CEIL * 115 / 100 )) ] && [ "${D_LBL:-0}" -eq 0 ]; } || { [ "$D_BYTES" -gt $(( D_CEIL * 115 / 100 )) ] && [ "${D_LBL:-0}" -gt 0 ]; }; then + ok "(twin) default --pack-task: hard ceiling honest — $D_BYTES B vs ceiling $D_CEIL B * 1.15, over_ceiling label=${D_LBL:-0}" +else + no "(twin) default --pack-task: $D_BYTES B vs ceiling $D_CEIL B * 1.15 with over_ceiling label=${D_LBL:-0} — fit-or-label broken on the hard default" +fi # ── 5b) W3FIX M1 — the FIXTURE BLIND SPOT the arm above had: a THREE-WORD task ────────────────────────── # $TASK is "compute perimeter distance" — 26 bytes. The header echoes the task TWICE (verbatim in task=, and @@ -234,17 +253,24 @@ else "$BIN" "$FIX" --no-cache --pack-task="$LONGTASK" --token-budget=$B --legend=full >"$TMP/lt.$B" 2>/dev/null LT_BYTES="$( wc -c < "$TMP/lt.$B" | tr -d ' ' )" LT_CEIL="$( grep -o 'ceiling [0-9]*' "$TMP/lt.$B" | head -1 | tr -dc '0-9' )" - # the disclosure lives in the header COMMENT — read only up to the first "-->" so a body cannot supply it - LT_HDR="$( head -c 6000 "$TMP/lt.$B" | sed -e 's/-->.*//' )" - if [ -z "$LT_CEIL" ]; then - no "5b --token-budget=$B (320-byte task): header reported no ceiling — the ledger is unreadable" - elif [ "$LT_BYTES" -le $(( LT_CEIL * 115 / 100 )) ]; then - ok "5b --token-budget=$B (320-byte task): delivered $LT_BYTES B <= ceiling $LT_CEIL B * 1.15" - elif printf '%s' "$LT_HDR" | grep -q 'over_ceiling'; then - ok "5b --token-budget=$B (320-byte task): $LT_BYTES B over ceiling $LT_CEIL B and DISCLOSES over_ceiling" - else - no "5b --token-budget=$B (320-byte task): $LT_BYTES B past ceiling $LT_CEIL B * 1.15 in SILENCE" - fi + # forbudget-soft (rule 4): "fit ceiling*1.15 or SAY over_ceiling" is gone on an explicit budget (a soft target labels + # nothing; the old grep -q 'over_ceiling' then passed on the legend's own "no over_ceiling" words). Replaced by HL + NOLBL + # + the floor: the 320-byte task rides VERBATIM in task= and in the header echo, no clause is dropped "(ceiling)", no + # task_echo: dropped, no over_ceiling= / "over_ceiling:" note, a ranking section with >= 1 row ships, and the sections + # fit ceiling*1.15. RED on base (d4325a8c: over_ceiling="1" + the "over_ceiling: the header floor" note). + LT_HDR="$( python3 -c 'import re,sys; s=open(sys.argv[1],encoding="utf-8").read(); m=re.match(r"]*>(?:)*",s,re.S); print(m.group(0) if m else "")' "$TMP/lt.$B" 2>/dev/null )" + LT_PAY="$( python3 -c 'import re,sys; s=open(sys.argv[1],"rb").read(); m=re.match(rb"]*>(?:)*",s,re.S); print(len(s)-m.end() if m else "")' "$TMP/lt.$B" 2>/dev/null )" + lt_bad="" + [ -n "$LT_CEIL" ] && [ -n "$LT_PAY" ] || lt_bad="$lt_bad; no ceiling ledger / no root (premise)" + printf '%s' "$LT_HDR" | grep -qF "]* over_ceiling=' && lt_bad="$lt_bad; over_ceiling on a soft target" + grep -qE ']*> budget_tokens, and the BYTE comparison -# the ladder's last rung is — the document past ceilingAllowanceBytes( budget ) = budget x 2.36 x 1.15 -# (serialize.h). The second is deliberately WIDER than the first, so "over_ceiling implies est > budget" would -# be a false invariant. The honest one, which is what arm (4) sweeps, is the disjunction: the label may ride -# only on a document that is over one of the two ceilings its own root states, in numbers a reader can check. +# forbudget-soft (rule 4, owner decisions 2026-10-14) MOVED WHERE THE VERDICTS LIVE, and this gate follows them +# rather than the old premise "a tight --token-budget emits over_ceiling". An explicit --token-budget on --for, +# --pack-task and --from-trace is now a SOFT target: there is no over_ceiling for budget_tokens at all, and the +# --for header ladder (and with it the last-rung notes this gate used to impersonate) is gone. The verdicts that +# still exist, and so the surfaces the forgery class can still reach: +# · the --for RUNAWAY GUARD (serialize.h kForRunawayGuardBytes / forRunawayGuardNote): over_ceiling="1" only +# from the structural row-drop cut (verbs_for.h forGuardCut = forSigsCut.rowDropCut) — e.g. --pack-top-n=400; +# · --max-tokens on --for --detail (the body ceiling, est_tokens > max_tokens); +# · the DEFAULT --pack-task (no flag, 6000 tokens): its hard ceiling ladder + over_ceiling are unchanged; +# · --from-trace --max-tokens (est_tokens > max_tokens). +# On each: the FORGED task (the verdict's own clause, verbatim) and a PLAIN twin (the same words, the marker +# punctuation removed, so the ranking is the same) must get the SAME verdict and the SAME shape — and where the +# verdict is real (the guard cut, a tight --max-tokens, a header floor past 6000 tokens) BOTH carry it, so a +# green "no forged label" cannot come from a deleted attribute. On the soft lenses the arms are now +# FORGE + NOLBL: forged vs plain give identical shown/total/next=/rows, and neither is labelled. +# +# WHAT IS ASSERTED, per arm (codes from the lane brief: FORGE, NOLBL): +# (0)/(0b)/(0c)/(0d) the probes are LIVE: each forged clause is what the code emits / splices today; +# (1)/(6) the forged text reaches the emitted document (the injection vector is exercised); +# (2) wide soft budget, (2b) default regime, (3) tight soft budget — --for: FORGE + NOLBL; +# (3g) the guard cut: forged and plain BOTH labelled, identical shape (the verdict still fires when real); +# (3m) --for --detail --max-tokens: forged vs plain identical verdict at a wide and a tight ceiling; +# (4) the soft sweep over both lenses: FORGE + NOLBL at every point (counted, never vacuous); +# (7)/(8) --pack-task wide/tight soft: FORGE + NOLBL; (8d) the DEFAULT --pack-task: FORGE, and a real label; +# (8t) --from-trace --max-tokens: FORGE at a wide and a tight ceiling; +# (5) the source-shape recurrence guard; (9) the map's unmeasured --max-tokens fit. # # bash test/ceilingverdictcheck.sh # build/ripwire # bash test/ceilingverdictcheck.sh build_base/ripwire # or RIPWIRE_BIN=… — both seams honored @@ -54,42 +73,43 @@ ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write th no(){ printf ' FAIL %s\n' "$*"; fail=1; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "ceilingverdictcheck: python3 is required (shape comparison)"; exit 2; } CORPUS="$ROOT/src" [ -d "$CORPUS" ] || { echo "missing corpus $CORPUS"; exit 2; } echo "ceilingverdictcheck: BIN=$BIN" -# The forged marker is the ladder's LAST-RUNG note, verbatim. Pinned here as a literal rather than parsed out -# of the C++ (it is a two-line string concatenation), with a PRESENCE GUARD below so a re-worded note reds -# this gate instead of silently turning the probe into a task that forges nothing (trap: a vanishing probe -# target). Both halves are checked, because the source splits the constant between them. -MARK_A='over_ceiling= is 1 on the root: the header floor (verbatim task echo + fixed legend) exceeds this budget' -MARK_B='- no payload left to trim]' -MARK="[$MARK_A $MARK_B" -FORGED="serialize the header $MARK" -PLAIN='serialize the header ranking' +# THE FORGED MARKERS — each verdict's own clause, verbatim, pinned as literals with a LIVENESS guard (arm 0*) +# so a re-worded clause reds this gate instead of silently turning a probe into a task that forges nothing. +# --for: the runaway guard's reading (serialize.h forRunawayGuardNote), the one over_ceiling a default --for root +# can still carry. Its liveness is checked on the EMITTED guard document, not the source (the value is spliced in). +# The guard's byte value is spliced into the clause, so it is read from src/serialize.h (a stale binary then reds (0)). +GUARD_BYTES="$( sed -n 's/^inline constexpr [a-z:_ ]*kForRunawayGuardBytes *= *\([0-9]*\);.*/\1/p' "$ROOT/src/serialize.h" | head -1 )" +GUARD_NOTE="[over_ceiling=1: the ranked head passed the runaway guard of ${GUARD_BYTES:-MISSING} B (header plus ranked payload), so the rows past it are cut and sigs next= serves them all; on a default-size head this is a bug signal, not a budget (a wide pack-top-n head can reach it too)]" +FORGED="serialize the header $GUARD_NOTE" +# the PLAIN twin: every marker character replaced by a space, byte for byte (the ranker reads words, so both rank alike — +# arms (2b)/(3g) prove it by comparing the served rows), with no substring a marker sniff could match. Same LENGTH on +# purpose: the default text tier and the continuation's budget are charged with the header, which echoes the task, so a +# shorter twin would move docs_dropped= / next= for a reason that is not the marker. +plainOf(){ printf '%s' "$1" | sed -E 's/[^A-Za-z0-9 ]/ /g'; } +PLAIN="$( plainOf "$FORGED" )" +# --for --detail --max-tokens: the legend clause that defines the body-ceiling label (serialize.h kOverCeilingMaxTokensLegend) +MT_NOTE='over_ceiling=1 says est_tokens exceeds max_tokens' +MT_FORGED="serialize the header [$MT_NOTE]" +MT_PLAIN="$( plainOf "$MT_FORGED" )" # --pack-task's own last-rung note, same role, its own punctuation (packtask.h kNotes). The forgeable part is # the marker WORD, which is all the old sniff looked for — so the probe task carries only that. PT_MARK='over_ceiling:' PT_FORGED="why does $PT_MARK fire on this root" -PT_PLAIN='why does over ceiling fire on this root' - -# (0) the probe is live: the note this task impersonates is still the note the code splices. +PT_PLAIN="$( plainOf "$PT_FORGED" )" +# --from-trace: the named-ceiling last-rung note (tracelocus.h kNotesNamedCeiling), up to its first apostrophe (an +# attribute escapes it, and grep -F reads the raw bytes). The trace SOURCE is what this lens echoes (task=, src=), so +# the forged text rides the trace file's NAME. +TR_NOTE='[over_ceiling: the header floor (verbatim src echo + fixed legend) plus the innermost frame' SRC="$ROOT/src/verbs_for.h" PSRC="$ROOT/src/packtask.h" -if grep -qF -e "$MARK_A" "$SRC" && grep -qF -e "$MARK_B" "$SRC"; then # -e: MARK_B starts with '-' - ok "(0) the forged marker is still the ladder's last-rung note in src/verbs_for.h (the probe can forge)" -else - no "(0) the last-rung note in src/verbs_for.h no longer matches this gate's MARK_A/MARK_B — re-pin them, or the forgery probe is inert and every arm below passes for the wrong reason" -fi -if grep -qF -e "| $PT_MARK the header floor" "$PSRC"; then - ok "(0b) '$PT_MARK' is still the marker in --pack-task's last-rung note in src/packtask.h (the pack-task probe can forge)" -else - no "(0b) src/packtask.h's last-rung note no longer spells '$PT_MARK' — re-pin PT_MARK, or arms (6)-(8) prove nothing" -fi +TSRC="$ROOT/src/tracelocus.h" -run(){ "$BIN" "$CORPUS" --no-cache --token-budget="$1" --for="$2" 2>/dev/null; } -runPack(){ "$BIN" "$CORPUS" --no-cache --token-budget="$1" --pack-task="$2" 2>/dev/null; } attr(){ grep -o " $2=\"[0-9]*\"" <<< "$1" | head -1 | tr -cd '0-9'; } # leading space: est_tokens must not match inside a longer name # THE LABEL IS A ROOT ATTRIBUTE, so every arm reads it off the ROOT ELEMENT and not off the document. Both # lenses echo the caller's task into an XML COMMENT with no quote escaping, so `over_ceiling="1"` typed into a @@ -99,146 +119,257 @@ attr(){ grep -o " $2=\"[0-9]*\"" <<< "$1" | head -1 | tr -cd '0-9'; } # leadin rootOf(){ grep -o ']*>' "$1" | head -1; } hasLabel(){ rootOf "$1" | grep -c 'over_ceiling="1"' || true; } -WIDE=100000 -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --for="$FORGED" > "$TMP/forged.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --for="$PLAIN" > "$TMP/plain.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget=300 --for="$FORGED" > "$TMP/forged.tight" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --pack-task="$PT_FORGED" > "$TMP/pt.forged.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --pack-task="$PT_PLAIN" > "$TMP/pt.plain.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget=300 --pack-task="$PT_FORGED" > "$TMP/pt.forged.tight" 2>/dev/null +# THE SHAPE a verdict could be forged into: the root's posture attributes, every windowed section's +# shown/total/capped/next_offset/past_target/docs_dropped and its next= (the task text replaced by TASK, since the +# continuation re-asks the caller's own task), the counts, and the served rows in rank order. Two +# documents whose tasks differ only in marker punctuation must print the SAME shape. Exit 3 = no root. +SHAPE="$TMP/shape.py" +cat > "$SHAPE" <<'PY_EOF' +import html, re, sys +s = open( sys.argv[1], encoding = 'utf-8', errors = 'replace' ).read() +task = sys.argv[2] +s = re.sub( r'', '', s, flags = re.S ) # a body's text is not the shape +s = re.sub( r'', '', s, flags = re.S ) # nor is the legend (it echoes the task) +root = re.search( r']*)>', s ) +if root is None: sys.exit( 3 ) +def attrs( text ): return dict( re.findall( r'(\w+)="([^"]*)"', text ) ) +def norm( v ): + v = html.unescape( v ) + return v.replace( task, 'TASK' ) if task else v +ra = attrs( root.group( 1 ) ) +print( 'root ' + ' '.join( '%s=%s' % ( k, ra[k] ) for k in ( 'route', 'bundle', 'bodies', 'reason', 'budget_tokens', 'max_tokens', 'over_ceiling' ) if k in ra ) ) +for m in re.finditer( r'<(sigs|bodies|callers|far|hops|tail|tests|notes|trace)\b([^>]*)>', s ): + a = attrs( m.group( 2 ) ) + keep = [ '%s=%s' % ( k, a[k] ) for k in ( 'shown', 'total', 'capped', 'next_offset', 'past_target', 'docs_dropped', + 'format', 'frame_lines', 'parsed', 'in_corpus', 'suspects', 'merged', 'unresolved', 'skipped' ) if k in a ] + if 'next' in a: keep.append( 'next=' + norm( a['next'] ) ) + print( m.group( 1 ) + ' ' + ' '.join( keep ) ) +sg = re.search( r']*>(.*?)', s, re.S ) +rows = [] +if sg: + for d in re.finditer( r']*)>', sg.group( 1 ) ): + a = attrs( d.group( 1 ) ) + r = a.get( 'r', '' ) + rows.append( ( int( r ) if r.isdigit() else 10**9, a.get( 'p', '?' ) + ':' + a.get( 'l', '?' ) + ':' + a.get( 'n', '?' ) ) ) +rows.sort() +print( 'rows %d %s' % ( len( rows ), ' '.join( x[1] for x in rows ) ) ) +PY_EOF +# sameShape FILE_A TASK_A FILE_B TASK_B — 0 when both documents parse to the same shape; else prints the first diff +sameShape(){ + python3 "$SHAPE" "$1" "$2" > "$TMP/sa" || { echo " no root in $( basename "$1" )"; return 1; } + python3 "$SHAPE" "$3" "$4" > "$TMP/sb" || { echo " no root in $( basename "$3" )"; return 1; } + grep -q '^rows [1-9]' "$TMP/sa" || { echo " $( basename "$1" ) serves no row — nothing to compare"; return 1; } + cmp -s "$TMP/sa" "$TMP/sb" && return 0 + diff "$TMP/sa" "$TMP/sb" | head -4 | cut -c1-220 | sed 's/^/ /' + return 1 +} +# forgeArm LABEL FORGED_DOC FORGED_TASK PLAIN_DOC PLAIN_TASK WANT — FORGE: identical shape; both labels == WANT (0|1) +forgeArm(){ + local fo po + [ -s "$2" ] && [ -s "$4" ] || { no "$1: a probe produced no document (forged $( wc -c < "$2" | tr -d ' ' ) B, plain $( wc -c < "$4" | tr -d ' ' ) B)"; return; } + fo="$( hasLabel "$2" )"; po="$( hasLabel "$4" )" + if [ "$fo" != "$po" ]; then + no "$1: FORGED — the marker text changed the root's verdict (forged over_ceiling=$fo vs plain $po)" + elif [ "$fo" != "$6" ]; then + no "$1: both roots carry over_ceiling count=$fo, expected $6 ($( [ "$6" = 1 ] && echo 'the verdict is real here and must still fire' || echo 'no ceiling this root names is exceeded' ))" + elif ! sameShape "$2" "$3" "$4" "$5"; then + no "$1: the forged and plain tasks served different shapes (diff above) — the marker text moved the answer" + else + ok "$1: forged and plain tasks get the same verdict (over_ceiling $( [ "$6" = 1 ] && echo 'on both' || echo 'on neither' )) and the same shown/total/next=/rows" + fi +} + +cliFor(){ "$BIN" "$CORPUS" --no-cache "$@" 2>/dev/null; } + +# ── the runs. The soft sweep first: its 300 and 100000 points are also arms (2), (3), (7), (8). ──────────────── +SWEEP="300 500 1000 2000 4000 20000 100000" +for b in $SWEEP; do + cliFor --token-budget="$b" --for="$FORGED" > "$TMP/for.$b.f" + cliFor --token-budget="$b" --for="$PLAIN" > "$TMP/for.$b.p" + cliFor --token-budget="$b" --pack-task="$PT_FORGED" > "$TMP/pt.$b.f" + cliFor --token-budget="$b" --pack-task="$PT_PLAIN" > "$TMP/pt.$b.p" +done +cliFor --for="$FORGED" > "$TMP/for.def.f" +cliFor --for="$PLAIN" > "$TMP/for.def.p" +cliFor --pack-top-n=400 --for="$FORGED" > "$TMP/for.guard.f" +cliFor --pack-top-n=400 --for="$PLAIN" > "$TMP/for.guard.p" + +# (0) the --for probe is live: the clause the forged task carries is, byte for byte, the clause the runaway guard +# splices — read off the PLAIN guard document, whose task carries no marker, so only the code can have put it there. +if [ "$( hasLabel "$TMP/for.guard.p" )" -ge 1 ] && grep -qF -e "$GUARD_NOTE" "$TMP/for.guard.p"; then + ok "(0) the forged clause is the runaway guard's own reading, as emitted on a guard cut (the --for probe can forge)" +else + no "(0) the plain --pack-top-n=400 run carries no over_ceiling=\"1\" with this gate's GUARD_NOTE verbatim — re-pin GUARD_NOTE (serialize.h forRunawayGuardNote), or the forgery probe is inert and every --for arm passes for the wrong reason" +fi +if grep -qF -e "| $PT_MARK the header floor" "$PSRC"; then + ok "(0b) '$PT_MARK' is still the marker in --pack-task's last-rung note in src/packtask.h (the pack-task probe can forge)" +else + no "(0b) src/packtask.h's last-rung note no longer spells '$PT_MARK' — re-pin PT_MARK, or the pack-task arms prove nothing" +fi +if grep -qF -e "\" $MT_NOTE\"" "$ROOT/src/serialize.h"; then + ok "(0c) the --max-tokens label's definition is still '$MT_NOTE' in src/serialize.h (the --detail probe can forge)" +else + no "(0c) src/serialize.h no longer defines the max_tokens label as '$MT_NOTE' — re-pin MT_NOTE" +fi +if grep -qF -e "\" $TR_NOTE" "$TSRC"; then + ok "(0d) the --from-trace named-ceiling note still opens '$TR_NOTE' in src/tracelocus.h (the trace probe can forge)" +else + no "(0d) src/tracelocus.h's named-ceiling note no longer opens '$TR_NOTE' — re-pin TR_NOTE" +fi -# (1) the vector really reaches the emitted document — the task echo carries the marker into the header the -# old code searched. Without this the forgery arm could pass because nothing was injected at all. -if grep -qF "$MARK_A" "$TMP/forged.wide"; then - ok "(1) the forged marker reaches the emitted header (the injection vector is exercised)" +# (1) the vector really reaches the emitted document — the task echo carries the marker into the header a +# sniff would search. Without this the forgery arms could pass because nothing was injected at all. +if grep -qF -e "$GUARD_NOTE" "$TMP/for.def.f" && ! grep -qF -e "$GUARD_NOTE" "$TMP/for.def.p"; then + ok "(1) the forged clause reaches the emitted default --for document, and only through the task (the injection vector is exercised)" else - no "(1) the forged task text does not appear in the emitted document — the injection never happened, so arm (2) proves nothing" + no "(1) the forged task text does not appear in the default --for document (or the plain twin carries it too) — the injection never happened, so arms (2)-(4) prove nothing" fi -# (2) THE HEADLINE: a document 27x inside its own byte allowance must not be labelled over its ceiling, and -# must not state a self-contradicting pair of numbers. -fw_est="$( attr "$( rootOf "$TMP/forged.wide" )" est_tokens )" -fw_bud="$( attr "$( rootOf "$TMP/forged.wide" )" budget_tokens )" -fw_over="$( hasLabel "$TMP/forged.wide" )" +# (2) wide SOFT budget: a document far inside the target it states must not be labelled, and must not state a +# self-contradicting pair of numbers; forbudget-soft (rule 4): its plain twin must also be shape-identical (FORGE). +fw_est="$( attr "$( rootOf "$TMP/for.100000.f" )" est_tokens )" +fw_bud="$( attr "$( rootOf "$TMP/for.100000.f" )" budget_tokens )" if [ -z "$fw_est" ] || [ -z "$fw_bud" ]; then no "(2) the wide-budget forged run emitted no est_tokens=/budget_tokens= pair to judge (est='$fw_est' budget='$fw_bud')" -elif [ "$fw_over" -ne 0 ]; then - no "(2) FORGED: task text put over_ceiling=\"1\" on a root that states est_tokens=$fw_est under budget_tokens=$fw_bud ($( wc -c < "$TMP/forged.wide" | tr -d ' ' ) B) — the verdict was read off the task echo, not off the document" elif [ "$fw_est" -ge "$fw_bud" ]; then - no "(2) the wide-budget probe is not wide enough to be unambiguous: est_tokens=$fw_est is not under budget_tokens=$fw_bud — raise WIDE" + no "(2) the wide-budget probe is not wide enough to be unambiguous: est_tokens=$fw_est is not under budget_tokens=$fw_bud — raise the sweep's top" else - ok "(2) task text cannot forge over_ceiling=\"1\" (est_tokens=$fw_est under budget_tokens=$fw_bud, attribute absent)" + forgeArm "(2) --for --token-budget=100000 (est_tokens=$fw_est)" "$TMP/for.100000.f" "$FORGED" "$TMP/for.100000.p" "$PLAIN" 0 fi -# (2b) fix round 5: the last rung has a SECOND reason now (src/verbs_for.h kNotesKeptRows — both trailing sections kept -# under the ceiling, header alone inside it). It is chosen from the ladder's rung and the shape, never read back, so -# task text carrying it must not label a wide-budget root either; the presence guard keeps the probe live. -KEPT_A='over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections, and with the' -if grep -qF -e "$KEPT_A" "$SRC"; then - "$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --detail=3 --with-graph --for="serialize the header [$KEPT_A" > "$TMP/kept.wide" 2>/dev/null - kw_est="$( attr "$( rootOf "$TMP/kept.wide" )" est_tokens )" - if grep -qF "$KEPT_A" "$TMP/kept.wide" && [ -n "$kw_est" ] && [ "$kw_est" -lt "$WIDE" ] && [ "$( hasLabel "$TMP/kept.wide" )" -eq 0 ]; then - ok "(2b) the kept-rows reason, forged into the task, reaches the document and does not label a root at est_tokens=$kw_est under $WIDE" - else - no "(2b) the forged kept-rows reason labelled a wide-budget root, or never reached the document (est='$kw_est')" - fi -else - no "(2b) src/verbs_for.h no longer carries the kept-rows last-rung reason — re-pin KEPT_A" -fi +# (2b) forbudget-soft (rule 4) — REPLACES the kept-rows last-rung reason (src/verbs_for.h kNotesKeptRows, deleted with +# the --for header ladder). The DEFAULT regime is where the --for guard verdict lives: the guard's own clause in +# the task must not label a default answer, nor move one row of it. +forgeArm "(2b) default --for (no budget)" "$TMP/for.def.f" "$FORGED" "$TMP/for.def.p" "$PLAIN" 0 -# (3) CONTRAST — the SAME task at a budget it genuinely blows must still be labelled. Arms (2) and (3) differ -# in NOTHING but the number after --token-budget, so (2) cannot be green because the attribute was deleted. -ft_est="$( attr "$( rootOf "$TMP/forged.tight" )" est_tokens )" -ft_over="$( hasLabel "$TMP/forged.tight" )" -if [ "$ft_over" -ge 1 ]; then - ok "(3) the same task at --token-budget=300 still carries over_ceiling=\"1\" (est_tokens=$ft_est) — the verdict still fires when it is real" +# (3) forbudget-soft (rule 4) — REPLACES "the same task at --token-budget=300 still carries over_ceiling" (a soft target +# has no ceiling verdict now). FORGE + NOLBL at a target the answer blows: same shown/total/next=/rows, no label. +ft_est="$( attr "$( rootOf "$TMP/for.300.f" )" est_tokens )" +if [ -n "$ft_est" ] && [ "$ft_est" -gt 300 ]; then + forgeArm "(3) --for --token-budget=300 (est_tokens=$ft_est, past the soft target)" "$TMP/for.300.f" "$FORGED" "$TMP/for.300.p" "$PLAIN" 0 else - no "(3) --token-budget=300 emitted no over_ceiling=\"1\" (est_tokens=$ft_est) — the label is gone, not fixed, and arm (2) is green for the wrong reason" + no "(3) --for --token-budget=300 is not past its target (est_tokens='$ft_est') — the tight point no longer probes a blown target" fi -# (4) THE INVARIANT SWEEP — over_ceiling="1" may ride only on a document that is over one of the two ceilings -# its own root names: est_tokens > budget_tokens (tokens) or bytes > budget x 2.36 x 1.15 (the ladder's -# allowance, serialize.h ceilingAllowanceBytes). Swept over both tasks so a pass needs the property to hold -# on the forged AND the honest text, and counted so the arm cannot be vacuous. -# Swept over BOTH lenses now, because both climb this ladder and only one of them was ever gated. -labelled=0; violations=0 -for b in 300 500 1000 2000 4000 20000 100000; do - # "lens:task" pairs — the split is on the FIRST colon, and every lens name is colon-free, so a task that - # contains one (the --for marker does) survives intact. - for probe in "for:$FORGED" "for:$PLAIN" "pack-task:$PT_FORGED" "pack-task:$PT_PLAIN"; do - lens="${probe%%:*}"; task="${probe#*:}" - if [ "$lens" = for ]; then run "$b" "$task" > "$TMP/sweep.xml"; else runPack "$b" "$task" > "$TMP/sweep.xml"; fi - [ -s "$TMP/sweep.xml" ] || { no "(4) $lens --token-budget=$b produced no document"; continue; } - [ "$( hasLabel "$TMP/sweep.xml" )" -ne 0 ] || continue - labelled=$(( labelled + 1 )) - est="$( attr "$( rootOf "$TMP/sweep.xml" )" est_tokens )"; bytes="$( wc -c < "$TMP/sweep.xml" | tr -d ' ' )" - allow="$( awk -v b="$b" 'BEGIN{ printf "%d", b * 2.36 * 1.15 }' )" - if [ -n "$est" ] && [ "$est" -le "$b" ] && [ "$bytes" -le "$allow" ]; then - violations=$(( violations + 1 )) - no "(4) $lens budget=$b: over_ceiling=\"1\" on a document inside BOTH ceilings it names (est_tokens=$est <= $b, bytes=$bytes <= allowance $allow) — task: $( printf '%.40s' "$task" )…" +# (3g) THE CONTRAST the old (3) was for — the verdict still fires when it is real. --pack-top-n=400 passes the runaway +# guard: BOTH tasks are labelled (from the structural cut), and the forged text moves nothing. +forgeArm "(3g) --for --pack-top-n=400 (the runaway guard cut)" "$TMP/for.guard.f" "$FORGED" "$TMP/for.guard.p" "$PLAIN" 1 + +# (3m) the --max-tokens body ceiling on --for --detail (a hard verdict that lives): forged definition vs plain, at a +# ceiling the answer fits and at one it blows — the same verdict both times, real where real. +cliFor --detail=3 --max-tokens=100000 --for="$MT_FORGED" > "$TMP/mt.wide.f" +cliFor --detail=3 --max-tokens=100000 --for="$MT_PLAIN" > "$TMP/mt.wide.p" +cliFor --detail=3 --max-tokens=300 --for="$MT_FORGED" > "$TMP/mt.tight.f" +cliFor --detail=3 --max-tokens=300 --for="$MT_PLAIN" > "$TMP/mt.tight.p" +grep -qF -e "$MT_NOTE" "$TMP/mt.tight.p" \ + && ok "(3m) the tight plain run defines its label with '$MT_NOTE' (the clause the forged task carries)" \ + || no "(3m) the tight plain --max-tokens run does not define over_ceiling with '$MT_NOTE' — the probe impersonates nothing" +forgeArm "(3m) --for --detail=3 --max-tokens=100000" "$TMP/mt.wide.f" "$MT_FORGED" "$TMP/mt.wide.p" "$MT_PLAIN" 0 +forgeArm "(3m) --for --detail=3 --max-tokens=300" "$TMP/mt.tight.f" "$MT_FORGED" "$TMP/mt.tight.p" "$MT_PLAIN" 1 + +# (4) forbudget-soft (rule 4) — REPLACES "every labelled document in the sweep is genuinely over a ceiling" (labelled == 0 +# was a FAIL). On the soft lenses a label is now never genuine, so the sweep asserts FORGE + NOLBL at every +# (lens, budget) point, counted so the arm cannot pass on zero comparisons. The real labels are (3g)/(3m)/(8d)/(8t). +compared=0; bad=0 +for b in $SWEEP; do + for lens in for pt; do + if [ "$lens" = for ]; then ft="$FORGED"; pt="$PLAIN"; else ft="$PT_FORGED"; pt="$PT_PLAIN"; fi + fd="$TMP/$lens.$b.f"; pd="$TMP/$lens.$b.p" + if [ ! -s "$fd" ] || [ ! -s "$pd" ]; then bad=$(( bad + 1 )); echo " $lens budget=$b: a probe produced no document"; continue; fi + compared=$(( compared + 1 )) + fo="$( hasLabel "$fd" )"; po="$( hasLabel "$pd" )" + if [ "$fo" != 0 ] || [ "$po" != 0 ]; then + bad=$(( bad + 1 )); echo " $lens budget=$b: over_ceiling on a soft target (forged $fo, plain $po)" + elif ! sameShape "$fd" "$ft" "$pd" "$pt"; then + bad=$(( bad + 1 )); echo " $lens budget=$b: ^ forged and plain shapes differ" fi done done -if [ "$labelled" -eq 0 ]; then - no "(4) no swept run carried over_ceiling=\"1\" at all — the invariant held vacuously; the sweep's tight budgets must produce at least one labelled document" -elif [ "$violations" -eq 0 ]; then - ok "(4) every one of the $labelled labelled documents in the sweep is genuinely over a ceiling its own root states" +if [ "$compared" -eq 0 ]; then + no "(4) the sweep compared nothing — the invariant held vacuously" +elif [ "$bad" -eq 0 ]; then + ok "(4) at all $compared (lens x soft budget) points: no over_ceiling, and forged/plain tasks serve the same shape" +else + no "(4) $bad of $( echo "$SWEEP" | wc -w | tr -d ' ' )x2 soft sweep points labelled a soft target or let the marker text move the answer" fi # (6) THE PACK-TASK INJECTION VECTOR is live — the same presence guard arm (1) is for the --for lens. -if grep -qF "$PT_MARK" "$TMP/pt.forged.wide"; then +if grep -qF "$PT_MARK" "$TMP/pt.100000.f"; then ok "(6) the forged marker reaches the emitted --pack-task document (the injection vector is exercised)" else no "(6) the forged pack-task text does not appear in the emitted document — the injection never happened, so arm (7) proves nothing" fi # (7) THE PACK-TASK HEADLINE, arm (2)'s twin: a 13-character substring in the task must not label the root. -pw_est="$( attr "$( rootOf "$TMP/pt.forged.wide" )" est_tokens )" -pw_bud="$( attr "$( rootOf "$TMP/pt.forged.wide" )" budget_tokens )" -pw_over="$( hasLabel "$TMP/pt.forged.wide" )" -pp_over="$( hasLabel "$TMP/pt.plain.wide" )" +pw_est="$( attr "$( rootOf "$TMP/pt.100000.f" )" est_tokens )" +pw_bud="$( attr "$( rootOf "$TMP/pt.100000.f" )" budget_tokens )" if [ -z "$pw_est" ] || [ -z "$pw_bud" ]; then no "(7) the wide-budget forged --pack-task run emitted no est_tokens=/budget_tokens= pair to judge (est='$pw_est' budget='$pw_bud')" -elif [ "$pw_over" -ne "$pp_over" ]; then - no "(7) FORGED: the marker word in a --pack-task task changed the root's verdict (forged over_ceiling=$pw_over vs plain $pp_over) on est_tokens=$pw_est under budget_tokens=$pw_bud — the verdict was read off the task echo" -elif [ "$pw_over" -ne 0 ]; then - no "(7) both --pack-task probes are labelled at budget_tokens=$pw_bud (est_tokens=$pw_est) — WIDE is not wide enough for the contrast to mean anything; raise it" elif [ "$pw_est" -ge "$pw_bud" ]; then - no "(7) the wide-budget --pack-task probe is not unambiguous: est_tokens=$pw_est is not under budget_tokens=$pw_bud — raise WIDE" + no "(7) the wide-budget --pack-task probe is not unambiguous: est_tokens=$pw_est is not under budget_tokens=$pw_bud — raise the sweep's top" else - ok "(7) task text cannot forge over_ceiling=\"1\" on --pack-task (est_tokens=$pw_est under budget_tokens=$pw_bud, attribute absent on the root, and absent on the plain twin too)" + forgeArm "(7) --pack-task --token-budget=100000 (est_tokens=$pw_est)" "$TMP/pt.100000.f" "$PT_FORGED" "$TMP/pt.100000.p" "$PT_PLAIN" 0 fi -# (8) PACK-TASK CONTRAST, arm (3)'s twin: same task, a budget it genuinely blows, label still there. -pt_est="$( attr "$( rootOf "$TMP/pt.forged.tight" )" est_tokens )" -if [ "$( hasLabel "$TMP/pt.forged.tight" )" -ge 1 ]; then - ok "(8) the same --pack-task at --token-budget=300 still carries over_ceiling=\"1\" (est_tokens=$pt_est) — the verdict still fires when it is real" +# (8) forbudget-soft (rule 4) — REPLACES "--pack-task --token-budget=300 still carries over_ceiling" (an explicit budget is +# a soft target): FORGE + NOLBL at a target the bundle blows. +pt_est="$( attr "$( rootOf "$TMP/pt.300.f" )" est_tokens )" +if [ -n "$pt_est" ] && [ "$pt_est" -gt 300 ]; then + forgeArm "(8) --pack-task --token-budget=300 (est_tokens=$pt_est, past the soft target)" "$TMP/pt.300.f" "$PT_FORGED" "$TMP/pt.300.p" "$PT_PLAIN" 0 else - no "(8) --pack-task --token-budget=300 emitted no over_ceiling=\"1\" (est_tokens=$pt_est) — the label is gone, not fixed, and arm (7) is green for the wrong reason" + no "(8) --pack-task --token-budget=300 is not past its target (est_tokens='$pt_est') — the tight point no longer probes a blown target" fi +# (8d) THE CONTRAST (8) was for, where the hard ladder still lives: the DEFAULT --pack-task (no flag, 6000 tokens). A +# short forged task is not labelled; a task whose header floor alone passes 6000 tokens IS — forged or plain alike. +cliFor --pack-task="$PT_FORGED" > "$TMP/ptd.f" +cliFor --pack-task="$PT_PLAIN" > "$TMP/ptd.p" +forgeArm "(8d) default --pack-task" "$TMP/ptd.f" "$PT_FORGED" "$TMP/ptd.p" "$PT_PLAIN" 0 +PT_HUGE_F="$( python3 -c 'import sys; print( ( ( sys.argv[1] + " " ) * 420 ).strip() )' "$PT_FORGED" )" +PT_HUGE_P="$( python3 -c 'import sys; print( ( ( sys.argv[1] + " " ) * 420 ).strip() )' "$PT_PLAIN" )" +cliFor --pack-task="$PT_HUGE_F" > "$TMP/pth.f" +cliFor --pack-task="$PT_HUGE_P" > "$TMP/pth.p" +forgeArm "(8d) default --pack-task, a ${#PT_HUGE_F}-char task (header floor past 6000 tokens)" "$TMP/pth.f" "$PT_HUGE_F" "$TMP/pth.p" "$PT_HUGE_P" 1 + +# (8t) --from-trace --max-tokens (the hard ceiling this lens still labels): the trace file's NAME carries the note. +mkdir -p "$TMP/tr" || no "(8t) cannot create $TMP/tr" +TR_F="$TMP/tr/trace $TR_NOTE.txt" +TR_P="$TMP/tr/$( plainOf "trace $TR_NOTE" ).txt" +printf 'src/graph.h:120:5: error: no member named zz\nsrc/serialize.h:200: in escapeXml\n' > "$TR_F" +cp "$TR_F" "$TR_P" +for mt in 100000 200; do + cliFor --from-trace="$TR_F" --max-tokens="$mt" > "$TMP/tr.$mt.f" + cliFor --from-trace="$TR_P" --max-tokens="$mt" > "$TMP/tr.$mt.p" +done +grep -qF -e "$TR_NOTE" "$TMP/tr.100000.f" \ + && ok "(8t) the forged trace name reaches the emitted --from-trace document (the injection vector is exercised)" \ + || no "(8t) the forged trace name does not appear in the --from-trace document — the injection never happened" +forgeArm "(8t) --from-trace --max-tokens=100000" "$TMP/tr.100000.f" "$TR_F" "$TMP/tr.100000.p" "$TR_P" 0 +forgeArm "(8t) --from-trace --max-tokens=200" "$TMP/tr.200.f" "$TR_F" "$TMP/tr.200.p" "$TR_P" 1 + # (5) RECURRENCE GUARD on the SOURCE SHAPE — and read its limits before you lean on it. It is a grep over one # spelling, in two directions, with comments stripped so a sentence ABOUT the defect cannot red it: # · NEGATIVE — no marker sniff: no `header.find(` in the --for finishing path, no `lastRungNote` field # for one to search for, and no `chosen.find(` in --pack-task. spliceBefore's `doc.find( boundary )` # is a different operation on a different string (a boundary the emitter itself wrote), not matched. -# · POSITIVE — each lens's verdict is ASSIGNED from the value the ladder returned. This half is what a -# RENAME cannot walk past: reintroducing the defect through a differently-named field (a `rungMarker` -# carrying the note text, say) deletes these assignments and reds here, where the negative half alone -# stayed green through exactly that mutation while arms (2)/(4) went red. -# WHAT IT IS NOT: a proof. Any guard shaped like a grep can be evaded by a spelling it does not know, and -# the arms that DISCRIMINATE are (2)/(4) for --for and (7)/(4) for --pack-task — behavioural, over real -# documents. Treat a green (5) as "the known shapes are absent", never as "the verdict cannot be forged". +# · POSITIVE — each lens's verdict is ASSIGNED from a structural value. This half is what a RENAME cannot +# walk past: reintroducing the defect through a differently-named field deletes these assignments and reds here. +# forbudget-soft (rule 4): the --for verdict no longer comes from a ladder rung (`lastRungFired = ( chosen.rung`, +# deleted with the header ladder); it is the runaway guard's row-drop cut, `forGuardCut = forSigsCut.rowDropCut`, +# handed to the root as `.guardOver = forGuardCut`. --pack-task's default ladder keeps its rung assignment. +# WHAT IT IS NOT: a proof. Any guard shaped like a grep can be evaded by a spelling it does not know; the arms +# that DISCRIMINATE are the behavioural ones above. Treat a green (5) as "the known shapes are absent". stripped(){ sed 's|//.*||' "$1"; } badfind="$( stripped "$SRC" | grep -c 'header\.find(' || true )" badnote="$( stripped "$SRC" | grep -c 'lastRungNote' || true )" badpack="$( stripped "$PSRC" | grep -c 'chosen\.find(' || true )" -forasn="$( stripped "$SRC" | grep -cE 'lastRungFired *= *\( *chosen\.rung' || true )" +forcut="$( stripped "$SRC" | grep -cE 'forGuardCut *= *forSigsCut\.rowDropCut' || true )" +forasn="$( stripped "$SRC" | grep -cE '\.guardOver *= *forGuardCut' || true )" packasn="$( stripped "$PSRC" | grep -cE 'chosen\.rung == (rw::)?CeilingRung::OverCeiling' || true )" -if [ "$badfind" -eq 0 ] && [ "$badnote" -eq 0 ] && [ "$badpack" -eq 0 ] && [ "$forasn" -ge 1 ] && [ "$packasn" -ge 1 ]; then - ok "(5) both lenses assign the verdict from the rung the ladder returned, and no marker sniff remains (spelling-level guard only — (2)/(4)/(7) are the discriminating arms)" +if [ "$badfind" -eq 0 ] && [ "$badnote" -eq 0 ] && [ "$badpack" -eq 0 ] && [ "$forcut" -ge 1 ] && [ "$forasn" -ge 1 ] && [ "$packasn" -ge 1 ]; then + ok "(5) --for assigns its verdict from the guard's row-drop cut and --pack-task from the rung its ladder returned; no marker sniff remains (spelling-level guard only)" else - no "(5) the rung verdict is re-derived from emitted text, or no longer assigned from the ladder's return: verbs_for.h header.find(=$badfind lastRungNote=$badnote rung-assignment=$forasn; packtask.h chosen.find(=$badpack rung-assignment=$packasn — each of the last two must be >= 1)" + no "(5) a verdict is re-derived from emitted text, or no longer assigned from a structural value: verbs_for.h header.find(=$badfind lastRungNote=$badnote guard-cut=$forcut guardOver-assignment=$forasn; packtask.h chosen.find(=$badpack rung-assignment=$packasn — the first three must be 0, the last three >= 1)" fi # ── (9) AN UNMEASURED FIT IS NOT A HELD CAP — the map's --max-tokens verdict when its probe cannot measure ────────── diff --git a/test/chainguardcheck.sh b/test/chainguardcheck.sh index c6c157bf8..5d6aa3eb0 100755 --- a/test/chainguardcheck.sh +++ b/test/chainguardcheck.sh @@ -214,7 +214,8 @@ GO2="$( "$BIN" "$FIX2" --callees=go --no-cache 2>/dev/null | grep -o ' "its calls of the called # LEAF name, any receiver or qualifier"), 3890 -> 3907 B. Checked mechanically: the reverse replacement of that one phrase # gives the previous pin byte for byte (at= masked). The six other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-15 (lane forbudget-soft round C, FOLD C, by design), for.xml only: the carries page 0 of the +# --for file page (rows with sym=), its full legend clause defines them, and est_tokens= prices them, 4111 -> 4500 B. Checked +# mechanically: with the element, the tail clause and est_tokens= masked, the new answer equals the previous pin byte for +# byte (at= masked). 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. @@ -546,6 +551,10 @@ probeFor() # callers prose minus that reading to the OLD 330; the existing (U-twin) holds safe-delete minus its reading to 720. # the pins follow the definitions, measured + 10 rounded up to 10. # schema pin measured +# RE-PINNED 2026-10-15 (lane forbudget-soft round C, FOLD C): ripwire.for/v1 775 -> 820 (measured 802; 761 at the last pin). +# The now carries page-0 rows of the file page with sym=, and the compact tail clause reads them: "; t p= file +# outside sigs (weaker), r= rank (gap = trimmed)" (58 B) -> "...(weaker), t sym= a file-page row: its top symbols, r= ..." +# (99 B, +41 B), nothing else moved. (L) stays under its 7,900 B bill (7,871 B). # 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: # ripwire.for/v1 738->761: " (absent cx/ccx/in = 0)" (+23 B) — the --for lens omits a zero cx=/ccx=/in=. @@ -645,7 +654,7 @@ ripwire.pack-task/v1 990 974 ripwire.pack-top-n/v1 760 745 ripwire.expand/v1 280 265 ripwire.expand-file/v1 510 508 -ripwire.for/v1 775 761 +ripwire.for/v1 820 802 ' pinFor() { @@ -936,6 +945,11 @@ echo "=== (P) L1: compact BUYS rows and never loses one; a compact answer is PRI # is covered, not lost (the tail lists files OUTSIDE the sigs, so a bigger sigs section legitimately shortens it). # Posture-generic: it compares the default against --legend=full, whatever the default is. The arm must also BIND — # at least one argv where the default carries strictly more rows — or it would pass on a binary whose default is full. +# forbudget-soft (2026-10-14): an explicit --token-budget on --for is now a SOFT TARGET charged against the rows only, +# never the header, so the two dialects serve the SAME rows and there is no ceiling ladder left to buy one with. This arm and +# (P1-B) still hold as written (the superset is implied by equality; ruling B's exemption has no live case — p1Exempt needs a +# compact answer whose rows paid for its next=, and nothing is paid from rows any more); the equality itself, CF, is pinned +# strictly by the replaced (P1-S)/(P1-S2)/(P4 floor twin) arms below. cat > "$TMP/rows.py" <<'PY' import re, sys def rows( path ): @@ -1100,82 +1114,165 @@ else 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" -# (P1-S) THE SWEEP (train 27). The pinned 1500 above is ONE point of a band, and the band moved under it: lego-charge (train 26c, -# bisected to its merge 7601d3ac) charged a stubbed / at its served size, which removed the slack that had been -# absorbing the compact header's excess over full's (the L1 cap charges the compact rows no more header than full's), so the -# default answer landed past budgets the full answer met (1300..1500, 1600..1700 on the merged tree), each with a last-rung note -# blaming "the header floor" while the header was about half the budget. Over --token-budget=1100..1700 step 50, on the same -# short relative root, both dialects, every budget must hold: -# FIT the default fits its budget wherever full does (so P4 — over the budget AND over full's — fires only where full is over too); -# NOTE in each dialect a last-rung reason is TRUE: "the header floor ... exceeds" only on a header (the root open tag and the -# comments before the payload) past the allowance (budget x 2.36 x 1.15), "the header alone fits" only on one inside it; -# P1 a row full keeps and the default lost is the ruled exemption (p1Exempt above), as on the fixed arms. -# …and it BINDS: some budget where full fits and the default paid rows for it (an exempt row), and some where both are over and -# the true reason rides (the note arm reads a real last rung, not an absence). -p1sbad=0; p1sn=0; p1sexempt=0; p1sbothover=0; p1sfirst="" +# ── forbudget-soft (rule 4): (P1-S) and (P1-S2) REPLACED; the (P4 floor twin) below uses the same reader ───────────────────── +# Owner decisions 2026-10-14: an explicit --token-budget=N on --for is a SOFT TARGET. T = N x 2.36 x 0.90 bytes is charged +# against the rows inside ONLY (the header and the disclosures are never charged), rows are added in rank order while +# they fit T, rank 1..4 always ship, the cut carries next=, nothing is labelled over_ceiling= and no header clause, task echo +# or route= is dropped "(ceiling)". The paid/unpaid continuation (knob-honesty-068 C3) is gone: one render, nothing paid from +# rows. What the two retired arms pinned is therefore gone BY DESIGN: (P1-S) needed an exempt row (a row the compact answer +# paid for its next=) and a both-over budget whose last-rung note told the true reason; (P1-S2) needed the full answer to fit +# 1300 / 1200 (its premise — the soft lens's est_tokens may exceed N by the uncharged header: HEAD 2331 / 2249) and the +# compact answer to fit or be labelled over_ceiling="1" at the floor. The core of what P1 protects — compact BUYS rows and +# never loses one — is now exact, because the rows are charged and the header is not, and is pinned as these invariants +# (one python3 per answer, the reader below; every field present and numeric before it is compared, CHECKLIST 14): +# CF the compact and full dialects serve the SAME rows (p:l:n in rank order) — equality, not the old superset; +# ST the rows' bytes inside (+ the zero cx=/ccx=/in= the lean spelling elides, which serialize.h's entryCost +# charges — fornotesbudgetcheck's soft.py reads them the same way) fit T, or only the rank 1..4 floor ships, the floor +# is never short; a cut is MAXIMAL: the next row of the same dialect's no-budget answer would cross T (160 B slack); +# UP (FIX2 C1, rule 4) the maximality twin: a cut past the floor serves no rows past T less the other sections and +# the tail reserve (test/lib/softbudget.py upper_bound), RED on a binary that ignores the tail reserve; +# RP the rows are a rank prefix of that dialect's no-budget rows; +# NOLBL no over_ceiling= on the root, and neither old over_ceiling clause ("[over_ceiling= is 1 on the root" / "[over_ceiling=1 +# also when the sigs next= rides unpaid") in the legend; +# NX a cut (rows < total) is capped="1" and carries next= on , and its next_offset= (when printed) is the row count; +# HL nothing is dropped "(ceiling)", no "task_echo: dropped", the root keeps route= and task= verbatim. +# softp.py FILE N TASK ROWSOUT [REF] prints ONE line "OK|BAD rows= total= charged= T= est= over= next= fits= maximal= upper= rp= hl="; +# exit 3 = no root or no block (a missing premise: the caller FAILs it, never skips). ROWSOUT gets the rows. +cat > "$TMP/softp.py" <<'PY' +import html, json, re, sys +def read( path ): + try: s = open( path, encoding = "utf-8", errors = "replace" ).read() + except OSError: return None + root = re.search( r"]*>", s ); m = re.search( r"]*)>(.*?)", s, re.S ) + if not root or not m: return None + inner = m.group( 2 ); charged = len( inner.encode() ); rows = [] + for x in re.finditer( r"]*)>.*?|]*)/>", inner, re.S ): + a = x.group( 1 ) or x.group( 2 ) + g = lambda k: ( re.search( r"\b" + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + elided = sum( n for k, n in ( ( "cx", 7 ), ( "ccx", 8 ), ( "in", 7 ) ) if not re.search( r"\b" + k + r'="', a ) ) + charged += elided; r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ), len( x.group( 0 ).encode() ) + elided ) ) + rows.sort(); sa = m.group( 1 ); rt = root.group( 0 ) + t = re.search( r'\btotal="(\d+)"', sa ); nx = re.search( r'\bnext="[^"]+"', sa ); no = re.search( r'\bnext_offset="(\d+)"', sa ) + e = re.search( r'\best_tokens="(\d+)"', rt ); tk = re.search( r'\btask="([^"]*)"', rt ) + return dict( s = s, rows = rows, charged = charged, total = int( t.group( 1 ) ) if t else len( rows ), capped = ' capped="1"' in sa, + next = nx is not None, nextoff = int( no.group( 1 ) ) if no else None, est = e.group( 1 ) if e else "-", + over = re.search( r'\bover_ceiling="', rt ) is not None or "[over_ceiling= is 1 on the root" in s + or "[over_ceiling=1 also when the sigs next= rides unpaid" in s, + hl = "(ceiling)" not in s and "task_echo: dropped" not in s and ' route="' in rt + and tk is not None and html.unescape( tk.group( 1 ) ) == sys.argv[ 3 ] ) +def sections_u( s ): + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served first + # and the rows past the cut fill what they leave of T, so maximality reads T less their bytes (U): XML, all after but + # plus the rows, less the --detail bodies (a root without bundle=); JSON, the "tail" object + if s.lstrip().startswith( "{" ): + try: return len( json.dumps( json.loads( s ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + u = ( len( s.split( "", 1 )[ 1 ].encode() ) - len( "" ) ) if "" in s else 0 + u += sum( len( h.encode() ) for h in re.findall( r"]*/>", s.split( "]*>", s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r"", s, re.S ) + u -= len( bm.group( 0 ).encode() ) if bm else 0 + return u +a = read( sys.argv[ 1 ] ) +if a is None: sys.exit( 3 ) +N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ); k = len( a[ "rows" ] ) +# the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) +import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size +U = sections_u( a[ "s" ] ) +if len( sys.argv ) > 5: + try: U += max( 0, tail_size( open( sys.argv[ 5 ], encoding = "utf-8", errors = "replace" ).read() ) - tail_size( a[ "s" ] ) ) + except OSError: pass +open( sys.argv[ 4 ], "w" ).write( "".join( x[ 1 ] + "\n" for x in a[ "rows" ] ) ) +cut = k < a[ "total" ] +fits = ( a[ "charged" ] <= X or k <= 4 ) and k >= min( 4, a[ "total" ] ) +nxt = not cut or ( a[ "capped" ] and a[ "next" ] and a[ "nextoff" ] in ( None, k ) ) +maximal = rp = upper = True +if len( sys.argv ) > 5: + ref = read( sys.argv[ 5 ] ) + if ref is None: sys.exit( 3 ) + if cut and k < len( ref[ "rows" ] ): maximal = a[ "charged" ] + ref[ "rows" ][ k ][ 2 ] > X - U - 160 + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + from softbudget import upper_bound + upper = upper_bound( a[ "s" ], ref[ "s" ], X, k, a[ "total" ], U )[ 0 ] + rp = k > 0 and [ x[ 1 ] for x in a[ "rows" ] ] == [ x[ 1 ] for x in ref[ "rows" ] ][ :k ] +ok = a[ "est" ] != "-" and fits and maximal and upper and rp and nxt and not a[ "over" ] and a[ "hl" ] +print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d next=%d fits=%d maximal=%d upper=%d rp=%d hl=%d" + % ( "OK" if ok else "BAD", k, a[ "total" ], a[ "charged" ], X, a[ "est" ], int( a[ "over" ] ), int( nxt ), int( fits ), + int( maximal ), int( upper ), int( rp ), int( a[ "hl" ] ) ) ) +PY +spfield(){ local re=" $2=([^ ]*)"; [[ " $1 " =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; } # spfield LINE KEY +# spRef LAB DIR ROOTARG TASK — the no-budget answer of both dialects (the ST/RP reference), and its rows in "$TMP/sp.ref.LAB.DIAL.rows". +spRef(){ + local d; for d in compact full; do + rm -f "$TMP/sp.ref.$1.$d.rows" + ( cd "$2" && "$BIN" "$3" --for="$4" --legend=$d ) >"$TMP/sp.ref.$1.$d" 2>/dev/null + python3 "$TMP/softp.py" "$TMP/sp.ref.$1.$d" 0 "$4" "$TMP/sp.ref.$1.$d.rows" >/dev/null + done +} +# softPair LAB DIR ROOTARG TASK N — both dialects at --token-budget=N (each asked by name, so it reads the same dialect on any +# binary); appends every broken invariant to $spWhy (empty = all hold) and leaves each dialect's verdict in $spLine_compact / +# $spLine_full and its rows in "$TMP/sp.DIAL.rows". +softPair(){ + local lab="$1" dir="$2" rootarg="$3" task="$4" tb="$5" d line rc nc nf + spWhy=""; spLine_compact=""; spLine_full="" + for d in compact full; do + rm -f "$TMP/sp.$d.rows" + ( cd "$dir" && "$BIN" "$rootarg" --for="$task" --token-budget="$tb" --legend=$d ) >"$TMP/sp.$d" 2>/dev/null; rc=$? + if [ "$rc" -ne 0 ]; then spWhy="$spWhy $d: exit $rc;"; continue; fi + line="$( python3 "$TMP/softp.py" "$TMP/sp.$d" "$tb" "$task" "$TMP/sp.$d.rows" "$TMP/sp.ref.$lab.$d" )" + case "$line" in OK*) ;; *) spWhy="$spWhy $d: ${line:-no / in the answer or its no-budget reference};" ;; esac + eval "spLine_$d=\"\$line\"" + done + nc="$( wc -l <"$TMP/sp.compact.rows" 2>/dev/null | tr -d ' ' )"; nf="$( wc -l <"$TMP/sp.full.rows" 2>/dev/null | tr -d ' ' )" + { [ "${nc:-0}" -gt 0 ] && cmp -s "$TMP/sp.compact.rows" "$TMP/sp.full.rows"; } \ + || spWhy="$spWhy CF: compact rows (${nc:-?}) are not full's (${nf:-?});" +} + +# (P1-S) THE SWEEP — REPLACED, forbudget-soft (rule 4). Was (train 27): over --token-budget=1100..1700 step 50 the default fits +# wherever full does, every last-rung over_ceiling reason is true, every lost row is ruling B's exemption, and it bound on an +# exempt row + a both-over budget with the true reason riding. Under the soft target there is no exemption, no last rung and +# no over_ceiling= to be truthful about (HEAD 480bc90b: 13 of 13 read, 0 exempt, 0 both-over — the arm's binding premise is the +# deleted contract). Now, on the same short relative root and band, both dialects: CF + ST + RP + NOLBL + NX + HL at every +# budget, the rows a rank prefix that grows with N (RP monotone), and it BINDS: at least one budget cuts the head past the +# rank 1..4 floor (so ST's fit and maximality are read on a real cut, not a floor or a complete answer). +spRef pr "$ROOT" src "rank symbols by pagerank" +p1sbad=0; p1sn=0; p1scut=0; p1sfirst=""; p1sprev=""; p1slo=""; p1shi="" p1sfail(){ p1sbad=$(( p1sbad + 1 )); [ "$p1sbad" -le 6 ] && p1sfirst="$p1sfirst [$1]"; } for tb in $( seq 1100 50 1700 ); do - ( cd "$ROOT" && "$BIN" src --for="rank symbols by pagerank" --token-budget="$tb" ) >"$TMP/p1s.def" 2>/dev/null; rd=$? - ( cd "$ROOT" && "$BIN" src --for="rank symbols by pagerank" --token-budget="$tb" --legend=full ) >"$TMP/p1s.full" 2>/dev/null; rf=$? - allow="$( awk "BEGIN{printf \"%d\", $tb*2.36*1.15}" )" - set -- $( python3 "$TMP/p1d.py" "$TMP/p1s.def" "$TMP/p1s.full" ) - if [ "$rd" -ne 0 ] || [ "$rf" -ne 0 ] || [ $# -ne 6 ] || ! printf '%s' "$1$2$3$4$allow" | grep -qE '^[0-9]+$'; then - p1sfail "@$tb: unreadable answers (exit $rd/$rf, facts '$*')"; continue - fi - dh="$1"; fh="$2"; de="$3"; fe="$4"; p1sn=$(( p1sn + 1 )) - [ "$fe" -le "$tb" ] && [ "$de" -gt "$tb" ] && p1sfail "@$tb FIT: default est_tokens $de over the budget, full fits at $fe" - [ "$de" -gt "$tb" ] && [ "$fe" -gt "$tb" ] && p1sbothover=$(( p1sbothover + 1 )) - for dial in def full; do - f="$TMP/p1s.$dial"; h="$dh"; [ "$dial" = full ] && h="$fh" - if grep -qF '[over_ceiling= is 1 on the root: the header floor' "$f"; then - [ $(( h + 6 )) -gt "$allow" ] || p1sfail "@$tb NOTE($dial): blames the header floor, but the header is $h B of a $allow B allowance" - fi - if grep -qF '[over_ceiling= is 1 on the root: the header alone fits this budget' "$f"; then - [ $(( h + 6 )) -le "$allow" ] || p1sfail "@$tb NOTE($dial): says the header alone fits, but it is $h B of a $allow B allowance" - [ "$dial" = def ] && [ "$de" -gt "$tb" ] && [ "$fe" -gt "$tb" ] && p1sbothnote=$(( ${p1sbothnote:-0} + 1 )) - fi - grep -qF '[over_ceiling= is 1 on the root: the ranked rows are kept' "$f" \ - && p1sfail "@$tb NOTE($dial): the kept-rows reason (the --detail + --with-graph shape) on a bare --for" - done - set -- $( python3 "$TMP/rows.py" "$TMP/p1s.def" "$TMP/p1s.full" | tr -d "[](),'" ) - if [ "${3:-1}" -ne 0 ]; then - if p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" "$tb"; then - [ "$fe" -le "$tb" ] && p1sexempt=$(( p1sexempt + 1 )) - else - p1sfail "@$tb P1: lost ${3:-?} row(s) full keeps (default ${1:-?}, full ${2:-?}), not the ruled exemption ($p1facts)" - fi + softPair pr "$ROOT" src "rank symbols by pagerank" "$tb" + r="$( spfield "$spLine_compact" rows )"; t="$( spfield "$spLine_compact" total )" + if ! printf '%s' "$r$t" | grep -qE '^[0-9]+$'; then p1sfail "@$tb unreadable:$spWhy"; continue; fi + p1sn=$(( p1sn + 1 )); [ -z "$p1slo" ] && p1slo="$r"; p1shi="$r" + [ -n "$spWhy" ] && p1sfail "@$tb$spWhy" + [ "$r" -lt "$t" ] && [ "$r" -gt 4 ] && p1scut=$(( p1scut + 1 )) + if [ -n "$p1sprev" ]; then + head -n "$( wc -l <"$TMP/p1s.prev" | tr -d ' ' )" "$TMP/sp.compact.rows" | cmp -s - "$TMP/p1s.prev" \ + || p1sfail "@$tb RP: the rows at $p1sprev are not a prefix of the rows at $tb" fi + cp "$TMP/sp.compact.rows" "$TMP/p1s.prev" 2>/dev/null; p1sprev="$tb" done -if [ "$p1sbad" -eq 0 ] && [ "$p1sn" -eq 13 ] && [ "$p1sexempt" -gt 0 ] && [ "${p1sbothnote:-0}" -gt 0 ]; then - ok "(P1-S) --for='rank symbols by pagerank' on root src, --token-budget=1100..1700 step 50, both dialects: the default fits wherever full does, every last-rung reason is true, every lost row is the exemption (binds: $p1sexempt exempt row budget(s) where full fits, $p1sbothover where both are over with the true reason riding ${p1sbothnote:-0}x)" +if [ "$p1sbad" -eq 0 ] && [ "$p1sn" -eq 13 ] && [ "$p1scut" -gt 0 ]; then + ok "(P1-S) --for='rank symbols by pagerank' on root src, --token-budget=1100..1700 step 50: compact rows == full rows at every budget, each fits T or is the floor and stops only where the next row would cross it, no over_ceiling=, every cut carries next=, no (ceiling) drop; rows $p1slo -> $p1shi, a growing rank prefix (binds: $p1scut budget(s) cut past the floor)" else - no "(P1-S) the budget sweep ($p1sn of 13 budgets read, $p1sexempt exempt where full fits, ${p1sbothnote:-0} both-over with the true reason):$p1sfirst ($p1sbad failure(s))" + no "(P1-S) the soft-target sweep ($p1sn of 13 budgets read, $p1scut cut past the floor):$p1sfirst ($p1sbad failure(s))" fi -# (P1-S2) THE OVERRUN PAYMENT, and what is left when it cannot pay. The sweep above fits on its own at every budget; the -# compact dialect's last payment (verbs_for.h runForLens: a paid answer still over pays its MEASURED overrun from the rows) -# is what fits "serialize the map" at 1300 (a mutant without it serves 6 rows at est_tokens 1457, over, while full fits at -# 1294): the default must fit there, with full fitting too (the premise). At 1200 the same task is the case the payment -# cannot reach — both dialects at the rank 1..4 floor, full fits by a token, the compact header is larger — so the default -# goes over, and then it must say so truthfully (rule 5): over_ceiling="1", the header-fits reason with the header inside the -# allowance, and the next= that serves the rows. -p1s2bad="" -for spec in "1300:fit" "1200:disclose"; do - tb="${spec%%:*}"; want="${spec#*:}" - ( cd "$ROOT" && "$BIN" src --for="serialize the map" --token-budget="$tb" ) >"$TMP/p1s2.def" 2>/dev/null - ( cd "$ROOT" && "$BIN" src --for="serialize the map" --token-budget="$tb" --legend=full ) >"$TMP/p1s2.full" 2>/dev/null - set -- $( python3 "$TMP/p1d.py" "$TMP/p1s2.def" "$TMP/p1s2.full" ) - allow="$( awk "BEGIN{printf \"%d\", $tb*2.36*1.15}" )" - if [ $# -ne 6 ] || ! printf '%s' "$1$3$4" | grep -qE '^[0-9]+$'; then p1s2bad="$p1s2bad [@$tb unreadable: '$*']"; continue; fi - [ "$4" -le "$tb" ] || p1s2bad="$p1s2bad [@$tb premise: full est_tokens $4 is over the budget — re-anchor]" - if [ "$want" = fit ]; then - [ "$3" -le "$tb" ] || p1s2bad="$p1s2bad [@$tb FIT: default est_tokens $3 over the budget, full fits at $4]" - elif [ "$3" -gt "$tb" ]; then - { grep -q '^]* over_ceiling="1"' "$TMP/p1s2.def" && grep -qF '[over_ceiling= is 1 on the root: the header alone fits this budget' "$TMP/p1s2.def" \ - && [ $(( $1 + 6 )) -le "$allow" ] && grep -qE ']* next="--for=' "$TMP/p1s2.def"; } \ - || p1s2bad="$p1s2bad [@$tb DISCLOSE: default over ($3) without over_ceiling + the true reason (header $1 B of $allow B) + sigs next=]" - fi +# (P1-S2) REPLACED, forbudget-soft (rule 4). Was: 'serialize the map' FITS 1300 where full fits (the compact overrun payment), +# and at 1200 (the floor the payment cannot reach) any overshoot is labelled over_ceiling="1" with the header-fits reason and +# the sigs next=. Both halves assert the hard ceiling (HEAD 480bc90b: full est_tokens 2331 / 2249 — "over" N by the header the +# soft target never charges, compact 1996 / 1914, no label). The task was chosen because its compact and full headers sat on +# either side of the budget; that is exactly where CF could break if a header were still charged, so the same two budgets keep +# the invariants of the sweep above: CF + ST + RP + NOLBL + NX + HL in both dialects. +spRef sm "$ROOT" src "serialize the map" +p1s2bad=""; p1s2facts="" +for tb in 1300 1200; do + softPair sm "$ROOT" src "serialize the map" "$tb" + [ -n "$spWhy" ] && p1s2bad="$p1s2bad [@$tb$spWhy]" + p1s2facts="$p1s2facts @$tb: $( spfield "$spLine_compact" rows ) of $( spfield "$spLine_compact" total ) rows, est $( spfield "$spLine_compact" est ) (full $( spfield "$spLine_full" est ));" done -[ -z "$p1s2bad" ] && ok "(P1-S2) 'serialize the map' on root src: fits at 1300 where full fits (the overrun payment), and where the floor cannot pay (1200) any overshoot is labelled with its true reason and the sigs next=" \ +[ -z "$p1s2bad" ] && ok "(P1-S2) 'serialize the map' on root src at 1300 and 1200: compact rows == full rows, each fits T or is the floor, maximal, no over_ceiling=, the cut carries next=, no (ceiling) drop ($p1s2facts)" \ || no "(P1-S2)$p1s2bad" # (P2) the default map's --token-budget gate decides on the price the default PRINTS. It used to decide on the full @@ -1235,49 +1332,77 @@ 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 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). -# RE-PINNED 2026-10-10 (train 26c): the near miss moves 750 -> 900. lego-charge charges / at their served size -# and #362's rides the bundle, so this fixture's 4-row floor answer prices est_tokens=918 compact / 963 full -# (was 814 / 859): at 750 both dialects are still at the floor, unpaid and labelled; the paid next= first fits in both -# dialects at 850 (784 / 829) and at 900 with margin (855 / 868). The 500 half is unchanged (4 rows, over, the clause). -p4floor_bad=0 +# (P4 floor twin) REPLACED, forbudget-soft (rule 4). Was (knob-honesty-068; rulings 2026-10-07; re-pinned 750 -> 900 in train +# 26c): at 500 both dialects ship next= UNPAID at the rank 1..4 floor, labelled over_ceiling="1" with the unpaid clause; +# at 900 the PAID next= fits (est_tokens <= 900) with no label. Both halves pin the retired hard ceiling and its C3 payment. +# HEAD 480bc90b on this fixture: at 500 the compact and full dialects serve the same 7 of 12 rows (989 of T = 1,062 B charged; +# the 8th row's 133 B would cross it), est 1105 / 1644, no label — the arm read "shown=7 over_ceiling=0 clause=0"; at 900 both +# serve all 12 rows of the head in a complete (no shown=/total=/capped=/next= on it, as on the no-budget answer: 1,655 of +# T = 1,911 B), which the old arm misread as "a capped block without next=" — there is no cut there to recover. Now: +# 500 the soft cut: CF + ST + RP + NOLBL + NX + HL (softPair above), the cut's next= RUNS on this fixture and serves the whole +# head (the no-budget answer's rows) uncut, the cut's rows its prefix; est_tokens prices the bytes delivered (P3's rule, +# kept from the old arm); +# 900 stop when complete: a target wider than the answer serves exactly the no-budget answer's rows, uncapped, no next=, +# no label — in both dialects; +# both the P4 claim itself, kept: compact est_tokens is inside N or no further over it than full's. +spRef p4 "$P4" . "widget routine dispatcher" +p4floor_bad=0; p4facts="" for tb in 500 900; do + softPair p4 "$P4" . "widget routine dispatcher" "$tb" + [ -n "$spWhy" ] && { p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb:$spWhy"; } 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" | 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 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 + f="$TMP/sp.$lg"; pb="$( wc -c <"$f" | tr -d ' ' )"; pe="$( grep -oE '^]*>' "$f" | head -1 | grep -oE ' est_tokens="[0-9]+"' | tr -dc '0-9' )" + sigs="$( grep -oE ']*)?>' "$f" | head -1 )" + eval "line=\"\$spLine_$lg\""; r="$( spfield "$line" rows )"; t="$( spfield "$line" total )" + if ! printf '%s' "$pe$pb$r$t" | grep -qE '^[0-9]+$'; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: unreadable (est_tokens='$pe' rows='$r' total='$t')"; continue + fi + truth=$(( pb * 100 / 250 )); floor=$(( pb * 100 / 255 )); diff=$(( pe - truth )); [ "$diff" -lt 0 ] && diff=$(( -diff )) + if [ $(( diff * 100 )) -gt $(( truth * 12 )) ] || [ "$pe" -lt "$floor" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: est_tokens=$pe does not price the $pb B delivered (bytes/2.50 = $truth, floor $floor)" 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" + if [ "$r" -ge "$t" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500 --legend=$lg: premise — $r of $t rows, nothing cut; the arm reads no soft cut" + elif [ "$lg" = compact ]; then + # NX: run the cut's next= (entity-decoded, shell-split) on the same root; it must serve the whole head uncut. + nargv="$( printf '%s' "$sigs" | python3 -c 'import sys,re,html,shlex; m=re.search(r"\snext=\"([^\"]+)\"",sys.stdin.read()); print("\n".join(shlex.split(html.unescape(m.group(1)))) if m else "")' )" + if [ -z "$nargv" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500: the cut's carries no next= to run" + else + nb=0; nbr="$( printf '%s\n' "$nargv" | sed -n 's/^--token-budget=\([0-9]*\)$/\1/p' | head -1 )"; [ -n "$nbr" ] && nb="$nbr" + ( cd "$P4" && printf '%s\n' "$nargv" | tr '\n' '\0' | xargs -0 "$BIN" . ) >"$TMP/p4n.out" 2>/dev/null + nl="$( python3 "$TMP/softp.py" "$TMP/p4n.out" "$nb" "widget routine dispatcher" "$TMP/p4n.rows" )" + nr="$( spfield "$nl" rows )"; nt="$( spfield "$nl" total )" + if printf '%s' "$nr$nt" | grep -qE '^[0-9]+$' && [ "$nr" -eq "$nt" ] && cmp -s "$TMP/p4n.rows" "$TMP/sp.ref.p4.compact.rows" \ + && head -n "$r" "$TMP/p4n.rows" | cmp -s - "$TMP/sp.compact.rows"; then + p4facts="$p4facts the 500 cut's next= ($( printf '%s' "$nargv" | tr '\n' ' ')) serves $nr of $nt rows, the no-budget head;" + else + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500: the cut's next= ($( printf '%s' "$nargv" | tr '\n' ' ')) served '${nl:-no }' — not the whole no-budget head with the cut's rows as its prefix" + fi + fi + fi + else + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are + # reserved before the rows past the cut, so 900 no longer serves the whole 12-row head (11 rows measured) — by design. + # What holds: the rows are a rank prefix of the no-budget answer's, and a cut carries capped + next=. + case "$sigs" in *' capped="1"'*) capd=1 ;; *) capd=0 ;; esac + case "$sigs" in *' next="'*) nxd=1 ;; *) nxd=0 ;; esac + if ! head -n "$r" "$TMP/sp.ref.p4.$lg.rows" | cmp -s - "$TMP/sp.$lg.rows" || { [ "$r" -lt "$t" ] && [ $(( capd * nxd )) -ne 1 ]; }; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=900 --legend=$lg: $r of $t rows, capped=$capd next=$nxd — the rows must be a rank prefix of the no-budget answer's, and a cut must carry capped + next=" 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 + p4facts="$p4facts $tb/$lg: $r of $t rows est $pe;" 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:-?}" + pc="$( spfield "$spLine_compact" est )"; pf="$( spfield "$spLine_full" est )" + if printf '%s' "$pc$pf" | grep -qE '^[0-9]+$'; then + lim=$tb; [ "$pf" -gt "$lim" ] && lim=$pf + [ "$pc" -gt "$lim" ] && { 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"; } + else + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: est_tokens unreadable (compact '$pc', full '$pf')" fi done -[ "$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 900 run's paid next= fits with no label, compact never further over than full" +[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) soft target on the twelve-routine fixture: at 500 both dialects serve the same maximal rank prefix within T, unlabelled, its next= serving the whole head, and with 900 the complete head ships uncut, est_tokens honest; compact never further over than full ($p4facts)" # (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/compactlegendfix/pre_l1_full/for.xml b/test/compactlegendfix/pre_l1_full/for.xml index ba0c0ada4..ed7922c7c 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/compactroutecheck.sh b/test/compactroutecheck.sh index 6f21b15a8..1cf56aa9b 100755 --- a/test/compactroutecheck.sh +++ b/test/compactroutecheck.sh @@ -44,7 +44,11 @@ echo "compactroutecheck: BIN=$BIN" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT -CONC="how are identifiers split into subtokens for ranking" # multi-word, no word is a whole symbol name +# for-body-slot (train 28; rule 4, by design): the question no longer opens with "how are" — an explanatory cue now carries +# ONE owner body beside the hops (forbodyslotcheck owns that contract; arm (1d) below reads the cue form). The compact +# contract pinned here is the conceptual route WITHOUT that cue, the same words otherwise. +CONC="identifiers split into subtokens for ranking" # multi-word, no word is a whole symbol name, no explanatory cue +CONC_CUE="how are identifiers split into subtokens for ranking" NAME="pageRankDouble" # name-exact: a small, stable anchor rw(){ "$BIN" src --no-cache "$@" 2>/dev/null; } @@ -77,6 +81,14 @@ grep -q '"$TMP/conccue" +n_cue="$( bodycount "$TMP/conccue" )" +if grep -q '' "$TMP/conc" \ diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index 14ed41c79..3eaeadeb3 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -47,6 +47,12 @@ # 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). +# P (L4) the grep CARRIER: a hit inside a variable/constant/initialiser prints reads=/reads_at= (the uses verb's read +# sites, Python and C++ where that verb indexes reads; the same sites --uses lists) and vals= (the function values it +# stores, slot=fn@definition line, path:line in another file); root next= becomes --uses= when reads > 0. +# Near misses: a carrier read nowhere (no reads=, next= stays the at verb), a string-only carrier (no vals=), a hit +# inside a function (row and next= unchanged), C/TS carriers (no reads=: not indexed), a C++ header carrier (no +# reads=: its .c readers are unseen). MCP twin: the same reads/reads_at/vals keys. # N legend: every new attribute is defined in the answer that carries it (compact and full dialects). # O determinism (x2) + xmllint. # @@ -406,14 +412,150 @@ for n in TestName Pair Handler; do fi done +# ── P: the grep CARRIER (L4, src/grepcarrier.h) ────────────────────────────────────────────────────────── +# A hit inside a variable/constant/initialiser: reads=/reads_at= (the uses verb's read sites, where it indexes reads) +# and vals= (the function values the carrier stores, @ the one definition each resolves to); next= the uses verb. +echo "=== P: grep on a carrier names its reads and the functions it stores ===" +mkdir -p "$W/pc/lib" "$W/cc" "$W/tsc" +printf 'def handle_get():\n return 1\n\ndef handle_put():\n return 2\n' > "$W/pc/lib/handlers.py" +printf 'from .handlers import handle_get\n\ndef local_put():\n return 3\n\nUNREAD = "PC_UNREAD_MARK"\nNAMED = "PC_NAMED_MARK"\nTABLE = {"get": handle_get, "put": local_put, "label": "PC_TABLE_MARK"}\n\ndef inner():\n return "PC_INNER_MARK"\n' > "$W/pc/lib/conf.py" +printf 'from . import conf\n\ndef first():\n return conf.NAMED\n\ndef second():\n x = conf.NAMED\n return x\n' > "$W/pc/lib/reader.py" +printf 'from lib import conf\n\ndef test_named():\n assert conf.NAMED\n' > "$W/pc/test_reader.py" +printf 'struct entry { const char *name; int (*exec)(int); int (*undo)(int); };\nint other_exec(int);\nstatic int local_exec(int v) { return v; }\nconst struct entry ent = {\n\t.name = "C_ENT_MARK",\n\t.exec = local_exec,\n\t.undo = other_exec\n};\nstatic const char *plain = "C_PLAIN_MARK";\nint body(void) { return 1 + (plain != 0); }\n' > "$W/cc/ent.c" +printf 'int other_exec(int v)\n{\n\treturn v + 1;\n}\n' > "$W/cc/other.c" +printf 'const char *kCppName = "CPP_NAME_MARK";\n' > "$W/cc/names.cpp" +printf 'extern const char *kCppName;\nconst char *use_name() { return kCppName; }\n' > "$W/cc/use.cpp" +printf 'extern const char *kHdrName;\nconst char *kHdrName = "CPP_HDR_MARK";\n' > "$W/cc/hdr.h" +printf '#include "hdr.h"\nconst char *use_hdr() { return kHdrName; }\n' > "$W/cc/usehdr.cpp" +printf 'function homeHandler() { return 1; }\nexport const ROUTES = { home: homeHandler, name: "TS_ROUTES_MARK" };\nexport function useRoutes() { return ROUTES.home; }\n' > "$W/tsc/routes.ts" + +OUT_P="$( run pc --grep=PC_NAMED_MARK )"; ROW_P="$( enc_row "$OUT_P" NAMED )" +[ -n "$ROW_P" ] || no "(P) premise: no row" +if [ "$( attr "$ROW_P" reads )" = 3 ]; then ok "(P) reads=\"3\" — the uses verb's three read sites of NAMED"; else no "(P) reads on NAMED: $ROW_P"; fi +if [ "$( attr "$ROW_P" reads_at )" = "lib/reader.py:4 lib/reader.py:7 test_reader.py:4" ]; then ok "(P) reads_at= lists them in tier, path, line order (source before tests, then path, line)"; else no "(P) reads_at on NAMED: $ROW_P"; fi +USES_P="$( run pc --uses=NAMED | grep -oE ' row" +case "$ROW_PU" in *reads=*|*reads_at=*) no "(P) near miss: a carrier read nowhere printed reads: $ROW_PU" ;; *) ok "(P) near miss: a carrier read nowhere carries no reads= (absent, never a zero)" ;; esac +if [ "$( attr "$R_PU" next )" = "--at=lib/conf.py:6" ]; then ok "(P) near miss: no reads => next= stays the at verb on the top hit"; else no "(P) near miss next: $R_PU"; fi + +ROW_PT="$( enc_row "$( run pc --grep=PC_TABLE_MARK )" TABLE )" +if [ "$( attr "$ROW_PT" vals )" = "[get]=handle_get@lib/handlers.py:1 [put]=local_put@3" ]; then ok "(P) vals= on a dict carrier: [key]=fn@its definition (path:line in another file)"; else no "(P) vals on TABLE: $ROW_PT"; fi + +ROW_PI="$( enc_row "$( run pc --grep=PC_INNER_MARK )" inner )"; R_PI="$( root_tag "$( run pc --grep=PC_INNER_MARK )" grep )" +[ -n "$ROW_PI" ] || no "(P) premise: no row" +case "$ROW_PI" in *reads=*|*vals=*) no "(P) negative: a literal in a function body got carrier attributes: $ROW_PI" ;; *) ok "(P) negative: a hit inside a function keeps its row unchanged" ;; esac +if [ "$( attr "$R_PI" next )" = "--at=lib/conf.py:11" ]; then ok "(P) negative: next= stays the at verb inside a function"; else no "(P) negative next: $R_PI"; fi + +ROW_PC="$( enc_row "$( run cc --grep=C_ENT_MARK )" ent )" +if [ "$( attr "$ROW_PC" vals )" = "exec=local_exec@3 undo=other_exec@other.c:1" ]; then ok "(P) C initialiser: vals=\"exec=local_exec@3 undo=other_exec@other.c:1\""; else no "(P) C initialiser row: $ROW_PC"; fi +case "$ROW_PC" in *reads=*) no "(P) C reads are not indexed, yet reads= printed: $ROW_PC" ;; *) ok "(P) C: no reads= (the uses verb indexes no C reads; floor_next stays the literal scan)" ;; esac +ROW_PCP="$( enc_row "$( run cc --grep=C_PLAIN_MARK )" plain )" +[ -n "$ROW_PCP" ] || no "(P) premise: no row" +case "$ROW_PCP" in *vals=*|*reads=*) no "(P) near miss: a C string constant got carrier attributes: $ROW_PCP" ;; *) ok "(P) near miss: a C constant holding a string and read in C carries neither attribute" ;; esac +ROW_PX="$( enc_row "$( run cc --grep=CPP_NAME_MARK )" kCppName )" +if [ "$( attr "$ROW_PX" reads )" = 1 ] && [ "$( attr "$ROW_PX" reads_at )" = "use.cpp:2" ]; then ok "(P) C++ (reads indexed): reads=\"1\" reads_at=\"use.cpp:2\""; else no "(P) C++ row: $ROW_PX"; fi +ROW_PH="$( enc_row "$( run cc --grep=CPP_HDR_MARK )" kHdrName )" +[ -n "$ROW_PH" ] || no "(P) premise: no row" +case "$ROW_PH" in *reads=*) no "(P) near miss: a header carrier counted reads its .c users hide: $ROW_PH" ;; *) ok "(P) near miss: a .h carrier carries no reads= (its readers may be .c files the uses verb never reads)" ;; esac +ROW_PS="$( enc_row "$( run tsc --grep=TS_ROUTES_MARK )" ROUTES )" +if [ "$( attr "$ROW_PS" vals )" = "home=homeHandler@1" ]; then ok "(P) TS object carrier: vals=\"home=homeHandler@1\""; else no "(P) TS row: $ROW_PS"; fi +case "$ROW_PS" in *reads=*) no "(P) TS reads are not indexed, yet reads= printed: $ROW_PS" ;; *) ok "(P) TS: no reads= (not indexed for TS)" ;; esac + +MCP_P="$( mcp_call grep "{\"path\":\"$W/pc\",\"pattern\":\"PC_NAMED_MARK\"}" )" +if printf '%s' "$MCP_P" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "NAMED", {} ) +sys.exit( 0 if r.get( "reads" ) == 3 and r.get( "reads_at" ) == [ "lib/reader.py:4", "lib/reader.py:7", "test_reader.py:4" ] else 1 )'; then ok "(P) MCP grep enclosing NAMED: reads 3 and the same reads_at as the CLI"; else no "(P) MCP grep NAMED: $MCP_P"; fi +if printf '%s' "$MCP_P" | python3 -c ' +import json, sys +sys.exit( 0 if json.loads( sys.stdin.read() ).get( "next" ) == "--uses=NAMED" else 1 )'; then ok "(P) MCP grep next is --uses=NAMED, as the CLI root next="; else no "(P) MCP grep next: $MCP_P"; fi +MCP_PC="$( mcp_call grep "{\"path\":\"$W/cc\",\"pattern\":\"C_ENT_MARK\"}" )" +if printf '%s' "$MCP_PC" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "ent", {} ) +sys.exit( 0 if r.get( "vals" ) == [ "exec=local_exec@3", "undo=other_exec@other.c:1" ] and "reads" not in r else 1 )'; then ok "(P) MCP grep enclosing ent: the same vals as the CLI, no reads key"; else no "(P) MCP grep ent: $MCP_PC"; fi + +# P/top (train-28 fix N1): reads are counted only when the TOP hit's carrier is readable. A page whose top hit is in a +# function and whose lower hit is in a readable carrier pays NO second ingest (the hard-line seam set for the reading +# that ingest would add never fires), and the lower carrier keeps floor_next= (the uses verb) as its clue, no reads=. +mkdir -p "$W/pm/lib" "$W/pw/a/lib" "$W/pw/b" +printf 'def early():\n return "PM_MIX_MARK"\n\nLATE = "PM_MIX_MARK"\n' > "$W/pm/lib/aa.py" +printf 'from lib import aa\n\ndef reader():\n return aa.LATE\n' > "$W/pm/lib/rd.py" +printf 'TOPV = "PW_WS_MARK"\n' > "$W/pw/a/lib/top.py" +printf 'from lib import top\n\ndef r2():\n return top.TOPV\n' > "$W/pw/a/lib/use.py" +printf 'OTHER = "PW_WS_MARK"\n\ndef r3():\n return OTHER\n' > "$W/pw/b/bb.py" +OUT_PM="$( cd "$W/pm" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PM_MIX_MARK 2>/dev/null )" +ROW_PM="$( enc_row "$OUT_PM" LATE )"; R_PM="$( root_tag "$OUT_PM" grep )" +[ -n "$ROW_PM" ] || no "(P/top) premise: no row" +[ -n "$( enc_row "$OUT_PM" early )" ] || no "(P/top) premise: no row (the top hit's function)" +case "$ROW_PM" in *reads=*|*reads_at=*) no "(P/top) a lower carrier counted reads although the top hit is in a function: $ROW_PM" ;; *) ok "(P/top) top hit in a function: the lower carrier LATE has no reads=" ;; esac +if [ "$( attr "$ROW_PM" floor_next )" = "--uses=LATE" ]; then ok "(P/top) the lower carrier keeps floor_next=\"--uses=LATE\" as the clue"; else no "(P/top) clue on LATE: $ROW_PM"; fi +case "$R_PM" in *reads_skipped=*) no "(P/top) a second ingest ran (the hard-line reading after it fired): $R_PM" ;; *) ok "(P/top) no second ingest: the hard-line reading it would add never happened" ;; esac +if [ "$( attr "$R_PM" next )" = "--at=lib/aa.py:2" ]; then ok "(P/top) next= stays the at verb on the top hit"; else no "(P/top) next: $R_PM"; fi +MCP_PM="$( mcp_call grep "{\"path\":\"$W/pm\",\"pattern\":\"PM_MIX_MARK\"}" )" +if printf '%s' "$MCP_PM" | python3 -c ' +import json, sys +r = { x["n"]: x for x in json.loads( sys.stdin.read() ).get( "enclosing", [] ) }.get( "LATE" ) +sys.exit( 0 if r is not None and "reads" not in r and r.get( "floor_next" ) == "--uses=LATE" else 1 )'; then ok "(P/top) MCP: the same page, the same verdict (no reads key on LATE, floor_next clue)"; else no "(P/top) MCP LATE: $MCP_PM"; fi + +# P/mem (fix N4): the second ingest runs under the memory guard. Over the hard line after it (seam: the third hard-line +# reading of a single-root grep, after main's ingest and graph readings), the answer keeps the lean rows and its root +# discloses the skipped reads in band, with the call that recovers them; exit 0, nothing refused. +OUT_PX="$( cd "$W/pc" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PC_NAMED_MARK 2>/dev/null; echo "rc=$?" )" +R_PX="$( root_tag "$OUT_PX" grep )"; ROW_PXM="$( enc_row "$OUT_PX" NAMED )" +[ -n "$ROW_PXM" ] || no "(P/mem) premise: no row on the stopped answer" +if [ "$( attr "$R_PX" reads_skipped )" = hard_limit ] && [ "$( attr "$R_PX" reads_next )" = "--uses=NAMED" ]; then ok "(P/mem) stopped: root reads_skipped=\"hard_limit\" reads_next=\"--uses=NAMED\""; else no "(P/mem) root on a stopped reads ingest: $R_PX"; fi +case "$ROW_PXM" in *reads=*) no "(P/mem) a stopped ingest still printed reads=: $ROW_PXM" ;; *) ok "(P/mem) stopped: no reads= on the row (unknown, never a short count)" ;; esac +case "$OUT_PX" in *rc=0) ok "(P/mem) stopped: exit 0, the lean answer stands" ;; *) no "(P/mem) stopped exit: ${OUT_PX##*rc=}" ;; esac +comments "$( cd "$W/pc" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PC_NAMED_MARK --legend=compact 2>/dev/null )" | grep -qF "reads_skipped=" \ + && ok "(P/mem) compact legend defines reads_skipped= on that answer" || no "(P/mem) compact legend lacks reads_skipped=" +comments "$( cd "$W/pc" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PC_NAMED_MARK --legend=full 2>/dev/null )" | grep -qF "reads_next" \ + && ok "(P/mem) full legend defines reads_next on that answer" || no "(P/mem) full legend lacks reads_next" +case "$OUT_P" in *reads_skipped*) no "(P/mem) near miss: an unstopped answer carries reads_skipped" ;; *) ok "(P/mem) near miss: an unstopped answer carries no reads_skipped (nor its legend)" ;; esac + +# P/cache (fix N5): an explicit --cache=F is honoured — the second ingest writes no rich auto-cache under TMPDIR. +mkdir -p "$W/tmpd" +OUT_PK="$( cd "$W/pc" && TMPDIR="$W/tmpd" XDG_CACHE_HOME="$W/tmpd" "$BIN" . --cache="$W/named.bin" --grep=PC_NAMED_MARK 2>/dev/null )" +[ "$( attr "$( enc_row "$OUT_PK" NAMED )" reads )" = 3 ] || no "(P/cache) premise: reads=\"3\" under --cache= (the second ingest ran)" +if [ -z "$( find "$W/tmpd" -type f 2>/dev/null )" ] && [ -f "$W/named.bin" ]; then ok "(P/cache) --cache=F: F written, no hidden rich auto-cache under TMPDIR"; else no "(P/cache) files under TMPDIR: $( find "$W/tmpd" -type f | tr '\n' ' ' )"; fi +OUT_PK2="$( cd "$W/pc" && TMPDIR="$W/tmpd" XDG_CACHE_HOME="$W/tmpd" "$BIN" . --cache="$W/named.bin" --grep=PC_NAMED_MARK 2>/dev/null )" +if [ "$OUT_PK" = "$OUT_PK2" ]; then ok "(P/cache) warm == cold under --cache=F"; else no "(P/cache) warm and cold answers differ under --cache=F"; fi + +# P/ws (fix N6): reads= on a workspace (multi-root) run — the CLI re-ingests each root rich and merges, so it prints the +# same reads/reads_at as the MCP workspace index (rich) on the same page. +OUT_PW="$( "$BIN" "$W/pw/a" "$W/pw/b" --no-cache --grep=PW_WS_MARK 2>/dev/null )" +ROW_PWA="$( enc_row "$OUT_PW" TOPV )"; ROW_PWB="$( enc_row "$OUT_PW" OTHER )" +if [ "$( attr "$ROW_PWA" reads_at )" = "a/lib/use.py:4" ] && [ "$( attr "$ROW_PWB" reads_at )" = "b/bb.py:4" ]; then ok "(P/ws) CLI workspace: reads_at= per root, label/path"; else no "(P/ws) CLI workspace rows: $ROW_PWA $ROW_PWB"; fi +if [ "$( attr "$( root_tag "$OUT_PW" grep )" next )" = "--uses=TOPV" ]; then ok "(P/ws) CLI workspace next=\"--uses=TOPV\""; else no "(P/ws) CLI workspace next: $( root_tag "$OUT_PW" grep )"; fi +MCP_PW="$( mcp_call grep "{\"paths\":[\"$W/pw/a\",\"$W/pw/b\"],\"pattern\":\"PW_WS_MARK\"}" )" +CLI_PW="$( for n in TOPV OTHER; do r="$( enc_row "$OUT_PW" "$n" )"; printf '%s %s %s\n' "$n" "$( attr "$r" reads )" "$( attr "$r" reads_at )"; done )" +MCPS_PW="$( printf '%s' "$MCP_PW" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +for x in d.get( "enclosing", [] ): + print( x["n"], x.get( "reads", "" ), " ".join( x.get( "reads_at", [] ) ) ) +print( "next", d.get( "next" ) )' )" +if [ "$MCPS_PW" = "$CLI_PW +next --uses=TOPV" ]; then ok "(P/ws) MCP workspace: reads/reads_at/next identical to the CLI"; else no "(P/ws) MCP vs CLI: [$MCPS_PW] vs [$CLI_PW]"; fi +OUT_PWS="$( RIPWIRE_TEST_MEMGUARD=hard:4 "$BIN" "$W/pw/a" "$W/pw/b" --no-cache --grep=PW_WS_MARK 2>/dev/null )" +if [ "$( attr "$( root_tag "$OUT_PWS" grep )" reads_skipped )" = hard_limit ] && [ -z "$( attr "$( enc_row "$OUT_PWS" OTHER )" reads )" ]; then ok "(P/ws) a workspace stopped after its first rich root: reads_skipped=, no partial reads= on any row"; else no "(P/ws) stopped workspace: $( root_tag "$OUT_PWS" grep )"; 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 )" + L_P="$( run pc --grep=PC_TABLE_MARK --legend=$dialect )"; L_PN="$( run pc --grep=PC_NAMED_MARK --legend=$dialect )" for pair in "L_A:callers_floor=" "L_A:uses_floor=" "L_A:unmodelled" "L_C:sites_at=" "L_E:callers_floor=" "L_E:floor_next=" \ - "L_K:implementors_floor=" "L_K:implementors_shown=" "L_K:floor_next="; do + "L_K:implementors_floor=" "L_K:implementors_shown=" "L_K:floor_next=" "L_P:vals=" "L_PN:reads=" "L_PN:reads_at="; 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" @@ -422,10 +564,16 @@ done OUT_N="$( run c --safe-delete=helper_dead --legend=compact )" [ -n "$( root_tag "$OUT_N" safe-delete )" ] || no "(N) premise: no answer for helper_dead (an empty answer must not read as present-only)" case "$OUT_N" in *callers_floor*|*uses_floor*|*sites_l*|*sites_at*) 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 +for dialect in compact full; do + OUT_NP="$( run pc --grep=PC_INNER_MARK --legend=$dialect )" + [ -n "$( root_tag "$OUT_NP" grep )" ] || no "(N) premise: no answer for PC_INNER_MARK" + case "$OUT_NP" in *reads_at*|*vals=*) no "(N/$dialect) a legend defines a carrier term its answer does not carry" ;; *) ok "(N/$dialect) present-only: no carrier term on an answer without a carrier attribute" ;; esac +done # ── 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 +for spec in "c --safe-delete=cell" "c --safe-delete=width_of" "py --grep=APP_BACKEND_ENV" "tsl --lego=Router" "pc --grep=PC_NAMED_MARK" "cc --grep=C_ENT_MARK" \ + "pc --grep=PC_TABLE_MARK --legend=full"; 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 diff --git a/test/crashsweepcheck.sh b/test/crashsweepcheck.sh index 33a3b4912..206f7314d 100755 --- a/test/crashsweepcheck.sh +++ b/test/crashsweepcheck.sh @@ -343,6 +343,7 @@ pincensus.h writePinCensus fopen 1 closes returns only on a failed open; one fcl planlint.h gitBlameLineSha popen 1 closes no exit between popen and pclose prcontext.h numstatChangedPaths popen 1 closes rc = pclose after the loop; no exit between quality.h SidecarWriteLock open 1 owned SidecarWriteLock's destructor unlocks and closes +quality.h forEachGitNulRecord popen 1 closes fread loop with no exit; pclose after it (qd-memory-degrade: the changed-files listing) quality.h gitBlameRangeWindowCommits popen 1 closes continue-only loop; pclose after it quality.h gitDiffHunksVsHead popen 1 closes continue-only loop; pclose after it quality.h gitRepoHasHistory popen 1 closes fgets-only loop, then pclose diff --git a/test/deckcheck_allowlist.txt b/test/deckcheck_allowlist.txt index 226d041e7..055403677 100644 --- a/test/deckcheck_allowlist.txt +++ b/test/deckcheck_allowlist.txt @@ -135,3 +135,6 @@ --langs test/langidentitycheck.sh's own option ("langidentitycheck.sh BASE HEAD --langs=", the language tags whose fixtures run) quoted in CONTRIBUTING.md's common-code Definition of done, not a ripwire flag --syms test/langidentitycheck.sh's own option ("--syms=FILE", the symbols a slice touched) quoted in the same CONTRIBUTING.md command as --langs above, not a ripwire flag --intended test/langidentitycheck.sh's own option ("--intended" rows that waive a DIFF) quoted in the same CONTRIBUTING.md paragraph as --langs above, not a ripwire flag +--no-ext-diff git's own flag, named in the CHANGELOG's git-subprocess hardening entry as an option ripwire passes to its own git diff/log/show/blame children, not a ripwire flag +--no-textconv git's own flag, same CHANGELOG hardening entry as --no-ext-diff above, not a ripwire flag +--ignore-submodules git's own flag ("--ignore-submodules=dirty" on status/diff), same CHANGELOG hardening entry as --no-ext-diff above, not a ripwire flag diff --git a/test/declinedsitecheck.sh b/test/declinedsitecheck.sh new file mode 100755 index 000000000..6c07c3a5a --- /dev/null +++ b/test/declinedsitecheck.sh @@ -0,0 +1,645 @@ +#!/usr/bin/env bash +# declinedsitecheck.sh — the call SITES the resolver declined are NAMED, as hedged rows that are never a resolved edge. +# +# test/declinedsitecheck.sh [BIN] # BIN defaults to $RIPWIRE_BIN, then build/ripwire +# +# THE GAP. A tier-3 decline (test/declinecheck.sh) or a builtin/standard-member gate decline leaves a call with no +# edge and counts it: --callees answers declined_calls="6", --callers declined_calls="33". The count is honest, but +# it names nothing: buildGraph interns only the candidate LIST (graph.h internDeclinedList, shared by every call with +# the same list), so no answer can say WHICH call, on which line, in which caller. A question "what does doPackage +# call" then misses `pkg.Package( info, f )` through an interface value, and "who calls TaskManager#define_task" +# misses every test that calls it on an untyped receiver — both one call away from the answer. +# +# THE CONTRACT (design: the lane's Phase A note). +# * At every decline site buildGraph also records ( caller, list index, line ): one record per declined CALL. +# * --callees=X: one row per declined call X makes, after the +# rows; a gate decline says which gate: gate="builtin" (the builtin-method name gate) or gate="std" (the C++ +# standard-member gate); a tier-3 decline carries no gate=. +# * --callers=T (and --safe-delete=T): one row per declined +# call whose candidate LIST holds a definition of T (membership, never the called name: two lists can share a name). +# * Ruby: a call the side cut declined (in reach, on the other side of self) reads gate="side"; a declined `new` names +# the called name (n="new"), never its `initialize` candidates. +# * NEVER an edge: count=, the rows, hop_tested=/hop_untested= and every MCP "count" are exactly what the +# pre-change binary printed; a row is its own element and never carries via="name" or prov=. +# * Capped AND counted: declined_calls= stays the total; past the cap the root says dc_shown="N" (rows printed) and +# dc_next= (the pasteable call that lists every site); rows past the cap are chosen file-first (each file's first +# site before any file's second), so a cut still names every file it can. +# * Candidates are named only on --legend=full (cand="FILE:LINE ...", the definitions the call could have meant); +# the default rows carry the count alone (cands=). +# * Multi-definition selectors (attrib-multidef): a callees-form carries of_def="K" (the definition that makes +# the call, a fact); a callers-form carries ONLY hedged tokens K~d ("this declined call's candidates include +# group K"), never a bare K. +# * Legend: an answer with rows defines them, saying what they do NOT mean ("the resolver saw K definitions of +# this name and proved none; not a claim this call reaches any of them"); an answer without carries no clause. +# * Dialects: CLI --json "dc":[{"n","l","cands","via"}] / [{"in","p","cands"}]; --format=columnar keeps the +# rows verbatim; the MCP twins (find_symbol, find_referencing_symbols) carry "declined_sites":[...] with the same +# sites ({"name","line","cands","via"} / {"in","file","line","cands"}) and an unchanged "count". +# * --safe-delete: a definition with no caller and no use but a declined call that could have meant it reads +# risk="possible-callers", never "none-found", and lists the rows; --impact and --for print no row. +# +# ARMS (fixture test/declinedsitefix: go/ py/ rs/ ts/ cpp/ rb/, plus test/declinefix, test/builtinbindfix, +# test/stdmembergatefix and test/lpinfix): +# (A) the three shapes the rows ask about: a Go interface value (pkg.ConventionalFileName twice, pkg.Package), a Rust +# &dyn trait call (preprocessor.run), Ruby test-DSL calls of TaskManager#define_task from three test files — +# every site named with its line, every candidate with --legend=full, none by default +# (B) language neutrality: the 16 ladder languages of test/declinefix (callees and callers form), plus TS (an +# interface-typed receiver), C++ (a template parameter receiver) and Python (two declines sharing ONE list keep +# two rows); Elixir's lexical refusal is not a decline and gets no row +# (C) near misses: a resolved call, a via="name" row and an external call never get a ; a builtin-gate and a +# std-gate decline stay labelled as such; count= and the rows are the pre-change binary's (pinned) +# (D) multi-definition labels (of_def on ) +# (E) dialects: --json, columnar, the MCP twins (same sites, same count), and every answer parses as XML +# (F) legend: defined where rows ride, absent elsewhere (and on the zero-decline corpus test/lpinfix) +# (G) the cap, on generated trees: callers and callees past the cap are counted, file-first, with dc_next= +# (H) safe-delete never reads none-found beside a declined possible caller; impact and for print no +# (K) the gates review's amendments: R1 the three Ruby decline sites (new, side, side over the assembled set) and +# the W1 discriminator (callers by candidate list, never by name); R2 rows == declined_calls= on every uncut +# answer (== declined_iface= on the TS interface answers); R3 W1 on the declinedlistcheck shapes (language and +# root filters split one name into two lists); R4 the default attribute whitelist (no candidates leak under +# another key, CLI JSON or MCP); R5 every other root attribute is the pre-change literal; R6 dc_next= runs and +# names every declined line; R7 the cut is file-first in path order; R8 safe-delete's possible-callers is +# per definition (control none-found; the TS interface method flips); R9/R10 the legend drops "no count or +# row" where rows ride and reads the review's hedge wording +# (N) a proven Ruby subscript the side cut declines (`self[ k ]` in a class method, the `def []` an instance method; +# the same through an included module): the record names the subscript's own reference — rc 0, no contract +# abort (the ladder runs on a copy of the site, so the record cannot be derived from the copy's address) +# (I) determinism; (J) the predicates can fail +# +# RED ON BASE: every [P] arm fails on a binary without the record (no row anywhere); [C] arms are controls that +# pass on both. Exits non-zero on any failure. + +set -u +export PYTHONDONTWRITEBYTECODE=1 +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$PWD/$BIN" # a relative binary is relative to the caller's cwd +echo "declinedsitecheck: BIN=$BIN" +[ -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; } +for d in declinedsitefix declinefix builtinbindfix stdmembergatefix lpinfix; do + [ -d "$ROOT/test/$d" ] || { echo "fixture missing: test/$d"; exit 2; } +done +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +# Every arm runs in the Python below; its exit status (1 on any FAIL arm, anything else on a crash) is the accumulator's input. +python3 - "$BIN" "$ROOT" "$TMP" <<'PY' || no "the arms above did not all pass (or the checker itself failed): python rc != 0" +import json, os, re, subprocess, sys +import xml.etree.ElementTree as ET + +BIN, ROOT, TMP = sys.argv[1], sys.argv[2], sys.argv[3] +T = os.path.join( ROOT, "test" ) +FIX, DF = os.path.join( T, "declinedsitefix" ), os.path.join( T, "declinefix" ) +BB, SM, CLEAN = os.path.join( T, "builtinbindfix" ), os.path.join( T, "stdmembergatefix" ), os.path.join( T, "lpinfix" ) +npass = nfail = 0 + +def say( good, tag, msg, why = "" ): + global npass, nfail + if good: + npass += 1; print( " PASS %s: %s" % ( tag, msg ) ) + else: + nfail += 1; print( " FAIL %s: %s%s" % ( tag, msg, ( " -- " + why ) if why else "" ) ) + +def run( root, *args ): + """( rc, stdout ) of one CLI answer; rc 124 on a timeout.""" + try: + p = subprocess.run( [ BIN, root, "--no-cache" ] + list( args ), capture_output = True, text = True, timeout = 240 ) + return p.returncode, p.stdout + except subprocess.TimeoutExpired: + return 124, "" + +def mcp( tool, arguments ): + """The MCP tool's text payload parsed as JSON, or None (initialize + one tools/call over stdio).""" + req = json.dumps( { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} } ) + "\n" + json.dumps( + { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": tool, "arguments": arguments } } ) + "\n" + try: + p = subprocess.run( [ BIN, "--mcp" ], input = req, capture_output = True, text = True, timeout = 240 ) + except subprocess.TimeoutExpired: + return None + for line in p.stdout.splitlines(): + try: + r = json.loads( line ) + except ValueError: + continue + if r.get( "id" ) == 2 and "result" in r: + try: + return json.loads( r[ "result" ][ "content" ][ 0 ][ "text" ] ) + except ( ValueError, KeyError, IndexError, TypeError ): + return None + return None + +class Answer: + """One XML answer: its root element, the / rows, the rows (anywhere), the leading legend comments.""" + def __init__( self, rc, text, tag ): + self.rc, self.text, self.tag = rc, text, tag + self.root, self.err = None, "" + m = re.match( r"(\s*)+", text, re.S ) + self.legend = m.group( 0 ) if m else "" + try: + doc = ET.fromstring( "" + text + "" ) + self.root = doc.find( tag ) + except ET.ParseError as e: + self.err = "not well-formed XML: %s" % e + self.dc = [ dict( e.attrib ) for e in self.root.iter( "dc" ) ] if self.root is not None else [] + rowtag = "c" if tag == "safe-delete" else "s" + self.rows = [ dict( e.attrib ) for e in self.root.findall( rowtag ) ] if self.root is not None else [] + @property + def ok( self ): + return self.rc == 0 and self.root is not None + def a( self, k ): + return self.root.get( k ) if self.root is not None else None + def why( self ): + if self.rc != 0: return "rc=%s" % self.rc + if self.root is None: return self.err or "no <%s> root" % self.tag + return "root %s; dc=%s" % ( dict( self.root.attrib ), self.dc ) + +def ask( root, verb, sel, *more ): + rc, out = run( root, "--%s=%s" % ( verb, sel ), *more ) + return Answer( rc, out, verb if verb != "safe-delete" else "safe-delete" ) + +def dc_sites( ans, key ): + return sorted( ( d.get( key[ 0 ] ), d.get( key[ 1 ] ), d.get( "cands" ) ) for d in ans.dc ) + +def line_of( path, needle ): + with open( path, encoding = "utf-8" ) as f: + for i, line in enumerate( f, 1 ): + if needle in line: + return str( i ) + return "?" + +LEG_DONT = "proved none" +LEG_NOT = "not a claim this call reaches any of them" + +# ── (A) the three shapes ──────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (A) the declined sites the rows ask about are named ===" ) +GO, RS, RB = os.path.join( FIX, "go" ), os.path.join( FIX, "rs" ), os.path.join( FIX, "rb" ) +a = ask( GO, "callees", "doPackage" ) +want = WANT_GO = [ ( "ConventionalFileName", "16", "2" ), ( "ConventionalFileName", "18", "2" ), ( "Package", "24", "2" ) ] +say( a.ok and dc_sites( a, ( "n", "l" ) ) == want, "[P] a_go", + "go --callees=doPackage names pkg.ConventionalFileName@16, @18 and pkg.Package@24 (cands=2 each)", a.why() ) +say( a.ok and all( d.get( "via" ) == "declined" and "gate" not in d for d in a.dc ) and len( a.dc ) == 3, "[P] a_go_via", + "each tier-3 row reads via=\"declined\" and carries no gate=", a.why() ) +say( a.ok and a.a( "count" ) == "2" and a.a( "declined_calls" ) == "3" and sorted( r.get( "n" ) for r in a.rows ) == [ "Get", "WithDefaults" ], + "[C] a_go_count", "count=\"2\" and the two resolved rows are the pre-change answer; declined_calls=\"3\" stays the total", a.why() ) +say( a.ok and not ( { d.get( "n" ) for d in a.dc } & { "Get", "WithDefaults", "Create", "Stat" } ), "[C] a_go_neg", + "the resolved (pkgr.Get, pkgr.WithDefaults) and external (os.Create) calls get no ", a.why() ) +say( a.ok and not any( "cand" in d for d in a.dc ), "[C] a_go_nocand", "default rows carry no candidate names (cand=)", a.why() ) +af = ask( GO, "callees", "doPackage", "--legend=full" ) +cands = { ( d.get( "n" ), d.get( "l" ) ): set( d.get( "cand", "" ).split() ) for d in af.dc } +say( af.ok and cands.get( ( "Package", "24" ) ) == { "deb/deb.go:15", "rpm/rpm.go:15" } + and cands.get( ( "ConventionalFileName", "16" ) ) == { "deb/deb.go:11", "rpm/rpm.go:11" }, "[P] a_go_full", + "--legend=full lists each row's candidates as file:line (Deb and RPM; not the interface's bodyless signature)", af.why() ) + +r = ask( RS, "callees", "preprocess_book" ) +say( r.ok and dc_sites( r, ( "n", "l" ) ) == [ ( "run", "18", "2" ) ], "[P] a_rs", + "rust --callees=preprocess_book names preprocessor.run@18 through Box (cands=2)", r.why() ) +say( r.ok and r.a( "count" ) == "2" and sorted( x.get( "n" ) for x in r.rows ) == [ "new", "preprocessor_should_run" ], "[C] a_rs_count", + "count=\"2\" (PreprocessorContext::new, preprocessor_should_run) unchanged", r.why() ) +rf = ask( RS, "callees", "preprocess_book", "--legend=full" ) +say( rf.ok and len( rf.dc ) == 1 and set( rf.dc[ 0 ].get( "cand", "" ).split() ) == { "src/builtin/index/mod.rs:10", "src/builtin/links/mod.rs:10" }, + "[P] a_rs_full", "--legend=full names LinkPreprocessor::run and IndexPreprocessor::run", rf.why() ) + +b = ask( RB, "callers", "lib/rake/task_manager.rb:define_task" ) +want = WANT_RB = sorted( [ ( "test_create_task_manager", "test/test_task_manager.rb:9", "2" ), ( "test_define_two", "test/test_task_manager.rb:13", "2" ), + ( "test_define_two", "test/test_task_manager.rb:14", "2" ), ( "test_load_rakefile", "test/test_application.rb:5", "2" ), + ( "test_top_level", "test/test_application.rb:9", "2" ), ( "test_namespace_lookup", "test/test_name_space.rb:6", "2" ) ] ) +say( b.ok and dc_sites( b, ( "in", "p" ) ) == want, "[P] a_rb", + "ruby --callers=TaskManager#define_task names the six test-DSL sites in all three test files", b.why() ) +say( b.ok and b.a( "count" ) == "2" and b.a( "declined_calls" ) == "6" and len( b.rows ) == 2, "[C] a_rb_count", + "count=\"2\" (Task.define_task, synthesize_file_task) and declined_calls=\"6\" unchanged", b.why() ) + +# ── (B) language neutrality ──────────────────────────────────────────────────────────────────────────────────────── +print( "=== (B) every ladder language names its declined site ===" ) +# lang | caller | callee | call file (relative to test/declinefix) | one definition selector +table = [ + ( "java", "javaDeclined", "jbody", "java/caller/JavaCaller.java", "java/alpha/Alpha.java:jbody" ), + ( "cpp", "cppDeclined", "crender", "cpp/caller/caller.cpp", "cpp/alpha/alpha.cpp:crender" ), + ( "py", "py_declined", "pyfetch", "py/caller/caller.py", "py/alpha/alpha.py:pyfetch" ), + ( "rust", "rust_declined", "rfetch", "rust/caller/caller.rs", "rust/alpha/alpha.rs:rfetch" ), +] +for lang, caller, callee in [ ( "ts", "tsDeclined", "tsHop" ), ( "js", "jsDeclined", "jsHop" ), ( "go", "GoDeclined", "GoHop" ), + ( "swift", "swiftDeclined", "swiftHop" ), ( "objc", "objcDeclined", "objcHop" ), + ( "bash", "bash_declined", "bash_hop" ), ( "ruby", "ruby_declined", "ruby_hop" ), + ( "csharp", "CsDeclined", "CsHop" ), ( "c", "c_declined", "c_hop" ), ( "php", "phpDeclined", "phpHop" ), + ( "lua", "lua_declined", "lua_hop" ), ( "dart", "dartDeclined", "dartHop" ) ]: + cdir, adir = os.path.join( DF, "sweep", lang, "c" ), os.path.join( DF, "sweep", lang, "a" ) + cfile = [ f for f in sorted( os.listdir( cdir ) ) ][ 0 ] + afile = [ f for f in sorted( os.listdir( adir ) ) ][ 0 ] + table.append( ( lang, caller, callee, "sweep/%s/c/%s" % ( lang, cfile ), "sweep/%s/a/%s:%s" % ( lang, afile, callee ) ) ) +for lang, caller, callee, cfile, defsel in table: + call_line = line_of( os.path.join( DF, cfile ), callee ) + x = ask( DF, "callees", caller ) + say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( callee, call_line, "2" ) ] and x.a( "count" ) == "0", "[P] b_%s" % lang, + "%s: --callees=%s names %s@%s (cands=2), count=\"0\"" % ( lang, caller, callee, call_line ), x.why() ) + y = ask( DF, "callers", defsel ) + say( y.ok and dc_sites( y, ( "in", "p" ) ) == [ ( caller, "%s:%s" % ( cfile, call_line ), "2" ) ] and y.a( "count" ) == "0", + "[P] b_%s_callers" % lang, "%s: --callers=%s names %s at %s:%s" % ( lang, defsel, caller, cfile, call_line ), y.why() ) +x = ask( DF, "callees", "ex_declined" ) +say( x.ok and x.dc == [] and x.a( "graph_unresolved" ) == "2", "[C] b_elixir", + "elixir: the lexical refusal is counted unresolved, not declined, and gets no ", x.why() ) +TS, CPP, PYF = os.path.join( FIX, "ts" ), os.path.join( FIX, "cpp" ), os.path.join( FIX, "py" ) +x = ask( TS, "callees", "dispatch" ) +say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( "match", "8", "2" ) ] and x.a( "count" ) == "1", "[P] b_ts_iface", + "ts: router.match@8 on an interface-typed receiver is named; clean() stays the one row", x.why() ) +x = ask( CPP, "callees", "drive" ) +say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( "run", "9", "2" ) ] and x.a( "count" ) == "1", "[P] b_cpp_tmpl", + "c++: engine.run@9 on a template-parameter receiver is named; twice() stays the one row", x.why() ) +x = ask( PYF, "callees", "caller/use.py:load_twice" ) +say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( "fetch", "11", "2" ), ( "fetch", "12", "2" ) ], "[P] b_py_shared", + "python: two declines sharing ONE interned candidate list keep two rows, one per line", x.why() ) + +# ── (C) near misses ──────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (C) near misses: no where nothing was declined; gate declines stay labelled ===" ) +x = ask( GO, "callees", "onlyResolved" ) +say( x.ok and x.dc == [] and x.a( "declined_calls" ) is None and not any( k.startswith( "dc_" ) for k in x.root.attrib ) + and ", no dc_ attribute, no legend clause", x.why() ) +x = ask( PYF, "callees", "fetch_all" ) +say( x.ok and x.dc == [] and len( x.rows ) == 2 and all( r.get( "via" ) == "name" for r in x.rows ), "[C] c_byname", + "via=\"name\" rows (a same-directory by-name binding) are edges, never ", x.why() ) +x = ask( BB, "callees", "py/plain.py:param_get" ) +say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "get" and x.dc[ 0 ].get( "gate" ) == "builtin" and x.a( "count" ) == "0", + "[P] c_gate_builtin", "the builtin-method gate's decline (config.get) reads gate=\"builtin\"", x.why() ) +x = ask( SM, "callees", "stdAppend" ) +stdm = x.root.find( "stdm" ) if x.root is not None else None +say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "append" and x.dc[ 0 ].get( "gate" ) == "std" and stdm is not None, + "[P] c_gate_std", "the C++ standard-member gate's decline reads gate=\"std\", beside its unchanged line", x.why() ) +for tag, root, verb, sel in [ ( "go", GO, "callees", "doPackage" ), ( "rs", RS, "callees", "preprocess_book" ), + ( "rb", RB, "callers", "lib/rake/task_manager.rb:define_task" ), ( "py", PYF, "callers", "fetch" ) ]: + x = ask( root, verb, sel ) + say( x.ok and len( x.rows ) == int( x.a( "count" ) or -1 ) and not any( r.get( "via" ) == "declined" for r in x.rows ) + and not any( "prov" in d or d.get( "via" ) == "name" for d in x.dc ), "[C] c_count_%s" % tag, + "%s --%s=%s: rows == count=, no row reads via=\"declined\", no reads like an edge" % ( tag, verb, sel ), x.why() ) + +# ── (D) multi-definition labels ──────────────────────────────────────────────────────────────────────────────────── +print( "=== (D) of_def on rows ===" ) +x = ask( PYF, "callees", "load" ) +got = sorted( ( d.get( "l" ), d.get( "of_def" ) ) for d in x.dc ) +say( x.ok and x.a( "defs" ) == "2" and got == [ ( "2", "2" ), ( "7", "1" ) ], "[P] d_callees", + "callees of two definitions: each names the definition that makes the call (of_def=\"1\" / \"2\", a fact)", x.why() ) +x = ask( PYF, "callers", "fetch" ) +toks = [ set( d.get( "of_def", "" ).split() ) for d in x.dc ] +say( x.ok and len( x.dc ) == 4 and all( t == { "1~d", "2~d" } for t in toks ), "[P] d_callers", + "callers of two definitions: every reads of_def=\"1~d 2~d\" (hedged: its candidates hold both), never a bare K", x.why() ) +say( x.ok and "~d" in x.legend, "[P] d_legend", "the legend defines the ~d mark", x.why() ) + +# ── (E) dialects ─────────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (E) --json, columnar, MCP ===" ) +rc, out = run( GO, "--callees=doPackage", "--json" ) +try: + j = json.loads( out ) +except ValueError: + j = {} +jd = sorted( ( d.get( "n" ), str( d.get( "l" ) ), str( d.get( "cands" ) ) ) for d in j.get( "dc", [] ) ) +say( rc == 0 and jd == WANT_GO and len( j.get( "callees", [] ) ) == 2 and j.get( "count" ) == 2, "[P] e_json_callees", + "--json carries \"dc\" with the XML sites; \"callees\" and \"count\" unchanged", "rc=%s dc=%s" % ( rc, j.get( "dc" ) ) ) +rc, out = run( RB, "--callers=lib/rake/task_manager.rb:define_task", "--json" ) +try: + j = json.loads( out ) +except ValueError: + j = {} +jd = sorted( ( d.get( "in" ), d.get( "p" ), str( d.get( "cands" ) ) ) for d in j.get( "dc", [] ) ) +say( rc == 0 and jd == WANT_RB and j.get( "count" ) == 2, "[P] e_json_callers", + "callers --json carries the same six sites", "rc=%s dc=%s" % ( rc, j.get( "dc" ) ) ) +rc, out = run( GO, "--callees=doPackage", "--format=columnar" ) +c = Answer( rc, out, "callees" ) +say( c.ok and dc_sites( c, ( "n", "l" ) ) == WANT_GO, "[P] e_columnar", "columnar keeps the rows verbatim", c.why() ) +m = mcp( "find_symbol", { "path": GO, "symbol": "doPackage" } ) or {} +md = sorted( ( d.get( "name" ), str( d.get( "line" ) ), str( d.get( "cands" ) ) ) for d in m.get( "declined_sites", [] ) ) +say( md == WANT_GO and m.get( "count" ) == 2 and all( d.get( "via" ) == "declined" for d in m.get( "declined_sites", [] ) ), + "[P] e_mcp_callees", "MCP find_symbol: \"declined_sites\" == the CLI rows, \"count\" unchanged", "declined_sites=%s count=%s" % ( m.get( "declined_sites" ), m.get( "count" ) ) ) +say( m != {} and not any( str( x.get( "via", "" ) ) == "declined" for x in m.get( "calls", [] ) ), "[C] e_mcp_calls", + "MCP \"calls\" holds no declined site", str( m.get( "calls" ) ) ) +m = mcp( "find_referencing_symbols", { "path": RB, "symbol": "lib/rake/task_manager.rb:define_task" } ) or {} +md = sorted( ( d.get( "in" ), "%s:%s" % ( d.get( "file" ), d.get( "line" ) ), str( d.get( "cands" ) ) ) for d in m.get( "declined_sites", [] ) ) +say( md == WANT_RB and m.get( "count" ) == 2, "[P] e_mcp_callers", + "MCP find_referencing_symbols: the same six sites, \"count\" unchanged", "declined_sites=%s count=%s" % ( m.get( "declined_sites" ), m.get( "count" ) ) ) +for tag, ans in [ ( "full_go", af ), ( "full_rs", rf ) ]: + say( ans.ok and ans.err == "", "[C] e_xml_%s" % tag, "the --legend=full answer with rows is well-formed XML", ans.why() ) + +# ── (F) legend ───────────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (F) the legend says what a row does NOT mean ===" ) +say( a.ok and " with 'proved none' and 'not a claim this call reaches any of them'", a.legend[ -400: ] ) +say( b.ok and " the same way", b.legend[ -400: ] ) +for sel, verb in [ ( "other", "callers" ), ( "go", "callees" ), ( "other", "impact" ) ]: + x = ask( CLEAN, verb, sel ) + say( x.ok and x.dc == [] and x.a( "declined_calls" ) is None and " row and no clause" % verb, x.why() ) + +# ── (G) the cap, on generated trees ──────────────────────────────────────────────────────────────────────────────── +print( "=== (G) capped AND counted ===" ) +G = os.path.join( TMP, "cap" ) +for d in ( "alpha", "beta", "callers", "big" ): + os.makedirs( os.path.join( G, d ), exist_ok = True ) +open( os.path.join( G, "alpha", "a.py" ), "w" ).write( "class A:\n def poll(self):\n return 1\n" ) +open( os.path.join( G, "beta", "b.py" ), "w" ).write( "class B:\n def poll(self):\n return 2\n" ) +for f in range( 3 ): + with open( os.path.join( G, "callers", "f%d.py" % f ), "w" ) as h: + for k in range( 12 ): + h.write( "def use_%d_%d(x):\n return x.poll()\n\n\n" % ( f, k ) ) +with open( os.path.join( G, "big", "big.py" ), "w" ) as h: + h.write( "def big(x):\n" + "".join( " x.poll()\n" for _ in range( 40 ) ) + " return x\n" ) +x = ask( G, "callers", "alpha/a.py:poll" ) +shown = int( x.a( "dc_shown" ) or 0 ) if x.ok else 0 +files = { d.get( "p", "" ).rsplit( ":", 1 )[ 0 ] for d in x.dc } +say( x.ok and x.a( "declined_calls" ) == "76" and 8 <= shown <= 24 and len( x.dc ) == shown and x.a( "dc_next" ), + "[P] g_callers_cut", "76 declined callers: dc_shown=N (8..24) rows printed, declined_calls=\"76\" the total, dc_next= present", x.why() ) +say( x.ok and files == { "callers/f0.py", "callers/f1.py", "callers/f2.py", "big/big.py" }, "[P] g_callers_files", + "the cut is file-first: all four calling files are named", "files=%s" % sorted( files ) ) +x = ask( G, "callees", "big" ) +shown = int( x.a( "dc_shown" ) or 0 ) if x.ok else 0 +ls = [ int( d.get( "l", "0" ) ) for d in x.dc ] +say( x.ok and x.a( "declined_calls" ) == "40" and 8 <= shown <= 24 and len( x.dc ) == shown and ls == sorted( ls ) and len( set( ls ) ) == len( ls ) + and x.a( "dc_next" ), "[P] g_callees_cut", "40 declined calls in one body: dc_shown rows in line order, dc_next= present", x.why() ) +x = ask( G, "callees", "use_0_0" ) +say( x.ok and len( x.dc ) == 1 and x.a( "dc_shown" ) is None and x.a( "dc_next" ) is None, "[P] g_uncut", + "under the cap: every row, no dc_shown= and no dc_next=", x.why() ) + +# ── (H) the other verbs ──────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (H) safe-delete, impact, for ===" ) +x = ask( GO, "safe-delete", "deb/deb.go:Package" ) +say( x.ok and x.a( "risk" ) == "possible-callers" and x.a( "callers" ) == "0", "[P] h_sd_risk", + "safe-delete of a definition only a declined call could mean: risk=\"possible-callers\", never none-found; callers=\"0\" unchanged", x.why() ) +say( x.ok and dc_sites( x, ( "in", "p" ) ) == [ ( "doPackage", "cmd/package.go:24", "2" ) ], "[P] h_sd_rows", + "safe-delete lists the declined possible caller as a row", x.why() ) +x = ask( PYF, "safe-delete", "beta/store.py:fetch" ) +say( x.ok and x.a( "risk" ) == "untested-radius" and x.a( "callers" ) == "1", "[C] h_sd_ctrl", + "a definition with a real caller keeps its risk= (untested-radius) and callers=\"1\"", x.why() ) +x = ask( GO, "impact", "deb/deb.go:Package" ) +say( x.ok and x.dc == [] and x.a( "reaches" ) == "0" and x.a( "declined_calls" ) == "1", "[C] h_impact", + "impact prints no row (declined_calls= stays its disclosure); reaches=\"0\" unchanged", x.why() ) +rc, out = run( GO, "--for=doPackage" ) +say( rc == 0 and " 0, "[C] h_for", "--for prints no row", "rc=%s" % rc ) + +# ── (K) the gates review's amendments (R1–R11): the wrong implementations that passed every arm above ──────────────── +print( "=== (K) R1 Ruby decline sites: new, and the two side cuts (gate=\"side\") ===" ) +RBS = os.path.join( FIX, "rbside" ) +x = ask( RBS, "callees", "build" ) +say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "new" and x.dc[ 0 ].get( "l" ) == "3" and x.dc[ 0 ].get( "cands" ) == "1" + and "gate" not in x.dc[ 0 ], "[P] k_rb_new", + "ruby --callees=build names the declined `new` at base.rb:3 as n=\"new\" (the CALLED name, never the candidate's initialize), cands=1, no gate=", x.why() ) +x = ask( RBS, "callers", "lib/child.rb:initialize" ) +say( x.ok and dc_sites( x, ( "in", "p" ) ) == [ ( "build", "lib/base.rb:3", "1" ) ], "[P] k_rb_new_callers", + "ruby --callers=lib/child.rb:initialize names the declined `new` in build at lib/base.rb:3", x.why() ) +for sel in ( "run", "make" ): + x = ask( RBS, "callees", sel ) + say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "helper" and x.dc[ 0 ].get( "l" ) == "7" and x.dc[ 0 ].get( "gate" ) == "side", + "[P] k_rb_side_%s" % sel, "ruby --callees=%s names helper@7 with gate=\"side\" (in reach, other side of self)" % sel, x.why() ) +for sel, only in ( ( "lib/widget.rb:helper", "run" ), ( "lib/gadget.rb:helper", "make" ) ): + x = ask( RBS, "callers", sel ) + say( x.ok and [ d.get( "in" ) for d in x.dc ] == [ only ], "[P] k_rb_w1_%s" % only, + "W1: --callers=%s names ONLY %s (candidate-list membership, never the called name)" % ( sel, only ), x.why() ) + +print( "=== (K) R3 W1 on the declinedlistcheck shapes: lists, never names ===" ) +ONE, WS = os.path.join( TMP, "one" ), os.path.join( TMP, "ws" ) +def cfn( path, name, body ): + os.makedirs( os.path.dirname( path ), exist_ok = True ) + open( path, "w" ).write( "int %s()\n{\n return %s;\n}\n" % ( name, body ) ) +def pyfn( path, name, body ): + os.makedirs( os.path.dirname( path ), exist_ok = True ) + open( path, "w" ).write( "def %s():\n return %s\n" % ( name, body ) ) +cfn( os.path.join( ONE, "a", "size.cpp" ), "size", "1" ); cfn( os.path.join( ONE, "b", "size.cpp" ), "size", "2" ) +pyfn( os.path.join( ONE, "c", "size.py" ), "size", "3" ); pyfn( os.path.join( ONE, "d", "size.py" ), "size", "4" ) +cfn( os.path.join( ONE, "e", "caller.cpp" ), "e_call", "size()" ); pyfn( os.path.join( ONE, "f", "caller.py" ), "f_call", "size()" ) +for r in ( "r1", "r2" ): + cfn( os.path.join( WS, r, "x.cpp" ), "foo", "1" ); cfn( os.path.join( WS, r, "y", "foo.cpp" ), "foo", "2" ) + cfn( os.path.join( WS, r, "z", "caller.cpp" ), "%s_call" % r, "foo()" ) +for sel, only in ( ( "a/size.cpp:size", "e_call" ), ( "c/size.py:size", "f_call" ) ): + x = ask( ONE, "callers", sel ) + say( x.ok and [ d.get( "in" ) for d in x.dc ] == [ only ] and x.a( "declined_calls" ) == "1", "[P] k_list_%s" % only, + "W1: --callers=%s names only %s (the language filter splits `size` into two lists)" % ( sel, only ), x.why() ) +rc, out = 124, "" +try: + p = subprocess.run( [ BIN, "r1", "r2", "--no-cache", "--callers=r1/x.cpp:foo" ], cwd = WS, capture_output = True, text = True, timeout = 240 ) + rc, out = p.returncode, p.stdout +except subprocess.TimeoutExpired: + pass +x = Answer( rc, out, "callers" ) +say( x.ok and [ d.get( "in" ) for d in x.dc ] == [ "r1_call" ], "[P] k_list_roots", + "W1: two roots: --callers=r1/x.cpp:foo names only r1's caller (the root filter splits `foo`)", x.why() ) + +print( "=== (K) R2 rows == declined_calls= on every uncut answer; TS: == declined_iface= too ===" ) +uncut = [ ( "go callees doPackage", ask( GO, "callees", "doPackage" ) ), ( "rb callers define_task", ask( RB, "callers", "lib/rake/task_manager.rb:define_task" ) ), + ( "py callers fetch", ask( PYF, "callers", "fetch" ) ), ( "py callees load", ask( PYF, "callees", "load" ) ), + ( "go safe-delete Package", ask( GO, "safe-delete", "deb/deb.go:Package" ) ), ( "rbside callees build", ask( RBS, "callees", "build" ) ), + ( "rbside callers widget helper", ask( RBS, "callers", "lib/widget.rb:helper" ) ), ( "bb callees param_get", ask( BB, "callees", "py/plain.py:param_get" ) ), + ( "sm callees stdAppend", ask( SM, "callees", "stdAppend" ) ), ( "ts callees dispatch", ask( TS, "callees", "dispatch" ) ), + ( "cpp callees drive", ask( CPP, "callees", "drive" ) ), ( "df callers java", ask( DF, "callers", "java/alpha/Alpha.java:jbody" ) ) ] +bad = [ "%s: rows=%d declined_calls=%s" % ( tag, len( x.dc ), x.a( "declined_calls" ) ) for tag, x in uncut + if not ( x.ok and x.a( "dc_shown" ) is None and len( x.dc ) > 0 and str( len( x.dc ) ) == x.a( "declined_calls" ) ) ] +say( not bad, "[P] k_rows_eq_declined", "on %d uncut answers the row count equals declined_calls= (every decline site records)" % len( uncut ), "; ".join( bad ) ) +for sel in ( "src/routers/trie/router.ts:match", "src/routers/router.ts:match" ): + x = ask( TS, "callers", sel ) + say( x.ok and len( x.dc ) == 1 and x.a( "declined_calls" ) == "1" and x.a( "declined_iface" ) == "1" and x.dc[ 0 ].get( "in" ) == "dispatch", + "[P] k_ts_iface_%s" % sel.split( "/" )[ -2 ], "ts --callers=%s: one (dispatch) == declined_calls=\"1\" == declined_iface=\"1\"" % sel, x.why() ) + +print( "=== (K) R4 attribute whitelist; R11 via=\"declined\" on every form ===" ) +CALLEE_KEYS, CALLER_KEYS = { "n", "l", "cands", "via", "gate", "of_def" }, { "in", "p", "cands", "via", "gate", "of_def" } +ce = [ x for tag, x in uncut if x.tag == "callees" ] + [ ask( G, "callees", "big" ) ] +cr = [ x for tag, x in uncut if x.tag != "callees" ] + [ ask( G, "callers", "alpha/a.py:poll" ) ] +say( all( x.ok and x.dc for x in ce ) and all( set( d ) <= CALLEE_KEYS and d.get( "via" ) == "declined" for x in ce for d in x.dc ), "[P] k_keys_callees", + "default callees keys stay within {n,l,cands,via,gate,of_def}, via=\"declined\" on each", str( [ d for x in ce for d in x.dc if set( d ) - CALLEE_KEYS ][ :3 ] ) ) +say( all( x.ok and x.dc for x in cr ) and all( set( d ) <= CALLER_KEYS and d.get( "via" ) == "declined" for x in cr for d in x.dc ), "[P] k_keys_callers", + "default callers and safe-delete keys stay within {in,p,cands,via,gate,of_def}, via=\"declined\" on each (Q2)", + str( [ d for x in cr for d in x.dc if set( d ) - CALLER_KEYS or d.get( "via" ) != "declined" ][ :3 ] ) ) +jbad = [] +for root, flag in ( ( GO, "--callees=doPackage" ), ( RB, "--callers=lib/rake/task_manager.rb:define_task" ) ): + rc, out = run( root, flag, "--json" ) + try: + j = json.loads( out ) + except ValueError: + j = {} + if rc != 0 or not j.get( "dc" ) or any( "cand" in d or "candidates" in d for d in j[ "dc" ] ): + jbad.append( "%s rc=%s dc=%s" % ( flag, rc, j.get( "dc" ) ) ) +say( not jbad, "[P] k_json_nocand", "--json default \"dc\" entries carry no \"cand\" (candidates only on legend=full)", "; ".join( jbad ) ) +mbad = [] +for tool, root, sym in ( ( "find_symbol", GO, "doPackage" ), ( "find_referencing_symbols", RB, "lib/rake/task_manager.rb:define_task" ) ): + m = mcp( tool, { "path": root, "symbol": sym } ) or {} + ds = m.get( "declined_sites", [] ) + if not ds or any( "candidates" in d or "cand" in d or d.get( "via" ) != "declined" for d in ds ): + mbad.append( "%s declined_sites=%s" % ( tool, ds ) ) +say( not mbad, "[P] k_mcp_nocand", "MCP default \"declined_sites\" entries carry via=\"declined\" and no \"candidates\"", "; ".join( mbad ) ) + +print( "=== (K) R5 the root keeps every base attribute (dc_shown/dc_next aside) ===" ) +BASE_ROOTS = [ + ( "a_go", GO, "callees", "doPackage", { "schema": "ripwire.callees/v1", "of": "doPackage", "defs": "1", "count": "2", "hop_tested": "0", "hop_untested": "2", + "declined_calls": "3", "graph_ambiguous": "0", "graph_unresolved": "0", "graph_unindexed": "1", "counts_floor": "1", "next": "--expand=doPackage" } ), + ( "a_rb", RB, "callers", "lib/rake/task_manager.rb:define_task", { "schema": "ripwire.callers/v1", "of": "lib/rake/task_manager.rb:define_task", "defs": "1", + "count": "2", "hop_tested": "0", "hop_untested": "2", "declined_calls": "6", "graph_ambiguous": "0", "graph_unresolved": "0", "counts_floor": "1", + "next": "--uses=define_task" } ), + ( "d_callers", PYF, "callers", "fetch", { "schema": "ripwire.callers/v1", "of": "fetch", "defs": "2", "count": "2", "hop_tested": "0", "hop_untested": "2", + "declined_calls": "4", "graph_ambiguous": "0", "graph_unresolved": "0", "counts_floor": "1", "next": "--uses=fetch" } ), + ( "g_callers", G, "callers", "alpha/a.py:poll", { "schema": "ripwire.callers/v1", "of": "alpha/a.py:poll", "defs": "1", "count": "0", "hop_tested": "0", + "hop_untested": "0", "declined_calls": "76", "graph_ambiguous": "0", "graph_unresolved": "0", "counts_floor": "1", "next": "--uses=poll" } ), +] +for tag, root, verb, sel, want in BASE_ROOTS: + x = ask( root, verb, sel ) + got = { k: v for k, v in ( x.root.attrib.items() if x.root is not None else [] ) if k not in ( "root", "dc_shown", "dc_next" ) } + say( x.ok and x.dc and got == want, "[P] k_root_%s" % tag, "%s --%s=%s: rows ride and every other root attribute is the base literal" % ( tag, verb, sel ), + "got %s" % { k: ( got.get( k ), want.get( k ) ) for k in set( got ) | set( want ) if got.get( k ) != want.get( k ) } ) + +print( "=== (K) R6 dc_next is a call that lists every site; R7 the cut is file-first in path order ===" ) +import shlex +G_CALLER_LINES = { "big/big.py:%d" % l for l in range( 2, 42 ) } | { "callers/f%d.py:%d" % ( f, 2 + 4 * k ) for f in range( 3 ) for k in range( 12 ) } +for verb, sel, want_lines in ( ( "callers", "alpha/a.py:poll", G_CALLER_LINES ), ( "callees", "big", { str( l ) for l in range( 2, 42 ) } ) ): + x = ask( G, verb, sel ) + nxt = x.a( "dc_next" ) or "" + rc, out = run( G, *shlex.split( nxt ) ) if nxt else ( 1, "" ) + if verb == "callers": + named = set( re.findall( r' p="([^"]+)"', out ) ) + else: + named = { m for m in re.findall( r']*\bl="(\d+)"', out ) } + say( x.ok and nxt and rc == 0 and want_lines <= named, "[P] k_dc_next_%s" % verb, + "the printed dc_next= (%s) runs (rc 0) and names all %d declined call lines" % ( nxt or "absent", len( want_lines ) ), + "rc=%s missing=%d" % ( rc, len( want_lines - named ) ) ) +x = ask( G, "callers", "alpha/a.py:poll" ) +first4 = [ d.get( "p", "" ).rsplit( ":", 1 )[ 0 ] for d in x.dc[ :4 ] ] +say( x.ok and first4 == [ "big/big.py", "callers/f0.py", "callers/f1.py", "callers/f2.py" ], "[P] k_cut_order", + "the first four callers rows of the cut are four distinct files in path order (each file's first site before any file's second)", "first4=%s" % first4 ) + +print( "=== (K) R8 safe-delete: possible-callers only where a declined call could mean THIS definition ===" ) +x = ask( GO, "safe-delete", "cmd/package.go:onlyResolved" ) +say( x.ok and x.a( "risk" ) == "none-found" and x.dc == [] and x.a( "declined_calls" ) is None, "[C] k_sd_ctrl", + "a definition no declined call could mean keeps risk=\"none-found\" on a tree that has declines", x.why() ) +x = ask( TS, "safe-delete", "src/routers/trie/router.ts:match" ) +say( x.ok and x.a( "risk" ) == "possible-callers" and [ d.get( "in" ) for d in x.dc ] == [ "dispatch" ], "[P] k_sd_ts", + "ts --safe-delete=src/routers/trie/router.ts:match reads risk=\"possible-callers\" with the dispatch row (base: none-found)", x.why() ) + +print( "=== (K) R9 + R10 legend: no \"no count or row\" where rides; the hedge in the review's words ===" ) +R10_EDGE = "NOT an edge and NOT a caller or callee claim" +R10_K1 = "(K may be 1)" +lbad = [] +for tag, x in uncut + [ ( "g callers", ask( G, "callers", "alpha/a.py:poll" ) ) ]: + if not x.ok or not x.dc: + lbad.append( "%s: no rows" % tag ) + elif "no count or row" in x.legend: + lbad.append( "%s: says 'no count or row'" % tag ) +say( not lbad, "[P] k_legend_drift", "no answer carrying rows also says 'no count or row' (compact)", "; ".join( lbad[ :4 ] ) ) +lbad = [] +for root, verb, sel in ( ( GO, "callees", "doPackage" ), ( RB, "callers", "lib/rake/task_manager.rb:define_task" ), ( GO, "safe-delete", "deb/deb.go:Package" ), + ( RBS, "callees", "run" ) ): + x = ask( root, verb, sel, "--legend=full" ) + if not x.ok or not x.dc: + lbad.append( "%s: no rows" % sel ) + elif "no count or row" in x.legend or R10_EDGE not in x.legend or R10_K1 not in x.legend or LEG_DONT not in x.legend or LEG_NOT not in x.legend: + lbad.append( "%s: legend lacks the R10 wording or keeps 'no count or row'" % sel ) + elif x.err: + lbad.append( "%s: %s" % ( sel, x.err ) ) +say( not lbad, "[P] k_legend_full", "--legend=full: the clause reads 'NOT an edge and NOT a caller or callee claim', '(K may be 1)', 'proved none', " + "'not a claim this call reaches any of them'; no 'no count or row'; well-formed XML", "; ".join( lbad ) ) +x = ask( RBS, "callees", "run", "--legend=full" ) +say( x.ok and x.dc and "gate=side" in x.legend, "[P] k_legend_side", "the gate=side reading rides where a gate=\"side\" row does", x.why() ) +x = ask( GO, "safe-delete", "deb/deb.go:Package", "--legend=full" ) +say( x.ok and "possible-callers:" in x.legend and "not a safety reading" in x.legend and "none-found beside declined_calls=" not in x.legend, + "[P] k_legend_sd", "safe-delete's risk sentence defines possible-callers and says it is not a safety reading", x.why() ) + +# ── (I) determinism ──────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (I) determinism ===" ) +rc1, o1 = run( GO, "--callees=doPackage", "--legend=full" ) +rc2, o2 = run( GO, "--callees=doPackage", "--legend=full" ) +say( rc1 == 0 and rc2 == 0 and o1 == o2 and len( o1 ) > 0, "[C] i_det", "two runs byte-identical" ) + +# ── (L) train 28: PR #380's inert quoted sites beside the records ───────────────────────────────────────────────── +# An Elixir call candidate inside inert quote AST is counted in declinedOut (the callees declined_calls=) but reaches no +# decline site, so it writes no record and has no row. Merged with this lane, declinedSitesMadeBy's postcondition +# (records == declined_calls=) aborted --callees on such a function (ENSURES, both branches: a tree with no record at all, +# and a tree whose records belong to other callers). Arms: an Elixir-only tree, then the same file beside a Python tree +# with tier-3 records; and the rows legend says the quoted sites have no row (only where both ride). +print( "=== (L) inert quoted sites (PR #380) carry no record and abort nothing ===" ) +EX = os.path.join( T, "elixirfix" ) +x = ask( EX, "callees", "quoted", "--legend=full" ) +say( x.ok and x.a( "declined_calls" ) == "1" and not x.dc, "[L] l_ex_only", + "elixirfix --callees=quoted: exit 0, declined_calls=\"1\" (the quoted nonexistent(x)), no row", x.why() ) +MIX = os.path.join( TMP, "mix" ) +import shutil +shutil.copytree( PYF, os.path.join( MIX, "py" ) ) +os.makedirs( os.path.join( MIX, "ex" ) ); shutil.copy( os.path.join( EX, "run.exs" ), os.path.join( MIX, "ex", "run.exs" ) ) +x = ask( MIX, "callees", "quoted" ) +say( x.ok and x.a( "declined_calls" ) == "1" and not x.dc, "[L] l_mixed_inert", + "beside Python records: --callees=quoted exit 0, declined_calls=\"1\", no row", x.why() ) +# a selector whose definitions made BOTH: a Python `quoted` with a declined store.fetch (rows) beside the Elixir quoted/0 +with open( os.path.join( MIX, "py", "caller", "q.py" ), "w", encoding = "utf-8" ) as f: + f.write( "def quoted(store, key):\n return store.fetch(key)\n" ) +rc, out = run( MIX, "--callees=quoted", "--legend=full" ) +y = Answer( rc, out, "callees" ) +say( y.ok and len( y.dc ) >= 1 and int( y.a( "declined_calls" ) or 0 ) == len( y.dc ) + 1 + and "No row names those quoted sites" in out and "inert quote AST" in out, "[L] l_rows_legend", + "--callees=quoted over a Python and an Elixir definition: the Python rows, declined_calls= one more (the inert site), the inert clause and its no-row sentence", y.why() ) +rc, out = run( PYF, "--callees=caller/use.py:load_twice", "--legend=full" ) +say( rc == 0 and "No row names those quoted sites" not in out and "inert quote AST" not in out, "[L] l_rows_legend_neg", + "the same answer on the Python tree alone carries neither sentence (present-only)" ) + +# (L, CR pre-pass) the inert sentences ride only an answer whose OWN definitions made an inert quoted site: on the same mixed +# tree a Python selector whose every declined call has its row carries neither (it was charged graph-wide before); +# and the MCP map (analyze) defines the inert sites its declined= counts, as the CLI map does. +rc, out = run( MIX, "--callees=load", "--legend=full" ) +y = Answer( rc, out, "callees" ) +say( y.ok and len( y.dc ) == int( y.a( "declined_calls" ) or -1 ) and "No row names those quoted sites" not in out and "inert quote AST" not in out, + "[L] l_rows_no_inert", "mixed tree, --callees=load: every declined call has its row, so neither inert sentence rides", y.why() ) +req = json.dumps( { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} } ) + "\n" + json.dumps( + { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "analyze", "arguments": { "path": EX, "legend": "full" } } } ) + "\n" +an = "" +try: + p = subprocess.run( [ BIN, "--mcp" ], input = req, capture_output = True, text = True, timeout = 240 ) + for line in p.stdout.splitlines(): + try: r = json.loads( line ) + except ValueError: continue + if r.get( "id" ) == 2 and "result" in r: an = r[ "result" ][ "content" ][ 0 ][ "text" ] +except ( subprocess.TimeoutExpired, KeyError, IndexError, TypeError ): an = "" +say( "declined=" in an and "inert-quote-AST" in an, "[L] l_mcp_map", "MCP analyze on elixirfix: declined= and the inert map clause, as on the CLI", an[ :160 ] ) + +# ── (M) train 28: the review's N1 (honesty-adjacent) and N3 ────────────────────────────────────────────────────────── +# N1: both declined_calls= clauses said "two or more same-language definitions, none in the caller's file or directory" +# of every counted call, false beside a Ruby side-cut row whose ONE candidate is in the caller's own file (rbside). The +# clause now scopes that to tier 3 and says a name gate or Ruby's side/new rule may decline one. N3: the MCP declined_note +# describes the "declined_sites" entries by their own keys, not the XML attributes. +print( "=== (M) the declined_calls= clauses scope tier 3; the MCP note speaks its own keys ===" ) +UNSCOPED = "none in the caller's file or directory, and no qualifier, receiver type or include chose one, so no edge" +SCOPED = "chose one (tier 3); a name gate or Ruby's side/new rule may decline one" +x = ask( RBS, "callees", "run", "--legend=full" ) +say( x.ok and any( d.get( "gate" ) == "side" and d.get( "cands" ) == "1" for d in x.dc ) and SCOPED in x.text and UNSCOPED not in x.text, + "[M] m_rows_clause", "rbside --callees=run beside a gate=\"side\" cands=\"1\" row: the rows clause scopes tier 3", x.why() ) +x = ask( RBS, "impact", "lib/widget.rb:helper", "--legend=full" ) +say( x.ok and ( "declined_calls=" not in x.text or ( SCOPED in x.text and UNSCOPED not in x.text ) ), "[M] m_base_clause", + "the row-less clause (impact) is scoped the same way where it rides", x.why() ) +m = mcp( "find_symbol", { "path": GO, "symbol": "doPackage" } ) or {} +note = str( m.get( "declined_note", "" ) ) +say( bool( m.get( "declined_sites" ) ) and note.startswith( "declined_sites:" ) and "{name, line" in note and " attributes", note[ :160 ] ) +m = mcp( "find_symbol", { "path": PYF, "symbol": "load" } ) or {} +note = str( m.get( "declined_note", "" ) ) +say( any( d.get( "of_def" ) for d in m.get( "declined_sites", [] ) ) and "of_def [K]" in note and " (rc 0, no abort)" % ( caller, line ), + x.why() ) +x = ask( RBSUB, "callers", "[]" ) +say( x.ok and sorted( ( d.get( "in" ), d.get( "p" ) ) for d in x.dc ) == [ ( "go", "k.rb:17" ), ( "run", "k.rb:6" ) ], "[P] n_sub_callers", + "ruby --callers=[]: both declined subscripts are named at their own lines", x.why() ) + +# ── (J) the predicates can fail ──────────────────────────────────────────────────────────────────────────────────── +print( "=== (J) the predicates reject what they must ===" ) +fake = Answer( 0, '', "callers" ) +say( fake.ok and not all( set( d.get( "of_def", "" ).split() ) <= { "1~d", "2~d" } for d in fake.dc ), "[C] j_bare_k", + "a callers-form (a bare K) is caught" ) +fake = Answer( 0, '', "callees" ) +say( fake.ok and ( len( fake.rows ) != int( fake.a( "count" ) ) or any( r.get( "via" ) == "declined" for r in fake.rows ) ), "[C] j_in_count", + "a declined site folded into the rows / count= is caught" ) +fake = Answer( 0, '', "callees" ) +say( not fake.ok, "[C] j_malformed", "a malformed answer is not read as an empty one" ) + +print( "declinedsitecheck: %d PASS / %d FAIL" % ( npass, nfail ) ) +sys.exit( 1 if nfail else 0 ) +PY +if [ "$fail" = 0 ]; then echo "declinedsitecheck: PASS"; else echo "declinedsitecheck: FAIL"; exit 1; fi diff --git a/test/declinedsitefix/cpp/alpha/engine.h b/test/declinedsitefix/cpp/alpha/engine.h new file mode 100644 index 000000000..aabb6bccb --- /dev/null +++ b/test/declinedsitefix/cpp/alpha/engine.h @@ -0,0 +1,4 @@ +struct AlphaEngine +{ + int run( int n ) { return n + 1; } +}; diff --git a/test/declinedsitefix/cpp/beta/engine.h b/test/declinedsitefix/cpp/beta/engine.h new file mode 100644 index 000000000..1a73a030b --- /dev/null +++ b/test/declinedsitefix/cpp/beta/engine.h @@ -0,0 +1,4 @@ +struct BetaEngine +{ + int run( int n ) { return n - 1; } +}; diff --git a/test/declinedsitefix/cpp/caller/drive.cpp b/test/declinedsitefix/cpp/caller/drive.cpp new file mode 100644 index 000000000..3b1448ce4 --- /dev/null +++ b/test/declinedsitefix/cpp/caller/drive.cpp @@ -0,0 +1,10 @@ +static int twice( int n ) +{ + return n * 2; +} + +template +int drive( Engine& engine, int n ) +{ + return engine.run( twice( n ) ); +} diff --git a/test/declinedsitefix/go/cmd/package.go b/test/declinedsitefix/go/cmd/package.go new file mode 100644 index 000000000..6da148932 --- /dev/null +++ b/test/declinedsitefix/go/cmd/package.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "os" + + "example.com/dsf/pkgr" +) + +func doPackage(target string, intoDir bool) error { + info := pkgr.WithDefaults(&pkgr.Info{}) + pkg, err := pkgr.Get("deb") + if err != nil { + return err + } + if target == "" { + target = pkg.ConventionalFileName(info) + } else if intoDir { + target = target + "/" + pkg.ConventionalFileName(info) + } + f, err := os.Create(target) + if err != nil { + return err + } + return pkg.Package(info, f) +} + +func onlyResolved() *pkgr.Info { + return pkgr.WithDefaults(&pkgr.Info{}) +} diff --git a/test/declinedsitefix/go/deb/deb.go b/test/declinedsitefix/go/deb/deb.go new file mode 100644 index 000000000..7b80d5650 --- /dev/null +++ b/test/declinedsitefix/go/deb/deb.go @@ -0,0 +1,17 @@ +package deb + +import ( + "io" + + "example.com/dsf/pkgr" +) + +type Deb struct{} + +func (Deb) ConventionalFileName(info *pkgr.Info) string { + return info.Target + ".deb" +} + +func (Deb) Package(info *pkgr.Info, w io.Writer) error { + return nil +} diff --git a/test/declinedsitefix/go/go.mod b/test/declinedsitefix/go/go.mod new file mode 100644 index 000000000..95c29d763 --- /dev/null +++ b/test/declinedsitefix/go/go.mod @@ -0,0 +1,3 @@ +module example.com/dsf + +go 1.21 diff --git a/test/declinedsitefix/go/pkgr/pkgr.go b/test/declinedsitefix/go/pkgr/pkgr.go new file mode 100644 index 000000000..5d11d4209 --- /dev/null +++ b/test/declinedsitefix/go/pkgr/pkgr.go @@ -0,0 +1,26 @@ +package pkgr + +import "io" + +// Info is what every packager reads. +type Info struct { + Target string +} + +// Packager is implemented once per package format. +type Packager interface { + ConventionalFileName(info *Info) string + Package(info *Info, w io.Writer) error +} + +var registry = map[string]Packager{} + +// Get looks a packager up by format name. +func Get(format string) (Packager, error) { + return registry[format], nil +} + +// WithDefaults fills the defaults in. +func WithDefaults(info *Info) *Info { + return info +} diff --git a/test/declinedsitefix/go/rpm/rpm.go b/test/declinedsitefix/go/rpm/rpm.go new file mode 100644 index 000000000..832f16dd1 --- /dev/null +++ b/test/declinedsitefix/go/rpm/rpm.go @@ -0,0 +1,17 @@ +package rpm + +import ( + "io" + + "example.com/dsf/pkgr" +) + +type RPM struct{} + +func (RPM) ConventionalFileName(info *pkgr.Info) string { + return info.Target + ".rpm" +} + +func (RPM) Package(info *pkgr.Info, w io.Writer) error { + return nil +} diff --git a/test/declinedsitefix/py/alpha/store.py b/test/declinedsitefix/py/alpha/store.py new file mode 100644 index 000000000..3473b6c37 --- /dev/null +++ b/test/declinedsitefix/py/alpha/store.py @@ -0,0 +1,6 @@ +class AlphaStore: + def fetch(self, key): + return key + + def fetch_all(self, keys, peer): + return [peer.fetch(k) for k in keys] diff --git a/test/declinedsitefix/py/beta/store.py b/test/declinedsitefix/py/beta/store.py new file mode 100644 index 000000000..377ae63ed --- /dev/null +++ b/test/declinedsitefix/py/beta/store.py @@ -0,0 +1,6 @@ +class BetaStore: + def fetch(self, key): + return None + + def fetch_all(self, keys, peer): + return {k: peer.fetch(k) for k in keys} diff --git a/test/declinedsitefix/py/caller/use.py b/test/declinedsitefix/py/caller/use.py new file mode 100644 index 000000000..e4db33ea1 --- /dev/null +++ b/test/declinedsitefix/py/caller/use.py @@ -0,0 +1,13 @@ +def normalise(key): + return key.strip() + + +def load(store, key): + k = normalise(key) + return store.fetch(k) + + +def load_twice(store, key): + first = store.fetch(key) + second = store.fetch(normalise(key)) + return first, second diff --git a/test/declinedsitefix/py/other/loader.py b/test/declinedsitefix/py/other/loader.py new file mode 100644 index 000000000..05dc1948e --- /dev/null +++ b/test/declinedsitefix/py/other/loader.py @@ -0,0 +1,2 @@ +def load(store, key): + return store.fetch(key) diff --git a/test/declinedsitefix/rb/lib/rake/application.rb b/test/declinedsitefix/rb/lib/rake/application.rb new file mode 100644 index 000000000..798a467b0 --- /dev/null +++ b/test/declinedsitefix/rb/lib/rake/application.rb @@ -0,0 +1,9 @@ +module Rake + class Application + include TaskManager + end + + def self.application + @application ||= Application.new + end +end diff --git a/test/declinedsitefix/rb/lib/rake/task.rb b/test/declinedsitefix/rb/lib/rake/task.rb new file mode 100644 index 000000000..141c9535a --- /dev/null +++ b/test/declinedsitefix/rb/lib/rake/task.rb @@ -0,0 +1,16 @@ +module Rake + class Task + def initialize(name) + @name = name + end + + class << self + def define_task(*args, &block) + Rake.application.define_task(self, *args, &block) + end + end + end + + class FileTask < Task + end +end diff --git a/test/declinedsitefix/rb/lib/rake/task_manager.rb b/test/declinedsitefix/rb/lib/rake/task_manager.rb new file mode 100644 index 000000000..f83d0d0d3 --- /dev/null +++ b/test/declinedsitefix/rb/lib/rake/task_manager.rb @@ -0,0 +1,11 @@ +module Rake + module TaskManager + def define_task(task_class, *args, &block) + task_class.new(args.first) + end + + def synthesize_file_task(task_name) + define_task(Rake::FileTask, task_name) + end + end +end diff --git a/test/declinedsitefix/rb/test/test_application.rb b/test/declinedsitefix/rb/test/test_application.rb new file mode 100644 index 000000000..d7844b854 --- /dev/null +++ b/test/declinedsitefix/rb/test/test_application.rb @@ -0,0 +1,11 @@ +require "minitest/autorun" + +class TestApplication < Minitest::Test + def test_load_rakefile + @app.define_task(Rake::Task, :default) + end + + def test_top_level + @app.define_task(Rake::FileTask, "a.o") + end +end diff --git a/test/declinedsitefix/rb/test/test_name_space.rb b/test/declinedsitefix/rb/test/test_name_space.rb new file mode 100644 index 000000000..90138500b --- /dev/null +++ b/test/declinedsitefix/rb/test/test_name_space.rb @@ -0,0 +1,8 @@ +require "minitest/autorun" + +class TestNameSpace < Minitest::Test + def test_namespace_lookup + mgr = fresh_manager + mgr.define_task(Rake::Task, "n:t") + end +end diff --git a/test/declinedsitefix/rb/test/test_task_manager.rb b/test/declinedsitefix/rb/test/test_task_manager.rb new file mode 100644 index 000000000..e3127df17 --- /dev/null +++ b/test/declinedsitefix/rb/test/test_task_manager.rb @@ -0,0 +1,16 @@ +require "minitest/autorun" + +class TestTaskManager < Minitest::Test + def setup + @tm = make_manager + end + + def test_create_task_manager + @tm.define_task(Rake::Task, :t) + end + + def test_define_two + @tm.define_task(Rake::Task, :a) + @tm.define_task(Rake::Task, :b) + end +end diff --git a/test/declinedsitefix/rbside/lib/base.rb b/test/declinedsitefix/rbside/lib/base.rb new file mode 100644 index 000000000..8aea9d10a --- /dev/null +++ b/test/declinedsitefix/rbside/lib/base.rb @@ -0,0 +1,5 @@ +class Base + def self.build + new + end +end diff --git a/test/declinedsitefix/rbside/lib/child.rb b/test/declinedsitefix/rbside/lib/child.rb new file mode 100644 index 000000000..e42e7788f --- /dev/null +++ b/test/declinedsitefix/rbside/lib/child.rb @@ -0,0 +1,5 @@ +class Child < Base + def initialize + @x = 1 + end +end diff --git a/test/declinedsitefix/rbside/lib/gadget.rb b/test/declinedsitefix/rbside/lib/gadget.rb new file mode 100644 index 000000000..8ac6c528c --- /dev/null +++ b/test/declinedsitefix/rbside/lib/gadget.rb @@ -0,0 +1,9 @@ +class Gadget + def helper + 2 + end + + def self.make + helper + end +end diff --git a/test/declinedsitefix/rbside/lib/widget.rb b/test/declinedsitefix/rbside/lib/widget.rb new file mode 100644 index 000000000..e3fa9a534 --- /dev/null +++ b/test/declinedsitefix/rbside/lib/widget.rb @@ -0,0 +1,9 @@ +class Widget + def self.helper + 1 + end + + def run + helper + end +end diff --git a/test/declinedsitefix/rs/Cargo.toml b/test/declinedsitefix/rs/Cargo.toml new file mode 100644 index 000000000..6d7de0d00 --- /dev/null +++ b/test/declinedsitefix/rs/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "dsf" +version = "0.1.0" +edition = "2021" diff --git a/test/declinedsitefix/rs/src/builtin/index/mod.rs b/test/declinedsitefix/rs/src/builtin/index/mod.rs new file mode 100644 index 000000000..bdae3c7b9 --- /dev/null +++ b/test/declinedsitefix/rs/src/builtin/index/mod.rs @@ -0,0 +1,13 @@ +use crate::preprocess::{Book, Preprocessor, PreprocessorContext}; + +pub struct IndexPreprocessor; + +impl Preprocessor for IndexPreprocessor { + fn name(&self) -> &str { + "index" + } + + fn run(&self, _ctx: &PreprocessorContext, book: Book) -> Book { + book + } +} diff --git a/test/declinedsitefix/rs/src/builtin/links/mod.rs b/test/declinedsitefix/rs/src/builtin/links/mod.rs new file mode 100644 index 000000000..44278e061 --- /dev/null +++ b/test/declinedsitefix/rs/src/builtin/links/mod.rs @@ -0,0 +1,13 @@ +use crate::preprocess::{Book, Preprocessor, PreprocessorContext}; + +pub struct LinkPreprocessor; + +impl Preprocessor for LinkPreprocessor { + fn name(&self) -> &str { + "links" + } + + fn run(&self, _ctx: &PreprocessorContext, book: Book) -> Book { + book + } +} diff --git a/test/declinedsitefix/rs/src/builtin/mod.rs b/test/declinedsitefix/rs/src/builtin/mod.rs new file mode 100644 index 000000000..16595b463 --- /dev/null +++ b/test/declinedsitefix/rs/src/builtin/mod.rs @@ -0,0 +1,2 @@ +pub mod index; +pub mod links; diff --git a/test/declinedsitefix/rs/src/driver/mod.rs b/test/declinedsitefix/rs/src/driver/mod.rs new file mode 100644 index 000000000..2cd320991 --- /dev/null +++ b/test/declinedsitefix/rs/src/driver/mod.rs @@ -0,0 +1,23 @@ +use crate::preprocess::{Book, Preprocessor, PreprocessorContext}; + +pub struct MDBook { + pub root: String, + pub preprocessors: Vec>, +} + +fn preprocessor_should_run(root: &str) -> bool { + !root.is_empty() +} + +impl MDBook { + pub fn preprocess_book(&self, book: Book) -> Book { + let ctx = PreprocessorContext::new(self.root.clone()); + let mut out = book; + for preprocessor in self.preprocessors.iter() { + if preprocessor_should_run(&self.root) { + out = preprocessor.run(&ctx, out); + } + } + out + } +} diff --git a/test/declinedsitefix/rs/src/lib.rs b/test/declinedsitefix/rs/src/lib.rs new file mode 100644 index 000000000..55cd32324 --- /dev/null +++ b/test/declinedsitefix/rs/src/lib.rs @@ -0,0 +1,3 @@ +pub mod builtin; +pub mod driver; +pub mod preprocess; diff --git a/test/declinedsitefix/rs/src/preprocess/mod.rs b/test/declinedsitefix/rs/src/preprocess/mod.rs new file mode 100644 index 000000000..0d0f0e1b4 --- /dev/null +++ b/test/declinedsitefix/rs/src/preprocess/mod.rs @@ -0,0 +1,18 @@ +pub struct Book { + pub chapters: Vec, +} + +pub struct PreprocessorContext { + pub root: String, +} + +impl PreprocessorContext { + pub fn new(root: String) -> PreprocessorContext { + PreprocessorContext { root } + } +} + +pub trait Preprocessor { + fn name(&self) -> &str; + fn run(&self, ctx: &PreprocessorContext, book: Book) -> Book; +} diff --git a/test/declinedsitefix/ts/src/app/dispatch.ts b/test/declinedsitefix/ts/src/app/dispatch.ts new file mode 100644 index 000000000..3a5e86908 --- /dev/null +++ b/test/declinedsitefix/ts/src/app/dispatch.ts @@ -0,0 +1,9 @@ +import { Router } from "../routers/router"; + +function clean(path: string): string { + return path.trim(); +} + +export function dispatch(router: Router, method: string, path: string): string[] { + return router.match(method, clean(path)); +} diff --git a/test/declinedsitefix/ts/src/routers/regexp/router.ts b/test/declinedsitefix/ts/src/routers/regexp/router.ts new file mode 100644 index 000000000..76d087519 --- /dev/null +++ b/test/declinedsitefix/ts/src/routers/regexp/router.ts @@ -0,0 +1,7 @@ +import { Router } from "../router"; + +export class RegExpRouter implements Router { + match(method: string, path: string): string[] { + return [path, method]; + } +} diff --git a/test/declinedsitefix/ts/src/routers/router.ts b/test/declinedsitefix/ts/src/routers/router.ts new file mode 100644 index 000000000..00d13f3dc --- /dev/null +++ b/test/declinedsitefix/ts/src/routers/router.ts @@ -0,0 +1,3 @@ +export interface Router { + match(method: string, path: string): string[]; +} diff --git a/test/declinedsitefix/ts/src/routers/trie/router.ts b/test/declinedsitefix/ts/src/routers/trie/router.ts new file mode 100644 index 000000000..a622eb109 --- /dev/null +++ b/test/declinedsitefix/ts/src/routers/trie/router.ts @@ -0,0 +1,7 @@ +import { Router } from "../router"; + +export class TrieRouter implements Router { + match(method: string, path: string): string[] { + return [method, path]; + } +} diff --git a/test/deeptailcheck.sh b/test/deeptailcheck.sh index 686681ce2..a7751afbd 100755 --- a/test/deeptailcheck.sh +++ b/test/deeptailcheck.sh @@ -13,6 +13,12 @@ # best-symbol projection of the same ranking, disclosed as total=/shown=/capped= and labelled # weaker-than-head evidence in the legend. # +# FOLD C (forbudget-soft round C, owner decision 2026-10-14; rule 4): the now lists the RANK rows above (paths only, +# ``, the files outside the shown head, files of trimmed rows first) and THEN page 0 of `--for=TASK --limit=40` +# minus the files already listed, each with sym= (``). total= counts every positive-score file (the page's +# total=), so the old "total = files outside the head" denominator is a by-design break: the arms below read the rank rows +# (no sym=) where they used to read the whole tail, and pin the page rows against the page itself. +# # RED-FIRST: every d1/d2 assertion below FAILS against the prebuilt d8e257d baseline (which emits neither # surface); each fuses presence with content bytes so a bare-exit arm cannot pass green-while-inert. # @@ -52,24 +58,53 @@ Q="widget frobnicate helper" XML="$( "$BIN" "$CORPUS" --no-cache --for="$Q" --pack-top-n=4 --legend=full 2>/dev/null )" printf '%s' "$XML" > "$TMP/for.xml" -# ── 1) d2: the file-grain tail — present, complete, disjoint from the head ───────────────────────────── +# ── 1) d2: the file-grain tail — present, complete; its rank rows disjoint from the head ─────────────── +# a python reader of the tail: "rank" rows (no sym=) and "page" rows (p, sym), in order +cat > "$TMP/tail.py" <<'PY' +import re, sys, html +s = open( sys.argv[ 1 ] ).read() +m = re.search( r"]*>(.*?)", s, re.S ) +rows = re.findall( r'', m.group( 1 ) if m else "" ) +mode = sys.argv[ 2 ] +for p, has, sym in rows: + if mode == "rank" and not has: print( html.unescape( p ) ) + if mode == "page" and has: print( html.unescape( p ) + "\t" + html.unescape( sym ) ) + if mode == "all": print( html.unescape( p ) + "\t" + html.unescape( sym ) ) +PY +# the page rows a tail must carry: page 0 of the file page, minus the rank rows, in page order +pagerows(){ "$BIN" "$CORPUS" --no-cache --for="$Q" --limit=40 2>/dev/null | python3 -c ' +import re, sys, html +rank = set( open( sys.argv[ 1 ] ).read().split( "\n" ) ) +for p, sym in re.findall( r"]*? sym=\"([^\"]*)\"", sys.stdin.read() ): + p = html.unescape( p ) + if p not in rank: print( p + "\t" + html.unescape( sym ) ) +' "$1"; } TAILTAG="$( printf '%s' "$XML" | grep -o ']*>' | head -1 )" case "$TAILTAG" in - *'total="11"'*'shown="11"'*'capped="0"'*) ok "tail element carries the exact split: $TAILTAG" ;; + *'total="12"'*'shown="12"'*'capped="0"'*) ok "tail element: every positive-score file (12), all shown: $TAILTAG" ;; '') no "no element in the --for bundle (d2 missing)" ;; - *) no "tail element has wrong counts: $TAILTAG (expected total=11 shown=11 capped=0)" ;; + *) no "tail element has wrong counts: $TAILTAG (expected total=12 shown=12 capped=0)" ;; esac -TROWS="$( printf '%s' "$XML" | grep -o '' | wc -l | tr -d ' ' )" -if [ "$TROWS" = "11" ]; then ok "tail serves 11 rows, no wrapper) -DUP="$( printf '%s' "$XML" | python3 -c ' +python3 "$TMP/tail.py" "$TMP/for.xml" rank > "$TMP/rank1.txt" +TROWS="$( grep -c . "$TMP/rank1.txt" )" +if [ "$TROWS" = "11" ]; then ok "tail serves 11 rank rows ( without sym=): the files outside the one-file head"; else no "tail rank rows: $TROWS (expected 11)"; fi +# disjoint: no rank row may also be a head row's p= (P7: the head is flat rows, no wrapper) +DUP="$( python3 - "$TMP/for.xml" "$TMP/rank1.txt" <<'PY' import re, sys -s = sys.stdin.read() -head = set( re.findall( r"]*?\bp=\"([^\"]*)\"", s ) ) -tails = re.findall( r"", s ) -print( sum( 1 for t in tails if t in head ) ) -' )" -if [ "$DUP" = "0" ]; then ok "tail files are disjoint from the head's files"; else no "$DUP tail file(s) duplicate a head file"; fi +s = open( sys.argv[ 1 ] ).read() +head = set( re.findall( r"]*?\bp=\"([^\"]*)\"", s ) ) +print( sum( 1 for t in open( sys.argv[ 2 ] ).read().split() if t in head ) ) +PY +)" +if [ "$DUP" = "0" ]; then ok "tail rank rows are disjoint from the head's files"; else no "$DUP tail rank row(s) duplicate a head file"; fi +# FOLD C: the page rows ARE the file page's first 40 rows not listed above, in page order, sym= included +python3 "$TMP/tail.py" "$TMP/for.xml" page > "$TMP/page1.txt" +pagerows "$TMP/rank1.txt" > "$TMP/page1_want.txt" +if [ -s "$TMP/page1_want.txt" ] && cmp -s "$TMP/page1.txt" "$TMP/page1_want.txt"; then + ok "FOLD C: the tail's page rows are the --limit=40 page's rows not listed above ($( grep -c . "$TMP/page1.txt" ), sym= equal)" +else + no "FOLD C: tail page rows differ from the file page's ($( grep -c . "$TMP/page1.txt" ) vs $( grep -c . "$TMP/page1_want.txt" ))" +fi # ── 2) d1: r= on every ranked row, exactly the ranks 1..K ────────────────────────────────────────────── RSEQ="$( printf '%s' "$XML" | grep -o '", xml ) +import html +xrows = [ ( html.unescape( p ), html.unescape( y ) ) for p, _, y in re.findall( r"", xml ) ] +xrows = [ ( p, y ) for p, _h, y in [ ( r[0], None, r[1] ) for r in xrows ] ] checks = [ bool( rs ) and rs == list( range( 1, len( rs ) + 1 ) ), - tail is not None and tail.get( "total" ) == 4 and tail.get( "shown" ) == 4, - tail is not None and tail.get( "files" ) == xtails, + tail is not None and tail.get( "total" ) == 12 and tail.get( "shown" ) == 12 + and [ f for f, y in zip( tail.get( "files", [] ), tail.get( "sym", [] ) ) if not y ] == [ "w09.py", "w10.py", "w11.py", "w12.py" ], + tail is not None and list( zip( tail.get( "files", [] ), tail.get( "sym", [] ) ) ) == xrows, ] print( " ".join( "Y" if c else "N" for c in checks ) ) EOF )" case "$JV" in - "Y Y Y") ok "JSON: r keys are contiguous 1..K; tail total/shown=4/4; files match the XML tail exactly" ;; + "Y Y Y") ok "JSON: r keys are contiguous 1..K; tail total/shown=12/12, rank rows w09..w12; files+sym match the XML tail exactly" ;; *) no "JSON dialect wrong (r-seq / tail-counts / xml-parity = $JV)" ;; esac @@ -121,28 +159,33 @@ if [ "$PTJ" = "Y" ]; then ok "pack-task JSON ranking rows carry r"; else no "pac TB="$( "$BIN" "$CORPUS" --no-cache --for="$Q" --pack-top-n=4 --token-budget=220 2>/dev/null )" TBTAG="$( printf '%s' "$TB" | grep -o ']*>' | head -1 )" case "$TBTAG" in - *'total="11"'*) ok "tight --token-budget keeps the tail disclosure: $TBTAG" ;; + *'total="12"'*) ok "tight --token-budget keeps the tail disclosure: $TBTAG" ;; '') no "tight --token-budget dropped the element entirely (disclosure lost)" ;; *) no "tight-budget tail lost its denominator: $TBTAG" ;; esac TBSHOWN="$( printf '%s' "$TBTAG" | grep -o 'shown="[0-9]*"' | grep -o '[0-9]*' )" TBCAP="$( printf '%s' "$TBTAG" | grep -o 'capped="[0-9]"' | grep -o '[0-9]' )" -if [ -n "$TBSHOWN" ] && [ "$TBSHOWN" -lt 11 ] && [ "$TBCAP" = "1" ]; then - ok "tight budget trimmed tail rows (shown=$TBSHOWN) and disclosed capped=1" +if [ -n "$TBSHOWN" ] && [ "$TBSHOWN" -lt 12 ] && [ "$TBCAP" = "1" ] && printf '%s' "$TBTAG" | grep -q ' next="'; then + ok "tight budget trimmed tail rows (shown=$TBSHOWN), disclosed capped=1 and named next=" else no "tight budget did not trim/disclose honestly (shown=$TBSHOWN capped=$TBCAP)" fi if printf '%s' "$TB" | xmllint --noout - 2>/dev/null; then ok "tight-budget bundle stays well-formed"; else no "tight-budget bundle fails xmllint"; fi -# ── 7) zero-tail honesty: a head that covers every file still emits the element (0 = none remain) ────── +# ── 7) zero-tail honesty: a head that covers every file leaves NO rank row (0 = none remain); the page rows still +# list every positive-score file (FOLD C), so total= = the 3 files and nothing is cut ───────────────────────── MINI="$TMP/mini" mkdir -p "$MINI" for i in 1 2 3; do printf 'def widget_frobnicate_%s():\n """widget frobnicate helper %s"""\n return 1\n' "$i" "$i" > "$MINI/m$i.py" done -Z="$( "$BIN" "$MINI" --no-cache --for="$Q" 2>/dev/null | grep -o ']*>' | head -1 )" +"$BIN" "$MINI" --no-cache --for="$Q" 2>/dev/null > "$TMP/mini.xml" +Z="$( grep -o ']*>' "$TMP/mini.xml" | head -1 )" +ZR="$( python3 "$TMP/tail.py" "$TMP/mini.xml" rank | grep -c . )" case "$Z" in - *'total="0"'*'shown="0"'*'capped="0"'*) ok "all-files-in-head bundle still emits (none remain, said out loud)" ;; + *'total="3"'*'shown="3"'*'capped="0"'*) + if [ "$ZR" = "0" ]; then ok "all-files-in-head bundle: 0 rank rows (none remain), the 3 files as page rows, nothing cut ($Z)" + else no "all-files-in-head bundle lists $ZR rank row(s) — a head file served as 'outside the head'"; fi ;; *) no "zero-tail bundle wrong/missing: '$Z'" ;; esac @@ -159,11 +202,17 @@ T9_TAG="$( printf '%s' "$T9" | grep -o ']*>' | head -1 )" T9_TOTAL="$( printf '%s' "$T9_TAG" | grep -oE 'total="[0-9]+"' | grep -oE '[0-9]+' )" case "$T9_SIGS" in *'capped="1"'*) ok "(9) fixture guard: the 900-token budget trims sigs rows ($T9_SIGS; $T9_NSHOWN file(s) shown)" ;; *) no "(9) fixture guard: expected the ladder to trim at --token-budget=900, got '$T9_SIGS'" ;; esac -[ "$T9_TOTAL" = "$(( 12 - T9_NSHOWN ))" ] \ - && ok "(9a) tail total=$T9_TOTAL = 12 files minus the $T9_NSHOWN shown (trimmed surface rows are IN the tail)" \ - || no "(9a) tail total=$T9_TOTAL but $T9_NSHOWN file(s) are shown — $(( 12 - T9_NSHOWN - ${T9_TOTAL:-0} )) ranked file(s) served nowhere ($T9_TAG)" +printf '%s' "$T9" > "$TMP/t9.xml" +T9_RANK="$( python3 "$TMP/tail.py" "$TMP/t9.xml" rank | grep -c . )" +T9_SHOWNROWS="$( printf '%s' "$T9_TAG" | grep -oE 'shown="[0-9]+"' | grep -oE '[0-9]+' )" +# every file outside the shown rows is a rank row (12 - shown files), or the cut tail stops short of them and says so (next=) +if [ "$T9_TOTAL" = "12" ] && { [ "$T9_RANK" = "$(( 12 - T9_NSHOWN ))" ] || { [ "$T9_SHOWNROWS" -lt "$(( 12 - T9_NSHOWN ))" ] && printf '%s' "$T9_TAG" | grep -q ' next="'; }; }; then + ok "(9a) tail total=12; rank rows $T9_RANK = 12 files minus the $T9_NSHOWN shown, or a cut tail naming next= (trimmed rows land IN the tail)" +else + no "(9a) tail total=$T9_TOTAL, $T9_RANK rank row(s) but $T9_NSHOWN file(s) shown — a ranked file served nowhere ($T9_TAG)" +fi T9_EXPECT_FIRST="$( for i in 01 02 03 04 05 06 07 08 09 10 11 12; do printf '%s\n' "w$i.py"; done | grep -vxF -f <( printf '%s\n' "$T9_SHOWN_FILES" ) | head -1 )" -T9_FIRST="$( printf '%s' "$T9" | grep -o '/dev/null )" -T9J_TOTAL="$( printf '%s' "$T9J" | grep -o '"tail":{"total":[0-9]*' | grep -oE '[0-9]+$' )" -T9J_NSHOWN="$( printf '%s' "$T9J" | grep -o '"p":"w[0-9]*\.py"' | sort -u | grep -c . )" # sigs rows carry "p":; the tail carries "files":[] -[ -n "$T9J_TOTAL" ] && [ "$T9J_TOTAL" = "$(( 12 - T9J_NSHOWN ))" ] \ - && ok "(9c) JSON twin: tail total=$T9J_TOTAL = 12 minus its own $T9J_NSHOWN shown file(s)" \ - || no "(9c) JSON twin: tail total='$T9J_TOTAL' but its sigs show $T9J_NSHOWN file(s)" +T9JV="$( printf '%s' "$T9J" | python3 -c ' +import json, sys +d = json.load( sys.stdin ); t = d.get( "tail", {} ) +shown = { s.get( "p" ) for s in d.get( "sigs", [] ) } +rank = [ f for f, y in zip( t.get( "files", [] ), t.get( "sym", [] ) ) if not y ] +cut_ok = t.get( "shown", 0 ) < 12 - len( shown ) and "next" in t +print( "%s %d %d %d" % ( "Y" if t.get( "total" ) == 12 and ( len( rank ) == 12 - len( shown ) or cut_ok ) and not ( set( rank ) & shown ) else "N", + t.get( "total", -1 ), len( rank ), len( shown ) ) )' )" +case "$T9JV" in + Y*) ok "(9c) JSON twin: tail total=12, rank rows = 12 minus its own shown files, none a shown file ($T9JV)" ;; + *) no "(9c) JSON twin: tail wrong (ok total rank shown = $T9JV)" ;; +esac # ── 8) the legend defines both new surfaces where the reader meets them ──────────────────────────────── HDR="$( printf '%s' "$XML" | sed 's/-->.*//' )" -if printf '%s' "$HDR" | grep -q 'tail: file-grain tail'; then ok "legend defines the tail (file-grain, weaker evidence)"; else no "legend does not define the tail"; fi +if printf '%s' "$HDR" | grep -q 'tail: file-grain rows, WEAKER evidence' && printf '%s' "$HDR" | grep -q 'sym= its top symbols'; then ok "legend defines the tail (file-grain, weaker evidence; rank rows, then page rows with sym=)"; else no "legend does not define the tail"; fi if printf '%s' "$HDR" | grep -q 'r= on a ranked row'; then ok "legend defines r= (true ranker order)"; else no "legend does not define r="; fi # ── 9) determinism + well-formedness ─────────────────────────────────────────────────────────────────── @@ -204,7 +260,7 @@ import json, sys r = json.load( sys.stdin ) print( "" if "error" in r else r["result"]["content"][0]["text"] ) ' )" -if printf '%s' "$MCP_INNER" | grep -q ' or no page rows"; fi if printf '%s' "$MCP_INNER" | grep -q ' block, so the shape admits the attribute; the count it asserts is unchanged. -sigblocks(){ grep -oE '' "$1" | wc -l | tr -d ' '; } +# forbudget-soft: the default answer's tag also carries past_target="K" (the rows past its 7.5 KB target) — the same block. +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/docdemotecheck.sh b/test/docdemotecheck.sh index a4fbdabc7..6cc5339a8 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,14 @@ 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 +# UN-PIN 2026-10-15 (lane for-body-slot FIX1; rule 4): docdemotegolden_for.xml back to its pre-lane 6529 B. The lane had +# re-pinned it to 7540 B for a pick=subtoken body (pin_page); the owner ruling dropped the subtoken fallback, and the +# question names no owner (no callable is named by a whole question word), so no body rides and the golden is byte-identical +# to the forbudget-soft FIX2 base's again. +# RE-PIN 2026-10-15 (lane forbudget-soft round C, FOLD C; rule 4): docdemotegolden_for.xml 5839 -> 6529 B (est_tokens "2340" -> +# "2616") and docdemotegolden_noroute.xml 9350 -> 10039 B ("3309" -> "3584"). Only the (page 0 of the file page after the +# rank rows, sym= on its rows, total= every positive-score file), the tail legend clause and est_tokens= moved (checked byte-equal +# with those three stripped). # 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, diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 132e16a1f..44e642ef9 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… \ 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 e2216ec48..b24048a9c 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 ):, which is exactly why this arm asserts presence and test/mcpforparitycheck.sh asserts the # candidate POOL both ladders start from). +# forbudget-soft (owner decisions 2026-10-14; rule 4): the MCP `for` DEFAULT now serves its whole ranked head (stop when +# complete; only the runaway guard drops rows), so the default no longer drops a positive and stays silent — asserted as +# the twin below. The arm's intent ("present when a budget genuinely cuts positives on the MCP dialect") moves to an +# explicit soft target: budget_tokens=1500 cuts the same 40-row head to its rank prefix. MCPCONC="resolve call edges by name" -mcp_call "$INIT" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"for\",\"arguments\":{\"path\":\"$CORPUS\",\"task\":\"$MCPCONC\"}}}" \ +mcp_call "$INIT" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"for\",\"arguments\":{\"path\":\"$CORPUS\",\"task\":\"$MCPCONC\",\"budget_tokens\":1500}}}" \ | result_text >"$TMP/mcp_for.xml" grep -q 'dropped_positive="[0-9]*"' "$TMP/mcp_for.xml" \ - && ok "#6 MCP for verb: dropped_positive= present on a query wide enough to drop" \ - || no "#6 MCP for verb: no dropped_positive= at all (the default MCP budget should trim this broad a query)" + && ok "#6 MCP for verb: dropped_positive= present where a budget_tokens soft target cuts positives" \ + || no "#6 MCP for verb: no dropped_positive= at budget_tokens=1500 on a broad query (the soft target cuts this head)" +mcp_call "$INIT" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"for\",\"arguments\":{\"path\":\"$CORPUS\",\"task\":\"$MCPCONC\"}}}" \ + | result_text >"$TMP/mcp_for_def.xml" +if [ -s "$TMP/mcp_for_def.xml" ] && grep -q ' "$TMP/head-holes/heads.ex" <<'EXHEADHOLES' +defmodule HeadHole do + def bar(x), do: x + def seed(), do: :generated + defmacro generated(x) do + quote do + def unquote(bar(x))(arg), do: arg + end + end + defmacro typed(x) do + quote do + @spec unquote(bar(x)) + end + end + defmacro bare() do + quote do + def unquote(seed)(arg), do: arg + end + end + defmacro bound_name(seed) do + quote do + def unquote(seed)(arg), do: arg + end + end + defmacro disabled(x) do + quote unquote: false do + def unquote(bar(x))(arg), do: arg + end + end +end +EXHEADHOLES +"$BIN" "$TMP/head-holes" --no-cache > "$TMP/head-holes.xml" +python3 - "$TMP/head-holes.xml" <<'PYHEADHOLES' +import sys, xml.etree.ElementTree as ET +syms = {s.get('n'): s for s in ET.parse(sys.argv[1]).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert calls('generated/1') == {'bar/1'}, 'quoted declaration head dropped the live-hole call' +assert calls('typed/1') == {'bar/1'}, 'quoted metadata dropped the live-hole call' +assert calls('bare/0') == {'seed/0'}, 'quoted declaration head mistook a live bare call for a pattern' +assert not calls('bound_name/1'), 'bound live-hole variable became a zero-arity call' +assert not calls('disabled/1'), 'disabled declaration-head hole became live' +print(' PASS live holes in quoted declaration heads/metadata, bare calls and variable negatives') +PYHEADHOLES + +# A module declaration which is itself quoted must not change the lexical +# module of an evaluated hole. A same-file namesake makes wrong-scope fallback visible. +mkdir "$TMP/module-hole" +cat > "$TMP/module-hole/modules.ex" <<'EXMODULEHOLE' +defmodule Real do + def bar(x), do: x + def run(x) do + quote do + defmodule Phantom do + unquote(bar(x)) + end + end + end +end +defmodule Other do + def bar(x), do: x +end +EXMODULEHOLE +"$BIN" "$TMP/module-hole" --no-cache > "$TMP/module-hole.xml" +python3 - "$TMP/module-hole.xml" <<'PYMODULEHOLE' +import sys, xml.etree.ElementTree as ET +syms = {(s.get('n'), s.get('sc', '')): s for s in ET.parse(sys.argv[1]).iter('s')} +assert 'bar/1' in {c.get('n') for c in syms['run/1', 'Real'].iter('c')}, 'inert module ancestor stole the live-hole lexical scope' +assert not any(n == 'Phantom' for n, sc in syms), 'inert module became a definition' +print(' PASS inert module declarations do not shadow live-hole lexical scope') +PYMODULEHOLE +"$BIN" "$TMP/module-hole" --callees=run/1 --no-cache > "$TMP/module-hole-callees.xml" +python3 - "$TMP/module-hole-callees.xml" <<'PYMODULETARGET' +import sys, xml.etree.ElementTree as ET +rows = {(s.get('n'), s.get('p')) for s in ET.parse(sys.argv[1]).iter('s')} +assert rows == {('bar/1', 'modules.ex:2')}, f'live hole bound to the wrong module: {rows}' +print(' PASS live hole reaches Real.bar/1, not the same-file Other.bar/1') +PYMODULETARGET + +# Issue #357: quoted syntax is inert, but unquote expressions are evaluated while +# constructing it. Keep this fixture separate from the golden corpus so the gate +# contrasts the call sites and the declined count directly. +mkdir "$TMP/inert" +cat > "$TMP/inert/quoted.ex" <<'EXINERT' +defmodule InertFixture do + def inert(x), do: x + def bar(x), do: x + def splice(x), do: [x] + def after_call(x), do: x + def quote(x), do: x + def comment(x), do: x + + def run(x) do + quote do + inert(x) + foo(unquote(bar(x))) + [unquote_splicing(splice(x))] + end + after_call(x) + InertFixture.quote(x) + InertFixture.comment(x) + end + + def options(x) do + quote bind_quoted: [item: bar(x)] do + inert(item) + end + end + + def nested(x) do + quote do + unquote(quote do + inert(x) + unquote(bar(x)) + end) + end + end +end +EXINERT +"$BIN" "$TMP/inert" --no-cache > "$TMP/inert.xml" +python3 - "$TMP/inert.xml" <<'PYINERT' +import re, sys, xml.etree.ElementTree as ET +data = open(sys.argv[1]).read() +syms = {s.get('n'): s for s in ET.fromstring(data).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert 'inert/1' not in calls('run/1'), 'ordinary quoted call became an edge' +assert 'bar/1' in calls('run/1'), 'unquote(bar(x)) lost its live call edge' +assert 'splice/1' in calls('run/1'), 'unquote_splicing(splice(x)) lost its live call edge' +assert 'after_call/1' in calls('run/1'), 'call after quote was suppressed' +assert {'quote/1', 'comment/1'} <= calls('run/1'), 'ordinary named functions were treated as special forms' +assert 'bar/1' in calls('options/1'), 'bind_quoted option expression was suppressed' +assert 'inert/1' not in calls('options/1'), 'bind_quoted quote body became live' +assert 'bar/1' in calls('nested/1'), 'inner unquote in nested quote lost its live call edge' +assert 'inert/1' not in calls('nested/1'), 'nested quote body became live' +match = re.search(r'\bdeclined=(\d+)\b', data) +assert match and int(match.group(1)) >= 3, 'inert-region calls vanished from declined accounting' +print(' PASS inert quote bodies, live holes, near misses, boundaries, and declined count') +PYINERT +mkdir "$TMP/inert-control" +python3 - "$TMP/inert/quoted.ex" "$TMP/inert-control/quoted.ex" <<'PYINERTCONTROL' +import pathlib, sys +original = pathlib.Path(sys.argv[1]).read_text() +lines = original.splitlines(keepends=True) +mutated = 0 +for i, line in enumerate(lines): + if line.strip() in {'inert(x)', 'inert(item)'}: + lines[i] = line[:len(line) - len(line.lstrip())] + 'x\n' + mutated += 1 + elif line.strip() == 'foo(unquote(bar(x)))': + lines[i] = line.replace('foo(unquote(bar(x)))', 'unquote(bar(x))') + mutated += 1 +assert mutated == 4, f'control did not remove all four inert call sites: {mutated}' +pathlib.Path(sys.argv[2]).write_text(''.join(lines)) +PYINERTCONTROL +"$BIN" "$TMP/inert-control" --no-cache > "$TMP/inert-control.xml" +python3 - "$TMP/inert.xml" "$TMP/inert-control.xml" <<'PYINERTCOUNT' +import re, sys +def declined(path): + match = re.search(r'\bdeclined=(\d+)\b', open(path).read()) + return int(match.group(1)) if match else 0 +assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) +print(' PASS four inert call sites move declined accounting by four') +PYINERTCOUNT +"$BIN" "$TMP/inert" --callees=run/1 --no-cache > "$TMP/inert-callees.xml" +"$BIN" "$TMP/inert" --callers=inert/1 --no-cache > "$TMP/inert-callers.xml" +python3 - "$TMP/inert.xml" "$TMP/inert-callees.xml" "$TMP/inert-callers.xml" <<'PYINERTVERBS' +import sys, xml.etree.ElementTree as ET +map_text, callees_text, callers_text = [open(p).read() for p in sys.argv[1:]] +assert 'including inert Elixir quote sites' in map_text, 'compact map misdescribes declined sites' +callees = ET.fromstring(callees_text) +assert callees.get('declined_calls') == '2', callees.attrib +assert 'including inert Elixir quote sites' in callees_text, 'compact callees misdescribes declined sites' +callers = ET.fromstring(callers_text) +assert callers.get('count') == '0', callers.attrib +assert 'declined_calls' not in callers.attrib, 'inert sites became possible callers' +print(' PASS declined map/callees accounting, compact readings, and no possible callers') +PYINERTVERBS + +# A non-Elixir body may quote our legend markers as source text. Only real +# comments outside CDATA may select the inert-decline reading. +mkdir -p "$TMP/marker/src" "$TMP/marker/a" "$TMP/marker/b" +cat > "$TMP/marker/src/run.cpp" <<'CPPMARKER' +int run() { + const char* marker = R"marker()marker"; + const char* clause = "ordinary Elixir call candidates in inert quote AST"; + return duplicate(); +} +CPPMARKER +printf 'int duplicate() { return 1; }\n' > "$TMP/marker/a/a.cpp" +printf 'int duplicate() { return 2; }\n' > "$TMP/marker/b/b.cpp" +"$BIN" "$TMP/marker" --pack-top-n=3 --no-cache > "$TMP/marker.xml" +python3 - "$TMP/marker.xml" <<'PYMARKER' +import sys, xml.etree.ElementTree as ET +text = open(sys.argv[1]).read() +ET.fromstring(text) +assert 'declined=1' in text and ' "$TMP/inert-kinds/count.ex" <<'EXKINDS' +defmodule InertKinds do + def bar(x), do: x + def run(x) do + quote do + bar(x) + 1 + 2 + &bar/1 + x + end + end +end +EXKINDS +"$BIN" "$TMP/inert-kinds" --no-cache > "$TMP/inert-kinds.xml" +python3 - "$TMP/inert-kinds.xml" <<'PYKINDS' +import re, sys, xml.etree.ElementTree as ET +text = open(sys.argv[1]).read() +syms = {s.get('n'): s for s in ET.fromstring(text).iter('s')} +assert not list(syms['run/1'].iter('c')), 'inert operator/capture became an edge' +match = re.search(r'\bdeclined=(\d+)\b', text) +assert match and int(match.group(1)) == 3, 'inert operator/named capture references disappeared or variable AST inflated counting' +print(' PASS inert calls, operators and named captures counted; variable AST excluded') +PYKINDS + +# quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). +mkdir "$TMP/quote-options" +cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' +defmodule QuoteOptions do + def bar(x), do: x + def splice(x), do: [x] + def bound(x) do + quote bind_quoted: [x: x] do + unquote(bar(x)) + [unquote_splicing(splice(x))] + end + end + def disabled(x) do + quote unquote: false, do: unquote(bar(x)) + end + def enabled(x) do + quote bind_quoted: [x: x], unquote: true do + unquote(bar(x)) + end + end + def nested_inert(x) do + quote do + quote do + unquote(bar(x)) + end + end + end + def nested_disabled(x) do + quote unquote: false do + unquote(quote do + unquote(bar(x)) + end) + end + end +end +EXOPTIONS +"$BIN" "$TMP/quote-options" --no-cache > "$TMP/options.xml" +python3 - "$TMP/options.xml" <<'PYOPTIONS' +import sys, xml.etree.ElementTree as ET +syms = {s.get('n'): s for s in ET.parse(sys.argv[1]).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert not calls('bound/1'), 'bind_quoted incorrectly activates unquote/splicing' +assert not calls('disabled/1'), 'unquote: false incorrectly activates unquote' +assert calls('enabled/1') == {'bar/1'}, 'explicit unquote: true failed to restore the live hole' +assert not calls('nested_inert/1'), 'unquote in an inert nested quote became live' +assert not calls('nested_disabled/1'), 'disabled outer hole evaluated its nested quote' +print(' PASS disabled unquote, bind_quoted default, and explicit re-enable') +PYOPTIONS +for n in a b c; do "$BIN" "$TMP/inert" > "$TMP/inert-$n.xml"; done +cmp "$TMP/inert.xml" "$TMP/inert-a.xml" +cmp "$TMP/inert-a.xml" "$TMP/inert-b.xml" +cmp "$TMP/inert-b.xml" "$TMP/inert-c.xml" +echo ' PASS inert count and live edges survive cold/warm round trips' for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" diff --git a/test/estcalib.manifest b/test/estcalib.manifest index 36099e7d0..bdea9c43f 100644 --- a/test/estcalib.manifest +++ b/test/estcalib.manifest @@ -9,8 +9,11 @@ pack-signatures 1801 1784 2116 --pack-signatures expand 541 542 455 --expand=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 re-pinned 2026-10-08 on the train 26b tree (knob-honesty's next= + floor clause, measured with pin.py; other rows kept). -for-budgeted 491 486 698 --for=dedupe --token-budget=400 +# for-named / for-named-noe re-pinned 2026-10-15 (forbudget-soft round C, FOLD C: the carries page 0 of the file page; +# 962->1031 / 898->967 o200k, measured with pin.py; the twins still differ by exactly the e= rows, 64 o200k). +for-named 1031 1026 1392 --for=billableTotal +for-named-noe 967 961 1298 RIPWIRE_FOR_ENDLINES=never --for=billableTotal +# for-budgeted re-pinned 2026-10-14 (forbudget-soft: an explicit budget is a soft row target — the header is no longer shed to fit, measured with pin.py; other rows kept). +# …and 2026-10-15 (round C: reserve sections first — the tail keeps its share and carries page rows with sym=; 855->982 o200k). +for-budgeted 982 974 1376 --for=dedupe --token-budget=400 pack-task 882 862 1104 --pack-task=dedupe diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 9c6f6f18f..8be2952e4 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -23,13 +23,18 @@ # gate — that band is the recurrence guard, and it is the reason this file asserts a band and never a # bit-exact token count (house rule: float/estimate assertions get a tolerance, never bit-equality). # -# BY-DESIGN BREAK (rule 4; owner ruling 2026-10-09, option B; reports/lego-charge.md fix round 5): #11 A7's old bar -# "--token-budget=2000 --detail=20 --with-graph on src stays inside N x 2.36 x 1.15" no longer holds for that ONE shape. -# With BOTH trailing sections under an explicit ceiling the answer KEEPS its ranked rows (a gold owner row, textual-12 -# Reactive._set, was cut when the two sections were reserved out of ) and goes over the budget, labelled -# over_ceiling="1" with a TRUE reason and a next= on every cut: answers first, a budget never silently removes an -# answer. The old assertion is kept as A7 twin wherever it must still hold (each flag alone at 2000, and both flags at -# a budget the rows fit); A7B asserts the new contract for both-flags-over-budget. +# BY-DESIGN BREAK (forbudget-soft (rule 4); owner decisions 2026-10-14). An explicit --token-budget=N on --for (and on +# --pack-task, --from-trace, MCP for/explore) is a SOFT TARGET, not a byte ceiling: T = N x 2.36 x 0.90 bytes is charged +# against the ROWS only, rows are added in rank order while they fit T (the rank 1..4 floor always ships), the cut +# carries next=, est_tokens may exceed N, and nothing is labelled over_ceiling= or dropped from the header for it (no rung +# zero, no "(ceiling)" drop, no trailing-section reserve, no paid/unpaid continuation). The arms that asserted the old +# byte promise (delivered <= N x 2.36 x 1.15, or over_ceiling="1"; the kept-rows / unpaid / header-floor reasons; the +# reserve; rung zero) are REPLACED below by the invariants that guard the new behaviour on the same fixtures: ST (rows fit +# T or are the floor, and a cut is maximal), NOLBL (no over_ceiling= on a soft lens), SEC (--detail / --with-graph never +# remove a row vs the bare run at the same N), RP (rows a rank prefix, monotone in N), NX (every cut carries a +# next= that runs), HL (no legend clause, task echo or route= dropped). What still holds is kept unchanged: est_tokens +# prices the delivered document exactly (#1, A7/A8 bands, A9/A10, §C1, #19), and every HARD ceiling keeps its twins +# (--max-tokens #5/§C4, the map/payload --token-budget exit-3 gate #3/#12, --handoff #15(c)). # # Usage: bash test/estchargecheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/estchargecheck.sh # Exits non-zero on any failure; prints PASS/FAIL per check, ALL PASS on success. @@ -37,6 +42,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" echo "estchargecheck: BIN=$BIN" @@ -134,8 +140,10 @@ est_of(){ grep -aoE 'est_tokens=[0-9]+' "$1" | head -1 | grep -oE '[0-9]+'; } bytes_of(){ wc -c < "$1" | tr -d ' '; } # ── SHARED DEFINITIONS (moved here unchanged from #11 so the sections below can run as independent chains) ─────── -# FOR_TASK and forest_of are read by #11, #14 and #15; KEPT_NOTE/FLOOR_NOTE by #11 and #11 A7R; sigs_unpaid_twin by -# #11 and its sweep. Defining them before the sections fork is the only reason they live here. +# FOR_TASK and forest_of are read by #11, #14 and #15; the forbudget-soft soft-target helpers (soft_rows / soft_st and +# the $TMP/soft_rows.py / soft_st.py they run, rows_prefix, ctx_root, SOFT_CLAUSE_*) by #11, its sweep, A7R/A7D, #15 and +# #19. Defining them before the sections fork is the only reason they live here. (forbudget-soft deleted KEPT_NOTE / +# FLOOR_NOTE / sigs_unpaid_twin with the hard-ceiling arms that read them: no section reads them any more.) FOR_TASK="serialize the map" # WEAK_QUERY is #11 A9's nonsense word. It is spelled ONLY here, at top level, never inside a function: the # repo's own bash functions are indexed symbols, so a section body that carried the word would make it match @@ -172,41 +180,151 @@ print( m.group( 1 ).decode() if m else '' ) PYX } -KEPT_NOTE='[over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections' -FLOOR_NOTE='[over_ceiling= is 1 on the root: the header floor' -# 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. Train 26c: the single A7 arm it guarded is superseded by the lane's A7 twin loop + -# A7B below; the twin itself still serves the sweep and the floor arm. 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 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 unpaid clause' ) -nxt = re.search( r' next="--for=[^"]*"', sigs.group( 0 ) ) if sigs else None -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 ) -lastRung = '[over_ceiling= is 1 on the root: ' in d # either last-rung reason: the header floor, or (lego-charge) the kept rows -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 -} +# ── forbudget-soft (rule 4): THE SOFT TARGET's invariants, shared by the A7/A7B/sweep/floor/C3/A7R/#15/#19 arms below. An +# explicit --token-budget=N on --for charges T = N x 2.36 x 0.90 bytes (serialize.h kMinBytesPerToken x kBudgetHeadroom) +# against the rows inside ONLY — never the header, a disclosure or a trailing section — so the old "delivered <= N x +# 2.36 x 1.15 or over_ceiling=1" bar is gone by design (est_tokens may exceed N, unlabelled) and these replace it: +# soft_rows FILE the served rows as p:l:n in RANK order (r=), one per line; exit 3 when there is no +# soft_st FILE N [REF] ST: the bytes inside (the rows as printed) fit T, or the rows are only the rank 1..4 floor; +# with REF (the same shape's complete, no-budget answer) a cut must also be MAXIMAL — in the ladder's +# own currency (serialize.h entryCost: a row's printed bytes plus the zero cx=/ccx=/in= the lean +# spelling elides), the served rows plus REF's next row would cross T (160 B slack, as +# forsoftbudgetcheck's (ST)). Prints "OK|BAD ...", exit 0|1. +# rows_prefix A B RP: rows file A is a non-empty rank prefix of rows file B +# ctx_root FILE the root open tag (an over_ceiling= is read off the root only: a can carry its own) +cat >"$TMP/soft_rows.py" <<'PY' +import re, sys +s = open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( "\n".join( x[ 1 ] for x in rows ) ) +PY +cat >"$TMP/soft_st.py" <<'PY' +import re, sys +def charge( a, printed ): + return printed + sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +def rb( p ): + s = open( p, encoding='utf-8', errors='replace' ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return None + inner = m.group( 1 ); rows = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + r = re.search( r'\br="(\d+)"', a ) + printed = len( d.group( 0 ).encode() ); c = charge( a, printed ); elided += c - printed + rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + rows.sort() + return len( inner.encode() ), len( inner.encode() ) + elided, rows +def row_ids( p ): # soft_rows.py's list, for an in-process caller (the sweep) + s = open( p, encoding='utf-8', errors='replace' ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return [] + rows = [] + for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) + rows.sort() + return [ x[ 1 ] for x in rows ] +def verdict( path, n, refpath=None ): + a = rb( path ); T = int( int( n ) * 2.36 * 0.90 ) + if a is None: return False, "BAD no " + pb, ab, ar = a; k = len( ar ) + fits = pb <= T or k <= 4 + if refpath is None: + return fits, "%s fits=%d rows=%d bytes=%d T=%d" % ( "OK" if fits else "BAD", fits, k, pb, T ) + ref = rb( refpath ) + if ref is None: return False, "BAD no in the reference answer" + rr = ref[ 2 ] + if k >= len( rr ): + return fits, "%s fits=%d rows=%d/%d bytes=%d T=%d complete" % ( "OK" if fits else "BAD", fits, k, len( rr ), pb, T ) + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served first + # and the rows past the target cut fill what they leave of T, so maximality is judged against T less their bytes (U: all + # after but , and the rows — less the --detail bodies, the caller's own request under its own ceiling: + # on a run without bundle= on the root the element is the --detail section) + s = open( path, encoding='utf-8', errors='replace' ).read() + U = ( len( s.split( '', 1 )[ 1 ].encode() ) - len( '' ) ) if '' in s else 0 + U += sum( len( h.encode() ) for h in re.findall( r']*/>', s.split( ']*>', s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r'', s, re.S ) + U -= len( bm.group( 0 ).encode() ) if bm else 0 + maximal = ab + rr[ k ][ 1 ] > T - U - 160 + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + from softbudget import upper_bound + upper, udet = upper_bound( s, open( refpath, encoding='utf-8', errors='replace' ).read(), T, k, len( rr ), U ) + return fits and maximal and upper, "%s fits=%d maximal=%d upper=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d U=%d%s" % ( + "OK" if fits and maximal and upper else "BAD", fits, maximal, upper, k, len( rr ), pb, ab, rr[ k ][ 1 ], T, U, "" if upper else " -- " + udet ) +if __name__ == "__main__": + good, msg = verdict( sys.argv[ 1 ], sys.argv[ 2 ], sys.argv[ 3 ] if len( sys.argv ) > 3 else None ) + print( msg ); sys.exit( 0 if good else 1 ) +PY +# #11 A7 sweep's one-pass judge and #18's rung-zero corpus writer: helper scripts in $TMP, written once here like soft_st.py +# (moved out of est_11_sweep / est_18 unchanged at train 28, so each section reads as its arms) +cat >"$TMP/a7s_judge.py" <<'PYS' +import re, sys +sys.path.insert( 0, sys.argv[ 2 ] ) +import soft_st +runs = badn = cut = over = 0; bad = []; prev = {} +for line in open( sys.argv[ 1 ] ): + label, n, rc, f, ref = line.split() + n = int( n ); runs += 1; why = [] + d = open( f, encoding='utf-8', errors='replace' ).read() + root = re.search( r'^]*>', d ); root = root.group( 0 ) if root else '' + est = re.search( r' est_tokens="(\d+)"', root ) + if rc != '0' or not root or not est: why.append( 'exit-%s/no-root-or-est' % rc ) + if ' over_ceiling=' in root or '[over_ceiling= is 1 on the root: ' in d: why.append( 'labelled' ) + good, msg = soft_st.verdict( f, n, ref ) + if not good: why.append( 'ST:[%s]' % msg ) + rows = soft_st.row_ids( f ) + if not rows or ( label in prev and rows[ :len( prev[ label ] ) ] != prev[ label ] ): why.append( 'not-monotone' ) + prev[ label ] = rows + sigs = re.search( r']*>', d ); sigs = sigs.group( 0 ) if sigs else '' + sh = re.search( r' shown="(\d+)"', sigs ); tot = re.search( r' total="(\d+)"', sigs ) + if sh and tot and int( sh.group( 1 ) ) < int( tot.group( 1 ) ): + cut += 1 + if ' next="--for=' not in sigs: why.append( 'cut-without-next' ) + if est and int( est.group( 1 ) ) > n: over += 1 + if why: + badn += 1 + if len( bad ) < 6: bad.append( '%s@%d:[%s]' % ( label, n, ' '.join( why ) ) ) +print( runs, badn, cut, over ) +for b in bad: print( b ) +PYS +cat >"$TMP/rz_corpus.py" <<'PYRZ' +import os, sys +out = sys.argv[ 1 ] +lines = [ "def widgetPingBoxRouter( alpha, beta ):", + ' """Widget ping box router: route every alpha reading onto the beta box."""', + " total = 0" ] +for j in range( 26 ): + lines.append( f" total = total + alpha * {j} - beta * {j} + widgetPingStep{j % 4}( total, {j} )" ) +lines.append( " return total" ) +with open( os.path.join( out, "router.py" ), "w" ) as fh: + fh.write( "\n".join( lines ) + "\n" ) +with open( os.path.join( out, "steps.py" ), "w" ) as fh: + for j in range( 4 ): + fh.write( f'def widgetPingStep{j}( total, step ):\n """Step {j}."""\n return total + step\n\n' ) +PYRZ +soft_rows(){ python3 "$TMP/soft_rows.py" "$1"; } +soft_st(){ python3 "$TMP/soft_st.py" "$@"; } +rows_prefix(){ [ -s "$1" ] && [ "$( head -n "$( wc -l < "$1" | tr -d ' ' )" "$2" )" = "$( cat "$1" )" ]; } +ctx_root(){ grep -aoE '^]*>' "$1" | head -1; } +# the soft-target legend clauses (verbs_for.h / graphlegend.h): the compact dialect's reading of budget_tokens=, and the full one's +SOFT_CLAUSE_COMPACT='budget_tokens= the soft row target asked (rows stop once it is reached)' +SOFT_CLAUSE_FULL='[budget_tokens=: the soft token target; rows stop once it is reached, the rank 1..4 rows and every disclosure always ship, so est_tokens may exceed it]' est_1(){ # ── #1: the CHARGE band. For every payload shape, bytes/est_tokens must sit inside the calibrated B/tok @@ -713,97 +831,106 @@ for label in for_graph for_detail for_both; do || no "#11 A7/A8 $label: est_tokens=$E did not move above the bare bundle's $E_FOR — that section is uncharged" done -# A7 — and --token-budget must SHAPE the bodies, not just the signatures. D10: --for trims to fit, exit 0. -# The pre-fix binary streamed 68 035 B here against a 4 248 B ceiling; the bar is the stated ceiling plus the -# documented single-entry overshoot tolerance (serialize.h kCeilingFirstEntryTolerance = 1.15). -# BY-DESIGN BREAK (owner ruling 2026-10-09, option B — see this file's header): the old arm ran ONLY the both-flags shape -# at 2000, which now keeps its rows and goes over on purpose. A7 TWIN keeps the old assertion, unchanged in substance -# (exit 0, delivered <= the allowance), on every shape where it must still hold: each trailing flag alone at 2000, and -# both flags at 5000 where the ranked rows fit (measured at fix round 5: 4727 / 4769 / 12337 B vs 5428 / 5428 / 13570 B). +# (The soft-target helpers — soft_rows / soft_st / rows_prefix / ctx_root and SOFT_CLAUSE_* — live in SHARED DEFINITIONS +# above: the sweep, A7R/A7D, #15 and #19 sections read them too.) + +# A7 TWIN — REPLACED (forbudget-soft (rule 4)). Was: exit 0 and delivered <= N x 2.36 x 1.15 for --detail=20 / --with-graph at +# 2000 and both flags at 5000 (the trailing sections bounded by the budget). Under a soft target the trailing sections are +# funded from what the rows left of T and the document may land past N, unlabelled — so the byte bar asserts the deleted +# contract. The invariant that must hold on the same three shapes: exit 0; NOLBL (no over_ceiling= on the root); SEC (the +# trailing flag never removes a row — the bare run's rows at the same N are a rank prefix of the shape's rows); and ST +# (the shape's rows fit T or are the floor, and a cut is maximal against the same shape's no-budget answer from A7/A8 above). +# MEASURED on d099ec21: 20/20/40-row prefixes, rows identical with and without the flags; the base binary (d4325a8c) served +# 10 / 11 rows with --detail=20 / --with-graph against the bare run's 14 — the reserve the soft target deleted. TBF=2000 -ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" -for tw in "2000:--detail=20" "2000:--with-graph" "5000:--detail=20 --with-graph"; do - tw_n="${tw%%:*}"; tw_args="${tw#*:}" - tw_allow="$( awk "BEGIN{printf \"%d\", $tw_n*2.36*1.15}" )" +ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" # still read by the no-budget --detail arm below +rw src --for="$FOR_TASK" --token-budget=$TBF >"$TMP/f_tbp.out" 2>/dev/null +rw src --for="$FOR_TASK" --token-budget=5000 >"$TMP/f_tbp5.out" 2>/dev/null +for tw in "2000:--detail=20:f_tbp:f_for_detail" "2000:--with-graph:f_tbp:f_for_graph" "5000:--detail=20 --with-graph:f_tbp5:f_for_both"; do + tw_n="${tw%%:*}"; tw_rest="${tw#*:}"; tw_args="${tw_rest%%:*}"; tw_rest="${tw_rest#*:}"; tw_bare="${tw_rest%%:*}"; tw_ref="${tw_rest#*:}" # shellcheck disable=SC2086 rw src --for="$FOR_TASK" --token-budget=$tw_n $tw_args >"$TMP/f_tw.out" 2>/dev/null rc_tw=$? - tw_b="$( bytes_of "$TMP/f_tw.out" )" - { [ "$rc_tw" -eq 0 ] && [ -n "$tw_b" ] && [ "$tw_b" -le "$tw_allow" ]; } 2>/dev/null \ - && ok "#11 A7 twin --for --token-budget=$tw_n $tw_args: $tw_b B within the $tw_allow B allowance (exit 0, SHAPED)" \ - || no "#11 A7 twin --for --token-budget=$tw_n $tw_args: ${tw_b:-?} B vs the $tw_allow B allowance, exit $rc_tw — the budget does not bound the appended sections" + tw_root="$( ctx_root "$TMP/f_tw.out" )" + soft_rows "$TMP/f_tw.out" >"$TMP/tw.rows" 2>/dev/null; soft_rows "$TMP/$tw_bare.out" >"$TMP/tw_bare.rows" 2>/dev/null + tw_st="$( soft_st "$TMP/f_tw.out" "$tw_n" "$TMP/$tw_ref.out" )"; tw_st_rc=$? + tw_why="" + { [ "$rc_tw" -eq 0 ] && [ -n "$tw_root" ]; } || tw_why="$tw_why exit-$rc_tw-or-no-root" + [ -n "$tw_root" ] && [ "${tw_root#* over_ceiling=}" = "$tw_root" ] || tw_why="$tw_why over_ceiling-on-a-soft-target" + # round C (rule 4): SEC ("the trailing flag never removes a row") is the by-design break — a trailing section is + # reserved before the rows past the cut — so the rows clause is RP: the shape's rows are a rank prefix of its own + # no-budget answer's rows (the cut and ST above/below keep the rest) + soft_rows "$TMP/$tw_ref.out" >"$TMP/tw_ref.rows" 2>/dev/null + rows_prefix "$TMP/tw.rows" "$TMP/tw_ref.rows" || tw_why="$tw_why rows:$( wc -l < "$TMP/tw.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" + [ "$tw_st_rc" -eq 0 ] || tw_why="$tw_why ST:[$tw_st]" + [ -z "$tw_why" ] \ + && ok "#11 A7 twin (forbudget-soft, round C) --for --token-budget=$tw_n $tw_args: exit 0, no over_ceiling=, $( wc -l < "$TMP/tw.rows" | tr -d ' ' ) rows a rank prefix of the no-budget answer (bare: $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' )) ($tw_st)" \ + || no "#11 A7 twin (forbudget-soft) --for --token-budget=$tw_n $tw_args —$tw_why" done -# A7B — THE NEW CONTRACT for both flags OVER the budget (owner ruling, option B). Same query, same 2000 tokens. It must: -# (premise) deliver past the allowance at exit 0 — else the shape now fits and this arm must be re-anchored, not passed; -# (rows) keep every row the same budget shows WITHOUT the trailing flags, and those rows are the uncapped -# answer's top rows in its order (no higher-ranked row of the uncapped answer is missing) — the trailing -# sections never remove an answer row; -# (label) carry over_ceiling="1" on the root; -# (reason) name the reason TRUTHFULLY: the last-rung note is the kept-rows one, not "the header floor ... exceeds", -# and what it names is there — the header alone fits the allowance, and rows, the graph block and a -# first --detail body are all present; -# (next) a cut first body carries a next= that recovers exactly the lines it cut (checked on A7R's corpus below, -# whose first body is longer than the floor; here a first body served whole carries no truncated=). -rows_of(){ grep -aoE ']*>' "$1" | sed -nE 's/.* n="([^"]*)".*/\1/p'; } +# A7B — REPLACED (forbudget-soft (rule 4)). Was the owner-option-B contract for both flags OVER the budget: a premise past the +# allowance, over_ceiling="1", the kept-rows reason. All three die with the ceiling; its ROWS clause is the soft target's own +# invariant and is kept: --detail=20 --with-graph at 2000 keeps every row of the flagless 2000 answer, and its rows are +# the uncapped answer's top rows in rank order (no higher-ranked row missing). Plus NOLBL, both trailing sections present, and +# a cut first body still carries the next= that serves the rest. rw src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? -rw src --for="$FOR_TASK" --token-budget=$TBF >"$TMP/f_tbp.out" 2>/dev/null -FTB="$( bytes_of "$TMP/f_tb.out" )" -a7b_root="$( grep -aoE '^]*>' "$TMP/f_tb.out" | head -1 )" -a7b_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b""$TMP/a7b.rows"; rows_of "$TMP/f_tbp.out" >"$TMP/a7b.plain"; rows_of "$TMP/f_for.out" >"$TMP/a7b.unc" +a7b_root="$( ctx_root "$TMP/f_tb.out" )" +soft_rows "$TMP/f_tb.out" >"$TMP/a7b.rows" 2>/dev/null; soft_rows "$TMP/f_tbp.out" >"$TMP/a7b.plain" 2>/dev/null; soft_rows "$TMP/f_for.out" >"$TMP/a7b.unc" 2>/dev/null a7b_n="$( wc -l < "$TMP/a7b.rows" | tr -d ' ' )"; a7b_pn="$( wc -l < "$TMP/a7b.plain" | tr -d ' ' )" a7b_why="" -{ [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ] && [ -n "$FTB" ] && [ "$FTB" -gt "$ALLOW" ]; } 2>/dev/null \ - || a7b_why="$a7b_why premise:${FTB:-?}B<=${ALLOW}B-or-exit-$rc_ftb(re-anchor)" -{ [ "$a7b_pn" -ge 1 ] && [ "$a7b_n" -ge "$a7b_pn" ] && [ -z "$( grep -vxF -f "$TMP/a7b.rows" "$TMP/a7b.plain" )" ] \ - && [ "$( head -n "$a7b_n" "$TMP/a7b.unc" )" = "$( cat "$TMP/a7b.rows" )" ]; } 2>/dev/null \ - || a7b_why="$a7b_why rows:$a7b_n-vs-plain-$a7b_pn-or-not-the-uncapped-prefix" -[ "${a7b_root#* over_ceiling=\"1\"}" != "$a7b_root" ] || a7b_why="$a7b_why no-over_ceiling" -{ grep -aqF "$KEPT_NOTE" "$TMP/f_tb.out" && ! grep -aqF "$FLOOR_NOTE" "$TMP/f_tb.out" && [ "${a7b_hdr:--1}" -gt 0 ] && [ "$a7b_hdr" -le "$ALLOW" ] \ - && grep -aq ']*>/dev/null \ - || a7b_why="$a7b_why reason:not-the-kept-rows-note-or-untrue(header ${a7b_hdr:-?}B)" +{ [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ]; } || a7b_why="$a7b_why exit-$rc_ftb-or-no-root" +# round C (rule 4): "keeps every flagless row" is the by-design break (the graph block and the first body are reserved before the +# rows past the cut); the uncapped answer's rank-prefix clause below is the rows invariant +rows_prefix "$TMP/a7b.rows" "$TMP/a7b.unc" || a7b_why="$a7b_why rows:not-the-uncapped-answer's-rank-prefix" +[ -n "$a7b_root" ] && [ "${a7b_root#* over_ceiling=}" = "$a7b_root" ] || a7b_why="$a7b_why over_ceiling-on-a-soft-target" +{ grep -aq ']*>/dev/null 2>&1; then rw src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --legend=full >"$TMP/f_tbl.out" 2>/dev/null a7x_bad="" + grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/f_tb.out" || a7x_bad="$a7x_bad f_tb:no-soft-target-clause" + grep -aqF "$SOFT_CLAUSE_FULL" "$TMP/f_tbl.out" || a7x_bad="$a7x_bad f_tbl:no-soft-target-clause" for f in f_tb f_tbl; do - grep -aqF "$KEPT_NOTE" "$TMP/$f.out" || a7x_bad="$a7x_bad $f:no-kept-rows-note(premise)" + [ -s "$TMP/$f.out" ] || a7x_bad="$a7x_bad $f:empty" xmllint --noout "$TMP/$f.out" 2>/dev/null || a7x_bad="$a7x_bad $f:xmllint-rejected" done [ -z "$a7x_bad" ] \ - && ok "#11 A7B-X the kept-rows answers (compact + --legend=full) carry the note and are well-formed XML" \ - || no "#11 A7B-X the kept-rows note breaks the answer's XML or no longer rides —$a7x_bad" + && ok "#11 A7B-X (forbudget-soft) the soft-target answers (compact + --legend=full) define budget_tokens= as a soft target and are well-formed XML" \ + || no "#11 A7B-X (forbudget-soft) the soft-target disclosure is missing or breaks the answer's XML —$a7x_bad" else printf ' SKIP #11 A7B-X xmllint not installed\n' fi -# A7B-N (fix round 6, checklist 1 — the negative of A7B's reason): the kept-rows note is chosen only when the header as -# built fits the allowance by itself. Same both-flags shape, but a 700-char task at 300 tokens: the verbatim task echo -# alone puts the header past the 814 B allowance, so the TRUE reason is the header floor and the kept-rows note would be -# false. Red on the mutant that drops the header-fits term from keptTrailingOverrun (review mutant MX). +# A7B-N — REPLACED (forbudget-soft (rule 4)). Was the negative of A7B's reason: a 700-char task at 300 tokens puts the header +# alone past the allowance, so the last rung must name the header floor. There is no ladder on a soft target — so the same +# shape now pins HL: the root keeps task= VERBATIM (all 700 chars) and route=, nothing is dropped "(ceiling)", no task_echo / +# legend-clause drop note, no last-rung note, no over_ceiling=; the rank 1..4 floor ships and the cut carries next=. A7N_TASK="$( python3 -c 'w="serialize the map rows into the output buffer while keeping the row order stable and flushing the writer "; print((w*10)[:700].strip())' )" rw src --for="$A7N_TASK" --token-budget=300 --detail=3 --with-graph >"$TMP/f_tbn.out" 2>/dev/null rc_a7n=$? -a7n_allow="$( awk "BEGIN{printf \"%d\", 300*2.36*1.15}" )" -a7n_root="$( grep -aoE '^]*>' "$TMP/f_tbn.out" | head -1 )" -a7n_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b"/dev/null \ - || a7n_why="$a7n_why premise:header-${a7n_hdr:-?}B-not-past-${a7n_allow}B-or-exit-$rc_a7n(re-anchor)" -[ "${a7n_root#* over_ceiling=\"1\"}" != "$a7n_root" ] || a7n_why="$a7n_why no-over_ceiling" -grep -aqF "$FLOOR_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why no-header-floor-note" -! grep -aqF "$KEPT_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why kept-rows-note-false-here" +{ [ "$rc_a7n" -eq 0 ] && [ -n "$a7n_root" ]; } || a7n_why="$a7n_why exit-$rc_a7n-or-no-root" +python3 -c 'import re,sys; m=re.search(r" task=\"([^\"]*)\"", sys.argv[1]); sys.exit(0 if m and m.group(1)==sys.argv[2] else 1)' "$a7n_root" "$A7N_TASK" \ + || a7n_why="$a7n_why task=-not-verbatim" +[ "${a7n_root#* route=\"}" != "$a7n_root" ] || a7n_why="$a7n_why no-route=" +[ "${a7n_root#* over_ceiling=}" = "$a7n_root" ] || a7n_why="$a7n_why over_ceiling-on-a-soft-target" +! grep -aqE '\(ceiling\)|task_echo: dropped|\[legend clauses:|\[over_ceiling= is 1 on the root' "$TMP/f_tbn.out" || a7n_why="$a7n_why header-dropped-for-the-budget" +{ [ -n "$a7n_sh" ] && [ -n "$a7n_tot" ] && { [ "$a7n_sh" -ge 4 ] || [ "$a7n_sh" -eq "$a7n_tot" ]; } \ + && { [ "$a7n_sh" -eq "$a7n_tot" ] || [ "${a7n_sigs#* next=\"--for=}" != "$a7n_sigs" ]; }; } 2>/dev/null \ + || a7n_why="$a7n_why floor-or-next:shown=${a7n_sh:-?}/${a7n_tot:-?}" [ -z "$a7n_why" ] \ - && ok "#11 A7B-N 700-char task at --token-budget=300 --detail=3 --with-graph: header ${a7n_hdr} B alone past the ${a7n_allow} B allowance, the last rung names the header floor, not the kept rows" \ - || no "#11 A7B-N header-alone-over shape names the wrong reason —$a7n_why" + && ok "#11 A7B-N (forbudget-soft) 700-char task at --token-budget=300 --detail=3 --with-graph: task= verbatim, route= kept, no drop, no over_ceiling=, floor $a7n_sh of $a7n_tot rows with next=" \ + || no "#11 A7B-N (forbudget-soft) 700-char task at --token-budget=300 —$a7n_why" # 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)" \ @@ -811,19 +938,18 @@ grep -aqF "$FLOOR_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why no-header-floor-no } est_11_sweep(){ -# A7 SWEEP (PR #135 CI, 2026-09-11) — the budget must bound the document at EVERY budget, not at the one operating -# point above. That arm went red at 5 429 B against its 5 428 B allowance when a merge moved the live src/ corpus, -# and the main binary emits the same 5 429 bytes on the same tree, so the corpus only exposed it. The defect: the -# ceiling ladder priced the document WITHOUT the root over_ceiling="1" (17 B) and the legend clause defining it -# (53 B), which runForLens splices on AFTER the ladder whenever est_tokens exceeds budget_tokens. est_tokens prices -# markup at 2.50 B/tok and the allowance is sized at 2.36 x 1.15 = 2.714 B/tok, so every bundle in that band -# carries 70 unpriced bytes, and one the ladder fitted within 70 B of the allowance is pushed past it with no rung -# fired. One budget on the live tree only sees that when the corpus lands a bundle in the 70 B window, so this arm -# builds its OWN git-less corpus in $TMP and runs it from a relative path (no at=, no churn, a fixed root=, nothing -# read from the live repo) and SWEEPS the budget in 10-token steps: the window recurs with every trimmed row -# (~50 tokens), so a sweep this dense crosses it whatever the legend lengths are. Two shapes — the default bundle -# and A7's own --detail=20 --with-graph. THE PROPERTY: delivered bytes <= N x 2.36 x 1.15 at exit 0, or the -# ladder's LAST rung fired and says so (the header floor alone exceeds the budget — the one overshoot it documents). +# A7 SWEEP — the harness is kept, its property REPLACED (forbudget-soft (rule 4)). Was (PR #135 CI, 2026-09-11): delivered +# bytes <= N x 2.36 x 1.15 at exit 0 at EVERY budget, or the ladder's disclosed last rung / the unpaid twin — the guard +# against a byte spliced in after the ladder priced the document. A soft target makes no byte promise, so the sweep now pins +# the soft target's own laws at every budget of a dense sweep over the same git-less corpus (run from a relative path: no +# at=, no churn, a fixed root=): exit 0; NOLBL (no root over_ceiling=, no last-rung note); ST (rows fit T or are the floor, +# and a cut is maximal against the spec's own no-budget answer); RP (rows(N) a rank prefix of rows(N + step) — monotone); +# NX (a with shown < total carries next=). The byte-exactness the old arm protected lives on in est_tokens' +# identities (A9/A10, §C1, #19): est_tokens prices the delivered document, whatever its size. +# RE-ANCHORED (forbudget-soft): the --detail=20 --with-graph spec ran 2880..3080, the old late-label band; on the soft target +# that range serves all 32 rows (complete), so it now sweeps 300..2500, where the rows run from the 4-row floor to complete +# (d099ec21: 4 rows at 300, 28 at 2000, 32 at 2500). The default spec keeps 760..1500 (8..20 rows); the one-row named task +# keeps 400..600 — its document prices at est_tokens=719, past every N there: the soft overshoot. A7S="$TMP/a7sweep" mkdir -p "$A7S/corpus" python3 - "$A7S/corpus" <<'PYG' @@ -842,96 +968,79 @@ 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_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 -# 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. -# 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 +a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_over=0; a7s_cut=0 +# RUNTIME (CI's 1200 s per-gate limit): the sweep keeps only the rungs where the served row set CHANGES — the first and the +# last budget of every distinct row count, plus the band edges — and judges all of them in ONE python pass after the runs +# (it was 131 rungs with three interpreter starts each, ~266 s of this gate's wall time). Measured on the 480bc90b binary at +# step 10 / 50 / 20: default 760..1500 serves 8..20 rows (13 counts over 75 rungs), --detail=20 --with-graph 300..2500 serves +# 4..32 (29 counts over 45 rungs), the named task 1 row everywhere. A rung dropped here served exactly the rows of a kept +# neighbour on both sides, so every ST fit (tightest at a count's FIRST budget) and every maximality (tightest at its LAST) +# is still asserted, and RP still compares each adjacent pair of distinct row sets. +A7S_RUNGS_default="760 770 780 840 850 900 910 960 970 1020 1030 1090 1100 1150 1160 1210 1220 1270 1280 1330 1340 1400 1410 1460 1470 1500" +A7S_RUNGS_detail_graph="300 500 550 600 650 700 750 800 850 900 950 1000 1050 1100 1150 1200 1250 1300 1350 1400 1450 1500 1550 1600 1650 1700 1750 1800 1850 1900 1950 2000 2050 2100 2150 2200 2250 2500" +A7S_RUNGS_named_one_row="400 600" +: >"$A7S/manifest" +for spec in "default:serialize the map:" "detail_graph:serialize the map:--detail=20 --with-graph" "named_one_row:serializeRow0_3:"; do + s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" + eval "s_rungs=\$A7S_RUNGS_$s_label" + # shellcheck disable=SC2086 + ( cd "$A7S" && rwcall corpus --for="$s_task" $s_args --no-cache ) >"$A7S/ref_$s_label.xml" 2>/dev/null # the spec's complete answer + for N in $s_rungs; do # shellcheck disable=SC2086 - ( cd "$A7S" && rwcall 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 )" - # fix round 5 (lego-charge): the last rung now names one of two reasons, each only where it is true — the header floor, - # or (both trailing sections under the ceiling) the kept rows. Either is the disclosure; the kept-rows one must also be - # TRUE here: only on the both-flags shape, and with the header alone inside the allowance. - s_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b" unpaid twin above - # holds in full - elif [ "$s_rc" -eq 0 ] && [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml" \ - && 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: ' "$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 - a7s_inside_labelled=$(( a7s_inside_labelled + 1 )) - fi + ( cd "$A7S" && rwcall corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o_${s_label}_$N.xml" 2>/dev/null + printf '%s %s %s %s %s\n' "$s_label" "$N" "$?" "$A7S/o_${s_label}_$N.xml" "$A7S/ref_$s_label.xml" >>"$A7S/manifest" 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; 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" -# TRAIN 26c RE-PIN (2026-10-10) of the three regime arms below — the floor twin, C3 and its near miss. lego-charge charges -# / to the budget at their served size and #362's rides the bundle, so on this corpus the -# merged binary's 4-row floor answer costs ~270 B more than 26b's: at "serialize the map" the cut never leaves 5 rows while -# both the unpaid and the paid answer land over (the paid 4-row answer fits from 920, the cut leaves 5 rows from 920 — no C3 -# window at any budget 700..1400, swept step 10 on the merged binary and on lego-charge's pre-continuation binary 1cfbfdcc). -# The task "map row serializer" on the same corpus has all three regimes: the floor at 890..910 (4 rows, the handle unpaid, -# no last-rung note; the pre-continuation binary fits with 4 rows), C3 at 920..939 (the cut leaves 5 rows — 1cfbfdcc serves -# 5, over, last rung fired — and the paid 4-row answer is over too), the payable near miss from 940 (the paid 4-row answer -# fits, no label). Pinned at 900 / 925 / 970. The sweep above keeps "serialize the map". Assertions unchanged. -# 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. +# one pass over every rung: exit 0 + root + est_tokens; NOLBL (no root over_ceiling=, no last-rung note); ST (soft_st's verdict); +# RP (each rung's rows a rank prefix of the next rung's, same spec); NX (shown < total carries next="--for=). Prints +# "runs badn cut over", then up to six "label@N:[why]" lines. +a7s_out="$( python3 "$TMP/a7s_judge.py" "$A7S/manifest" "$TMP" )" # the one-pass judge: SHARED DEFINITIONS +read -r a7s_runs a7s_badn a7s_cut a7s_over <<<"$( printf '%s\n' "$a7s_out" | head -1 )" +a7s_bad="$( printf '%s\n' "$a7s_out" | tail -n +2 | tr '\n' ' ' )" +{ [ -n "$a7s_runs" ] && [ "$a7s_runs" -eq 66 ] && [ "$a7s_badn" -eq 0 ]; } 2>/dev/null \ + && ok "#11 A7 sweep (forbudget-soft): $a7s_runs budgets over a git-less corpus (the first and last budget of every row count: default 760..1500, --detail=20 --with-graph 300..2500, one-row named task 400/600) — every answer exit 0, unlabelled, its rows fit T or the floor and a cut is maximal, rows monotone in N, every cut carries next=" \ + || no "#11 A7 sweep (forbudget-soft): ${a7s_badn:-?} of ${a7s_runs:-?} budgets break the soft target, or the sweep did not judge its 66 rungs (first: $a7s_bad)" +# control (REPLACED, forbudget-soft (rule 4); was ">= 1 budget labelled over_ceiling INSIDE the allowance", a band the soft +# target deleted): the sweep must actually exercise both halves of the contract — rows CUT at the target (else ST and NX ran +# on complete answers only) and an answer whose est_tokens exceeds its N (the soft overshoot, which NOLBL then pins unlabelled). +{ [ "$a7s_cut" -gt 0 ] && [ "$a7s_over" -gt 0 ]; } \ + && ok "#11 A7 sweep control: $a7s_cut budget(s) cut the rows at the target and $a7s_over priced past their N — both halves exercised" \ + || no "#11 A7 sweep control: cut=$a7s_cut over-N=$a7s_over — the sweep no longer reaches a cut or a soft overshoot, re-anchor its ranges" +# A7 FLOOR / NEXT @900 — REPLACED (forbudget-soft (rule 4)). Was the UNPAID TWIN (knob-honesty-068): at the rank 1..4 +# floor the next= rode unpaid, the root said over_ceiling="1" and the legend carried the unpaid clause. The paid/unpaid +# continuation is gone — one render, nothing paid from rows. The same call ("map row serializer" at 900 on this corpus) now +# pins: exit 0; NOLBL and no unpaid clause; ST against the task's no-budget answer (d099ec21: 10 rows, base d4325a8c: the +# 4-row floor, not maximal); and NX — the cut carries next=, and that next= RUNS (from the same root) and serves the +# whole head: every one of total= rows, the cut's rows its rank prefix. ( cd "$A7S" && rwcall corpus --for="map row serializer" --token-budget=900 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? -fl_a="$( awk 'BEGIN{printf "%d", 900*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 @900: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" +( cd "$A7S" && rwcall corpus --for="map row serializer" --no-cache ) >"$A7S/floor_ref.xml" 2>/dev/null +fl_root="$( ctx_root "$A7S/floor.xml" )"; fl_sigs="$( grep -aoE ']*>' "$A7S/floor.xml" | head -1 )" +fl_tot="$( printf '%s' "$fl_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" +fl_next="$( printf '%s' "$fl_sigs" | sed -nE 's/.* next="(--for=[^"]*)".*/\1/p' | sed "s/'/'/g; s/"/\"/g; s/&/\\&/g" )" +fl_why="" +{ [ "$fl_rc" -eq 0 ] && [ -n "$fl_root" ]; } || fl_why="$fl_why exit-$fl_rc-or-no-root" +[ "${fl_root#* over_ceiling=}" = "$fl_root" ] || fl_why="$fl_why over_ceiling-on-a-soft-target" +! grep -aqF 'next= rides unpaid' "$A7S/floor.xml" || fl_why="$fl_why unpaid-clause" +fl_st="$( soft_st "$A7S/floor.xml" 900 "$A7S/floor_ref.xml" )" || fl_why="$fl_why ST:[$fl_st]" +if [ -z "$fl_next" ] || [ -z "$fl_tot" ]; then + fl_why="$fl_why -cut-without-next=-or-total=" else - no "#11 A7 unpaid twin @900 (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" + eval "set -- $fl_next" + ( cd "$A7S" && rwcall corpus "$@" --no-cache ) >"$A7S/floor_next.xml" 2>/dev/null + soft_rows "$A7S/floor.xml" >"$A7S/floor.rows" 2>/dev/null; soft_rows "$A7S/floor_next.xml" >"$A7S/floor_next.rows" 2>/dev/null + { [ "$( wc -l < "$A7S/floor_next.rows" | tr -d ' ' )" = "$fl_tot" ] && rows_prefix "$A7S/floor.rows" "$A7S/floor_next.rows"; } \ + || fl_why="$fl_why next=-does-not-serve-the-head($( wc -l < "$A7S/floor_next.rows" | tr -d ' ' )-of-$fl_tot)" 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). -# TRAIN 27 RE-PIN (2026-10-11) of C3 and its near miss (the floor twin above is unchanged). Under a spent ceiling the compact -# header no longer carries clauses for sections served as stubs nor a false task_echo / "(total= shown= capped=)" note, and the -# compact dialect pays a measured overrun from the rows (verbs_for.h runForLens), so on this corpus the paid 4-row answer FITS -# from 910 and no budget 700..1500 serves the C3 shape on any task tried (C3 then correctly serves the paid answer: 925 = 4 rows, -# est_tokens=907, no label). The regime lives on a twin of the same corpus WITHOUT the doc comments (smaller rows, so the cut -# alone leaves 5 rows while the 4-row floor is still over): C3 at 740..750 (494f97b6: 740..770, 5 rows, last rung + unpaid -# clause), the payable near miss from 760 (494f97b6: from 780). Pinned at 745 / 790, inside both binaries' windows. RED on a -# mutant whose C3 fallback pays from the rows (4 rows, over). Assertions unchanged. +[ -z "$fl_why" ] \ + && ok "#11 A7 floor/next @900 (forbudget-soft): unlabelled, no unpaid clause, $fl_st; its next= serves all $fl_tot rows, the cut's its prefix" \ + || no "#11 A7 floor/next @900 (forbudget-soft) —$fl_why" +# C3 @745 / NEAR MISS @790 — REPLACED (forbudget-soft (rule 4)). Was orchestrator ruling C3 (never drop rows to pay for next= +# on an answer over its ceiling either way: 745 keeps >= 5 rows, more than the paid 790 answer, labelled) and its payable near +# miss (790: the handle paid from rows, inside the allowance, unlabelled). Under a soft target next= is never paid from rows +# at all, which is what made the 745 answer serve MORE rows than the 790 one; that inversion is now impossible by contract. +# The same doc-less corpus and the same two budgets pin RP: rows(745) is a rank prefix of rows(790), both a rank prefix of +# the task's no-budget answer; both unlabelled with no unpaid clause; both ST-maximal (the handle bought with no row). +# MEASURED: d099ec21 serves 11 / 12 rows; base d4325a8c 5 / 4 (745 labelled) — not monotone. mkdir -p "$A7S/c3/corpus" python3 - "$A7S/c3/corpus" <<'PYG' import os, sys @@ -950,40 +1059,36 @@ for i in range( 4 ): PYG ( cd "$A7S/c3" && rwcall corpus --for="map row serializer" --token-budget=745 --no-cache ) >"$A7S/c3.xml" 2>/dev/null; c3_rc=$? ( cd "$A7S/c3" && rwcall corpus --for="map row serializer" --token-budget=790 --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 @745 (doc-less twin): over its ceiling either way, the answer keeps the cut's own $c3_sh rows (>= 5, > the paid 790 answer's $pd_sh) — next= unpaid, over_ceiling=\"1\" + the unpaid clause" -else - no "#11 A7 C3 @745 (doc-less twin, exit $c3_rc): shown=${c3_sh:-?} (want >= 5 and > the paid 790 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 (790 since the train-27 re-pin above) — 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", 790*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 'next= rides unpaid' "$A7S/paid.xml"; then - ok "#11 A7 unpaid twin near miss @790 (doc-less twin): the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no unpaid clause" -else - no "#11 A7 unpaid twin near miss @790 (doc-less twin, exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" -fi +( cd "$A7S/c3" && rwcall corpus --for="map row serializer" --no-cache ) >"$A7S/c3_ref.xml" 2>/dev/null +soft_rows "$A7S/c3.xml" >"$A7S/c3.rows" 2>/dev/null; soft_rows "$A7S/paid.xml" >"$A7S/paid.rows" 2>/dev/null; soft_rows "$A7S/c3_ref.xml" >"$A7S/c3_ref.rows" 2>/dev/null +c3_n="$( wc -l < "$A7S/c3.rows" | tr -d ' ' )"; pd_n="$( wc -l < "$A7S/paid.rows" | tr -d ' ' )" +c3_why="" +for c3f in c3:745:$c3_rc paid:790:$pd_rc; do + c3_lbl="${c3f%%:*}"; c3_rest="${c3f#*:}"; c3_bud="${c3_rest%%:*}"; c3_frc="${c3_rest#*:}" + c3_root="$( ctx_root "$A7S/$c3_lbl.xml" )" + { [ "$c3_frc" -eq 0 ] && [ -n "$c3_root" ]; } || c3_why="$c3_why @$c3_bud:exit-$c3_frc-or-no-root" + [ "${c3_root#* over_ceiling=}" = "$c3_root" ] || c3_why="$c3_why @$c3_bud:over_ceiling" + ! grep -aqF 'next= rides unpaid' "$A7S/$c3_lbl.xml" || c3_why="$c3_why @$c3_bud:unpaid-clause" + c3_st="$( soft_st "$A7S/$c3_lbl.xml" "$c3_bud" "$A7S/c3_ref.xml" )" || c3_why="$c3_why @$c3_bud:ST:[$c3_st]" +done +rows_prefix "$A7S/c3.rows" "$A7S/paid.rows" || c3_why="$c3_why rows(745)=$c3_n-not-a-prefix-of-rows(790)=$pd_n" +rows_prefix "$A7S/paid.rows" "$A7S/c3_ref.rows" || c3_why="$c3_why rows(790)-not-a-prefix-of-the-no-budget-answer" +[ -z "$c3_why" ] \ + && ok "#11 A7 C3/near miss (forbudget-soft, doc-less twin): rows(745)=$c3_n is a rank prefix of rows(790)=$pd_n, itself a prefix of the no-budget answer; both unlabelled, no unpaid clause, both ST-maximal" \ + || no "#11 A7 C3/near miss (forbudget-soft, doc-less twin) —$c3_why" } est_11_a7r(){ -# ── #11 A7R (lego-charge fix round 4): THE TRAILING-SECTION RESERVE. Under --token-budget, --with-graph's block and the -# first --detail body were charged only AFTER had claimed the whole sig-side ceiling, so both rode past the -# budget, and the ladder's last rung then blamed the HEADER floor ("no payload left to trim") while was the -# trimmable payload. MEASURED on the parent binary over this arm's corpus: --detail=20 --with-graph at 2000 tokens = -# 6054 B vs a 5428 B allowance; --detail=3 served its first body as a one-line over_ceiling stub at every budget. -# runForLens now reserves the graph bytes and a MEASURED first-body floor out of the sig claim -# (forSigClaimWithTrailingReserve / forDetailFirstBodyFloor). The corpus is git-less, run from a relative path, every -# body is longer than a quarter of the smallest budget's bundle (so the floor's head-cut + next= path runs), and its -# 40 signature rows overflow the default sig share (so is capped="1" in every regime below — the negatives -# compare a CUT section, not a whole one). +# ── #11 A7R (lego-charge fix round 4): THE TRAILING-SECTION RESERVE — and its forbudget-soft (rule 4) REPLACEMENT. Under the +# old hard ceiling --with-graph's block and the first --detail body were charged only AFTER had claimed the whole +# sig-side ceiling, so lego-charge reserved the graph bytes and a measured first-body floor OUT of the sig claim +# (forSigClaimWithTrailingReserve / forDetailFirstBodyFloor) and these arms pinned "inside N x 2.36 x 1.15, unlabelled". The +# soft target deletes both the byte promise and the reserve: T is charged against the rows only, and a trailing +# section takes what the rows left of T. So on this corpus the arms now pin SEC — a trailing section NEVER removes a +# row: with --with-graph / --detail the rows are the bare run's rows at the same N — plus ST, NOLBL, and the first body's +# floor (never the one-line stub; a head-cut carries the next= that recovers it). The corpus is git-less, run from a +# relative path, every body is longer than a quarter of the smallest budget's bundle (so the floor's head-cut + next= path +# runs), and its 72 rows overflow the default byte target (so is capped="1", rows cut or shrunk, in every regime below). A7R="$TMP/a7reserve" mkdir -p "$A7R/corpus" python3 - "$A7R/corpus" <<'PYG' @@ -1003,26 +1108,34 @@ PYG A7R_Q="serialize the map row" a7r_out(){ ( cd "$A7R" && rwcall corpus --for="$A7R_Q" "$@" --no-cache ) 2>/dev/null; } a7r_sigs(){ python3 -c 'import re,sys; m=re.search(r"",open(sys.argv[1]).read(),re.S); print(m.group(0) if m else "")' "$1"; } -# A7R-1 (positive): every budget, three shapes — exit 0, inside the allowance, NO over_ceiling on the root (est_tokens fits -# budget_tokens), no ladder last rung, present, and with --detail a first that is not the one-line over_ceiling -# stub; a first body the floor cut carries the next= that serves the rest (rule 5: every cut recoverable). +a7r_out >"$A7R/n0.xml" # the no-budget answer: the ST reference below, and the A7R no-ceiling / A7D default arms +# A7R-1 — REPLACED (forbudget-soft (rule 4)). Was: --with-graph / --detail=3 at 1500..3000 — exit 0, inside N x 2.36 x 1.15, no +# over_ceiling, no last rung, first body served (the reserve made room for it). Now: exit 0; NOLBL and no last-rung note; SEC +# (rows identical to the bare run at the same N — d099ec21: 12/16/21/26 rows each; base d4325a8c: --with-graph 6/11/16/20 and +# --detail=3 4/6/10/15 against the bare 8/13/18/22, the reserve at work); ST against the no-budget answer; and with --detail a +# first that is not the one-line over_ceiling stub, a head-cut one carrying next= (rule 5: every cut recoverable). a7r_bad=""; a7r_runs=0 -for spec in "graph:--with-graph" "detail:--detail=3"; do - r_label="${spec%%:*}"; r_args="${spec#*:}" - for N in 1500 2000 2500 3000; do +for N in 1500 2000 2500 3000; do + a7r_out --token-budget=$N >"$A7R/o0.xml" + soft_rows "$A7R/o0.xml" >"$A7R/o0.rows" 2>/dev/null + for spec in "graph:--with-graph" "detail:--detail=3"; do + r_label="${spec%%:*}"; r_args="${spec#*:}" # shellcheck disable=SC2086 a7r_out --token-budget=$N $r_args >"$A7R/o.xml" r_rc=$? a7r_runs=$(( a7r_runs + 1 )) - r_b="$( bytes_of "$A7R/o.xml" )" - r_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - r_root="$( grep -aoE '^]*>' "$A7R/o.xml" | head -1 )" + r_root="$( ctx_root "$A7R/o.xml" )" + soft_rows "$A7R/o.xml" >"$A7R/o.rows" 2>/dev/null r_why="" [ "$r_rc" -eq 0 ] || r_why="exit $r_rc" [ -n "$r_root" ] && grep -aq '/dev/null || r_why="$r_why ${r_b:-?}B>${r_a}B" - [ "${r_root#* over_ceiling=\"1\"}" = "$r_root" ] || r_why="$r_why over_ceiling" - ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" # either reason (fix round 5) + # round C (rule 4): SEC is the by-design break (a trailing section is reserved before the rows past the cut): RP instead + soft_rows "$A7R/n0.xml" >"$A7R/n0.rows" 2>/dev/null + [ -s "$A7R/o.rows" ] && rows_prefix "$A7R/o.rows" "$A7R/n0.rows" \ + || r_why="$r_why rows:$( wc -l < "$A7R/o.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" + r_st="$( soft_st "$A7R/o.xml" "$N" "$A7R/n0.xml" )" || r_why="$r_why ST:[$r_st]" + [ "${r_root#* over_ceiling=}" = "$r_root" ] || r_why="$r_why over_ceiling" + ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" if [ "${r_args#*--detail}" != "$r_args" ]; then r_b1="$( grep -aoE ']*>' "$A7R/o.xml" | head -1 )" [ -n "$r_b1" ] || r_why="$r_why no-first-body" @@ -1033,48 +1146,44 @@ for spec in "graph:--with-graph" "detail:--detail=3"; do done done [ "$a7r_runs" -eq 8 ] && [ -z "$a7r_bad" ] \ - && ok "#11 A7R reserve: 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) — inside N x 2.36 x 1.15, no over_ceiling, no last rung, first body served (cut ones carry next=)" \ - || no "#11 A7R reserve: spent the bytes of the graph block / first --detail body —$a7r_bad" -# A7R-K (the PRE-REGISTERED KILL, reports/lego-charge.md fix round 4): with BOTH --detail and --with-graph the reserve is -# off — reserving both cut a gold-relevant owner row out of on the measured question set. So that shape's ranked -# rows keep their claim: never FEWER rows than the same budget's --with-graph run (which reserves the graph alone), -# a delivery past the allowance is disclosed (root over_ceiling="1"), and the first body still gets its floor — not the -# one-line over_ceiling stub, and a head-cut carries next=. + && ok "#11 A7R (forbudget-soft, round C): 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) serve a rank prefix of the no-budget rows, ST against T less the sections, unlabelled, first body served (cut ones carry next=)" \ + || no "#11 A7R no reserve (forbudget-soft): a trailing section changed the rows or the soft target broke —$a7r_bad" +# A7R-K — REPLACED in part (forbudget-soft (rule 4)). Was the pre-registered kill (reports/lego-charge.md fix round 4): with BOTH +# --detail and --with-graph the reserve was off, so the rows were never FEWER than the --with-graph run's; a delivery past +# the allowance was labelled over_ceiling="1"; the first body got its floor. The rows clause tightens to SEC (both flags serve +# exactly the bare rows); the allowance-or-label clause becomes ST + NOLBL; the first-body floor clause is unchanged. a7k_bad="" for N in 2000 2500 3000; do a7r_out --token-budget=$N --detail=20 --with-graph >"$A7R/k.xml"; k_rc=$? - a7r_out --token-budget=$N --with-graph >"$A7R/kg.xml" - k_rows="$( grep -aoE ']*>' "$A7R/k.xml" | wc -l | tr -d ' ' )"; g_rows="$( grep -aoE ']*>' "$A7R/kg.xml" | wc -l | tr -d ' ' )" - k_b="$( bytes_of "$A7R/k.xml" )"; k_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - k_root="$( grep -aoE '^]*>' "$A7R/k.xml" | head -1 )" + a7r_out --token-budget=$N >"$A7R/kb.xml" + soft_rows "$A7R/k.xml" >"$A7R/k.rows" 2>/dev/null; soft_rows "$A7R/kb.xml" >"$A7R/kb.rows" 2>/dev/null + k_root="$( ctx_root "$A7R/k.xml" )" k_b1="$( grep -aoE ']*>' "$A7R/k.xml" | head -1 )" k_why="" [ "$k_rc" -eq 0 ] && [ -n "$k_root" ] || k_why="exit $k_rc / no root" - [ "${k_rows:-0}" -ge 1 ] && [ "${k_rows:-0}" -ge "${g_rows:-0}" ] 2>/dev/null || k_why="$k_why rows ${k_rows:-?}/dev/null || [ "${k_root#* over_ceiling=\"1\"}" != "$k_root" ] || k_why="$k_why ${k_b:-?}B>${k_a}B-unlabelled" + soft_rows "$A7R/n0.xml" >"$A7R/n0.rows" 2>/dev/null # round C (rule 4): RP instead of SEC, as A7R-1 + [ -s "$A7R/k.rows" ] && rows_prefix "$A7R/k.rows" "$A7R/n0.rows" || k_why="$k_why rows $( wc -l < "$A7R/k.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" + k_st="$( soft_st "$A7R/k.xml" "$N" "$A7R/n0.xml" )" || k_why="$k_why ST:[$k_st]" + [ "${k_root#* over_ceiling=}" = "$k_root" ] || k_why="$k_why over_ceiling" [ -n "$k_b1" ] && [ "${k_b1#* over_ceiling=}" = "$k_b1" ] || k_why="$k_why first-body-stub" { [ "${k_b1#* truncated=\"1\"}" = "$k_b1" ] || [ "${k_b1#* next=\"--expand=}" != "$k_b1" ]; } || k_why="$k_why cut-without-next" [ -z "$k_why" ] || a7k_bad="$a7k_bad @$N:[${k_why# }]" done [ -z "$a7k_bad" ] \ - && ok "#11 A7R kill: --detail=20 --with-graph at 2000..3000 keeps at least the --with-graph run's rows, labels any overshoot over_ceiling, serves the first body at its floor" \ - || no "#11 A7R kill: the both-sections shape —$a7k_bad" -# A7R-K2 (fix round 5, owner ruling option B — the A7B contract on a corpus whose first body is LONGER than the floor): -# at 2000 and 2500 tokens --detail=20 --with-graph delivers past the allowance with over_ceiling="1", the last rung names -# the kept rows (never the header floor: the header alone fits), and the head-cut first body's next= RECOVERS EXACTLY the -# lines it cut — the cut CDATA plus the next= answer's CDATA is the function's source, line for line (rule 5). + && ok "#11 A7R both sections (forbudget-soft, round C): --detail=20 --with-graph at 2000..3000 serves a rank prefix of the no-budget rows, ST, unlabelled, first body at its floor" \ + || no "#11 A7R both sections (forbudget-soft): —$a7k_bad" +# A7R-K2 — REPLACED in part (forbudget-soft (rule 4)). Was the A7B contract on this corpus: past the allowance with +# over_ceiling="1" and the kept-rows reason (both gone with the ceiling), well-formed despite the note, and the head-cut first +# body's next= RECOVERING EXACTLY the lines it cut. The recovery half is the soft target's NX invariant and is kept unchanged: +# at 2000 and 2500 tokens --detail=20 --with-graph serves its first body head-cut (T minus the rows funds it), and the cut +# CDATA plus the next= answer's CDATA is the function's source, line for line (rule 5). Plus NOLBL and xmllint. a7k2_bad="" for N in 2000 2500; do a7r_out --token-budget=$N --detail=20 --with-graph >"$A7R/k2.xml"; k2_rc=$? - k2_b="$( bytes_of "$A7R/k2.xml" )"; k2_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - k2_root="$( grep -aoE '^]*>' "$A7R/k2.xml" | head -1 )" - k2_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b"/dev/null \ - || k2_why="$k2_why premise/label:${k2_b:-?}B-vs-${k2_a}B" - { grep -aqF "$KEPT_NOTE" "$A7R/k2.xml" && ! grep -aqF "$FLOOR_NOTE" "$A7R/k2.xml" && [ "${k2_hdr:--1}" -gt 0 ] && [ "$k2_hdr" -le "$k2_a" ]; } 2>/dev/null \ - || k2_why="$k2_why reason" - # fix round 6 (checklist 25): the kept-rows note rides in the header XML comment here too — the answer must parse + { [ "$k2_rc" -eq 0 ] && [ -n "$k2_root" ] && [ "${k2_root#* over_ceiling=}" = "$k2_root" ]; } || k2_why="$k2_why exit-$k2_rc/no-root/over_ceiling" + ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/k2.xml" || k2_why="$k2_why last-rung-note" if command -v xmllint >/dev/null 2>&1; then xmllint --noout "$A7R/k2.xml" 2>/dev/null || k2_why="$k2_why xmllint-rejected" fi @@ -1102,8 +1211,8 @@ PYR [ -z "$k2_why" ] || a7k2_bad="$a7k2_bad @$N:[${k2_why# }]" done [ -z "$a7k2_bad" ] \ - && ok "#11 A7R kill+next (by-design, owner option B): --detail=20 --with-graph at 2000/2500 is over_ceiling with the kept-rows reason, and the cut first body's next= returns exactly the lines it cut" \ - || no "#11 A7R kill+next (by-design, owner option B): —$a7k2_bad" + && ok "#11 A7R cut body next= (forbudget-soft): --detail=20 --with-graph at 2000/2500 is unlabelled and well-formed, and the cut first body's next= returns exactly the lines it cut" \ + || no "#11 A7R cut body next= (forbudget-soft): —$a7k2_bad" # A7R-2 (the floor's cut is the one the floor priced): at 1500 tokens the first body is longer than the floor, so it ships # head-cut — truncated="1" with lines= and next= — never dropped and never the bare one-line stub. a7r_out --token-budget=1500 --detail=3 >"$A7R/f.xml" @@ -1115,7 +1224,7 @@ r_hi="$( printf '%s' "$r_b1" | sed -nE 's/.* lines="1-([0-9]+)\/[0-9]+".*/\1/p' || no "#11 A7R floor: the first --detail body at 1500 tokens is not the floor's head-cut of >= 2 lines (got '${r_b1:-none}')" # A7R-N1 (near-miss negative, NO explicit ceiling): the reserve must not exist — is byte-identical with and # without --with-graph (the default regime's graph rides on top, as before). -a7r_out >"$A7R/n0.xml"; a7r_out --with-graph >"$A7R/n1.xml" +a7r_out --with-graph >"$A7R/n1.xml" n0="$( a7r_sigs "$A7R/n0.xml" )"; n1="$( a7r_sigs "$A7R/n1.xml" )" { [ -n "$n0" ] && [ "$n0" = "$n1" ] && [ "${n0#*capped=\"1\"}" != "$n0" ]; } \ && ok "#11 A7R no ceiling: (capped=1) byte-identical with and without --with-graph — nothing reserved without --token-budget" \ @@ -1132,43 +1241,59 @@ w0="$( a7r_sigs "$A7R/w0.xml" )"; w1="$( a7r_sigs "$A7R/w1.xml" )" est_11_a7d(){ # ── #11 A7D (fix round 4, review rv-wrap-recipe-k29 F1; rule 5 — never a silent cut): under --token-budget the compact -# section is no longer DROPPED with no element when the ceiling is spent. The arm corpus is on the conceptual route. -# A7D-1: at 2000 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with -# no budget — its legend clause rides the header, the answer stays inside its budget (the disclosure's bytes are -# reserved), and the next= RECOVERS the rows: that call's serves at least one row of the same total. -a7r_out --token-budget=2000 >"$A7R/h.xml" +# section is never DROPPED with no element when the target is spent. The arm corpus is on the conceptual route. +# forbudget-soft (rule 4): a trailing section now takes T minus the rows already served, and the DEFAULT compact serves +# all its rows (decision D-3: no 1,000 B cap). So the spent-target marker moved from 2000 to 1500 tokens on this corpus (the +# rows now fill more of T: d099ec21 serves 3 of 6 hop rows at 2000, 0 at 1500), the partial cut moved from 5000 to 2000 (5000 +# is wide enough for the whole set), and the no-budget negative inverts: the default is WHOLE (base d4325a8c: 5 of 6, +# capped). Re-anchored with the invariant unchanged where it still holds: every cut is counted and carries next=. +# A7D-1: at 1500 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with no +# budget — its legend clause rides the header, no over_ceiling=, and the next= RECOVERS the rows: that call (the default +# answer) serves every one of the same total= hop rows. +# round C (reserve first; rule 4): the hops are funded from the rows at the cut, so 1500 now serves the whole set — re-anchored +# to 600 (the marker) and 800 (the partial cut), measured on the round C build: 600 shown=0, 800 shown=3 of 6, 900+ whole +a7r_out --token-budget=600 >"$A7R/h.xml" h_tag="$( grep -aoE ']*>' "$A7R/h.xml" | head -1 )" -h_root="$( grep -aoE '^]*>' "$A7R/h.xml" | head -1 )" +h_root="$( ctx_root "$A7R/h.xml" )" h_tot="$( printf '%s' "$h_tag" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" -a7r_out >"$A7R/hd.xml" -hd_tag="$( grep -aoE ']*>' "$A7R/hd.xml" | head -1 )" +hd_tag="$( grep -aoE ']*>' "$A7R/n0.xml" | head -1 )" hd_shown="$( printf '%s' "$hd_tag" | sed -nE 's/" ] && [ "${h_tot:-0}" -ge 1 ] 2>/dev/null \ && grep -aqF '; hops: this budget left out the call-hop rows (one hop of callees for each of total= top rows)' "$A7R/h.xml" && [ "${h_root#* over_ceiling=}" = "$h_root" ] \ - && [ "${hd_shown:-0}" -ge 1 ] 2>/dev/null && [ "$hd_tot" = "$h_tot" ]; } \ - && ok "#11 A7D hops: the spent-ceiling is the counted marker ($h_tag), defined, inside budget; its next= serves $hd_shown of $hd_tot rows" \ - || no "#11 A7D hops: spent-ceiling not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" + && [ -n "$h_root" ] && [ -n "$hd_shown" ] && [ "$hd_shown" = "$h_tot" ] && [ "$hd_tot" = "$h_tot" ]; } \ + && ok "#11 A7D hops: the spent-target at 600 is the counted marker ($h_tag), defined, unlabelled; its next= serves all $hd_shown of $hd_tot rows" \ + || no "#11 A7D hops: spent-target at 600 not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" # A7D-1 (train 27): the compact dialect above defines the marker in its own short clause — it already reads total= shown= # capped=1 and next= in its un-droppable clauses (verbs_for.h kForHopsBudgetCutLegendCompact). The FULL dialect keeps the -# long clause that defines both: the same marker, the same next=, inside the same budget, with shown=0 capped=1 and next= read. -a7r_out --token-budget=2000 --legend=full >"$A7R/hf.xml" -hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( grep -aoE '^]*>' "$A7R/hf.xml" | head -1 )" -{ [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] \ +# long clause that defines both: the same marker, the same next=, with shown=0 capped=1 and next= read. +a7r_out --token-budget=600 --legend=full >"$A7R/hf.xml" +hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( ctx_root "$A7R/hf.xml" )" +{ [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] && [ -n "$hf_root" ] \ && grep -aqF '; hops shown=0 capped=1: this budget left out the call-hop rows (one hop of callees for each of total= top rows); next= serves them' "$A7R/hf.xml" \ && [ "${hf_root#* over_ceiling=}" = "$hf_root" ] && ! grep -aqF '; hops: this budget' "$A7R/hf.xml"; } \ - && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), inside budget" \ - || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause or the budget fit is missing" -# A7D-2: a PARTIAL hops cut under the budget carries the same next= (5000 tokens: some rows fit, not all). -a7r_out --token-budget=5000 >"$A7R/hp.xml" + && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), unlabelled" \ + || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause is missing or the root is labelled" +# A7D-2: a PARTIAL hops cut under the target carries the same next= (800 tokens since round C: some rows fit, not all). +a7r_out --token-budget=800 >"$A7R/hp.xml" hp_tag="$( grep -aoE ']*>' "$A7R/hp.xml" | head -1 )" -{ [ "${hp_tag#*shown=\"0\"}" = "$hp_tag" ] && [ "${hp_tag#* capped=\"1\"}" != "$hp_tag" ] && [ "${hp_tag#* next=\"--for='$A7R_Q'\"}" != "$hp_tag" ]; } \ - && ok "#11 A7D partial hops cut at 5000 tokens carries next= ($hp_tag)" \ - || no "#11 A7D partial hops cut at 5000 tokens: no next= on a cut with rows (got '${hp_tag:-none}')" -# A7D-N (near-miss negative): WITHOUT --token-budget the default regime is unchanged — its cut (capped="1") gets no -# next= from this change (a named deferral: adding it there moves every default --for answer). -{ [ "${hd_tag#* capped=\"1\"}" != "$hd_tag" ] && [ "${hd_tag#* next=}" = "$hd_tag" ]; } \ - && ok "#11 A7D no budget: the default regime's cut is byte-unchanged (capped=1, no next= added): $hd_tag" \ - || no "#11 A7D no budget: the default changed or is not cut (got '${hd_tag:-none}') — the disclosure leaked into the default regime" +hp_sh="$( printf '%s' "$hp_tag" | sed -nE 's//dev/null \ + && ok "#11 A7D partial hops cut at 800 tokens carries next= ($hp_tag)" \ + || no "#11 A7D partial hops cut at 800 tokens: not a partial cut with next= (got '${hp_tag:-none}')" +# A7D-W (forbudget-soft, replaces the old 5000-token partial cut): a target wide enough for the rows AND the hop rows serves the +# whole hop set, like the default answer — stop when complete, nothing left to continue (no capped=, so no next= owed). +a7r_out --token-budget=5000 >"$A7R/hw.xml" +hw_tag="$( grep -aoE ']*>' "$A7R/hw.xml" | head -1 )" +{ [ -n "$hw_tag" ] && [ -n "$hd_tot" ] && [ "$hw_tag" = "" ]; } \ + && ok "#11 A7D wide target (forbudget-soft): at 5000 tokens serves the whole set ($hw_tag)" \ + || no "#11 A7D wide target (forbudget-soft): at 5000 tokens is not the whole set of $hd_tot (got '${hw_tag:-none}')" +# A7D-N — REPLACED (forbudget-soft (rule 4), decision D-3). Was: WITHOUT --token-budget the default regime's cut +# (capped="1") gets no next= (a named deferral). The default compact route now serves ALL its hop rows (no 1,000 B cap), so +# the no-budget is whole: shown == total, capped="0" — nothing cut, nothing to continue. +{ [ -n "$hd_tag" ] && [ -n "$hd_tot" ] && [ "$hd_tag" = "" ] && [ "$hd_tot" -ge 1 ]; } 2>/dev/null \ + && ok "#11 A7D no budget (forbudget-soft): the default serves its whole set ($hd_tag)" \ + || no "#11 A7D no budget (forbudget-soft): the default is not whole (got '${hd_tag:-none}') — the compact route still caps its hop rows" # 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 @@ -1812,11 +1937,17 @@ jsonEnvelopeCase "--token-budget=600" --token-budget=600 jsonEnvelopeCase "--token-budget=3000" --token-budget=3000 jsonEnvelopeCase "--token-budget=16000" --token-budget=16000 -# the over_ceiling key must actually FIRE somewhere in that sweep, or the §C2 arm above is vacuous. rw src --for="$JSON_TASK" --json --token-budget=200 > "$TMP/fj200.out" 2>/dev/null || true -grep -q '"over_ceiling":true' "$TMP/fj200.out" \ - && ok "§C2 control: over_ceiling DOES fire at a small budget (the charge arm above is not vacuous)" \ - || no "§C2 control: no budget in the sweep produced over_ceiling - the §C2 arms proved nothing" +# §C2 control — REPLACED (forbudget-soft (rule 4)). Was: the over_ceiling key must FIRE at --token-budget=200, or the §C2 arm +# above is vacuous. A --for --json budget is a soft target: est_tokens may exceed it and the key is never emitted for it (it +# survives only at the default regime's runaway guard). The control now pins both halves of that at the same call: the +# overshoot is REAL (est_tokens > 200 — the §C1 identity above already proved that number prices the whole document) and the +# document carries NO over_ceiling key (NOLBL). Base d4325a8c: "over_ceiling":true here. +# ($TMP/fj200.out is the --token-budget=200 capture made just above this arm.) +c2_est="$( python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("est_tokens","")); sys.exit(0 if "over_ceiling" not in d else 1)' "$TMP/fj200.out" 2>/dev/null )"; c2_rc=$? +{ [ "$c2_rc" -eq 0 ] && [ -n "$c2_est" ] && [ "$c2_est" -gt 200 ]; } 2>/dev/null \ + && ok "§C2 control (forbudget-soft): --for --json --token-budget=200 prices past its target (est_tokens=$c2_est) with no over_ceiling key — a soft target is never labelled" \ + || no "§C2 control (forbudget-soft): --for --json --token-budget=200: est_tokens='${c2_est:-unreadable}', over_ceiling key $( [ "$c2_rc" -eq 1 ] && echo present || echo 'absent/unparseable' ) — want est_tokens > 200 and no over_ceiling key" } est_c4(){ @@ -1902,14 +2033,26 @@ band15 "--pack-task --token-budget=1500" "$TMP/p15_pt.xml" ctx 420 [ "$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --pack-task: root budget_tokens=\"1500\" — the ceiling in the SAME unit as est_tokens=" \ || no "#15 --pack-task: root carries no budget_tokens=\"1500\" beside est_tokens= (got '$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )')" +# forbudget-soft (rule 4): an explicit --pack-task budget is a SOFT target (decision 3) — the section cascade still shapes +# toward it, but nothing is labelled over it and the header is never dropped for it. Was: root over_ceiling="1" at 50 (the +# header floor exceeds the ceiling). Now NOLBL + HL at the same call: no over_ceiling= on the root, task= verbatim and route= +# kept, no "(ceiling)" / task-echo drop; the full dialect reads budget_tokens= as the soft target (packtask.h +# kPackTaskSoftBudgetSentence). The overshoot stays real and priced: est_tokens > 50. rw src --pack-task="$FOR_TASK" --token-budget=50 >"$TMP/p15_pt50.xml" 2>/dev/null -[ "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" = "1" ] \ - && ok "#15 --pack-task --token-budget=50: root over_ceiling=\"1\" (the header floor exceeds the ceiling)" \ - || no "#15 --pack-task --token-budget=50: no over_ceiling=\"1\" on the root while the ledger prose says the floor exceeds the budget" +rw src --pack-task="$FOR_TASK" --token-budget=50 --legend=full >"$TMP/p15_pt50f.xml" 2>/dev/null +pt50_why="" +[ -n "$( perl -0pe 's###gs' "$TMP/p15_pt50.xml" | grep -oE ']*)?>' | head -1 )" ] || pt50_why="$pt50_why no-root" +[ -z "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" ] || pt50_why="$pt50_why over_ceiling-on-a-soft-target" +[ "$( root_attr "$TMP/p15_pt50.xml" ctx task )" = "$FOR_TASK" ] && [ -n "$( root_attr "$TMP/p15_pt50.xml" ctx route )" ] || pt50_why="$pt50_why task=/route=-dropped" +! grep -aqE '\(ceiling\)|task_echo: dropped' "$TMP/p15_pt50.xml" || pt50_why="$pt50_why header-dropped-for-the-budget" +grep -aqF 'budget_tokens= is the soft token target asked' "$TMP/p15_pt50f.xml" || pt50_why="$pt50_why full-legend-not-soft" +[ -z "$pt50_why" ] \ + && ok "#15 --pack-task --token-budget=50 (forbudget-soft): no over_ceiling=, task=/route= kept, nothing dropped, the full legend reads a soft target" \ + || no "#15 --pack-task --token-budget=50 (forbudget-soft) —$pt50_why" E50="$( root_est "$TMP/p15_pt50.xml" ctx )" [ -n "$E50" ] && [ "$E50" -gt 50 ] 2>/dev/null \ - && ok "#15 --pack-task --token-budget=50: est_tokens=$E50 > 50 — the label and the number agree" \ - || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the budget it is labelled over" + && ok "#15 --pack-task --token-budget=50: est_tokens=$E50 > 50 — the soft overshoot is priced on the root" \ + || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the 50-token target (the floor cannot fit 50 tokens)" # (b) --from-trace: the same two shapes on a name-resolved frame printf 'Traceback (most recent call last):\n File "src/graph.h", line 1, in rankGraphTeleport\n' >"$TMP/p15_trace.txt" rw src --from-trace="$TMP/p15_trace.txt" --token-budget=1500 >"$TMP/p15_ft.xml" 2>/dev/null @@ -1917,65 +2060,73 @@ band15 "--from-trace --token-budget=1500" "$TMP/p15_ft.xml" ctx 420 [ "$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --from-trace: root budget_tokens=\"1500\"" \ || no "#15 --from-trace: root carries no budget_tokens=\"1500\" (got '$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )')" +# forbudget-soft (rule 4): an explicit --from-trace budget is a SOFT target (decision 4): no ladder, the trace echo always ships, +# over_ceiling= only against max_tokens=. Was: root over_ceiling="1" at 50. Now at the same call: est_tokens > 50 (the overshoot +# is priced), no over_ceiling= (no max_tokens= was passed), the echo of the trace file ships, and the full dialect +# reads budget_tokens= as the soft target (tracelocus.h). rw src --from-trace="$TMP/p15_trace.txt" --token-budget=50 >"$TMP/p15_ft50.xml" 2>/dev/null -[ "$( root_attr "$TMP/p15_ft50.xml" ctx over_ceiling )" = "1" ] \ - && ok "#15 --from-trace --token-budget=50: root over_ceiling=\"1\"" \ - || no "#15 --from-trace --token-budget=50: no over_ceiling=\"1\" on the root (the prose label alone is what a parser discards)" +rw src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --legend=full >"$TMP/p15_ft50f.xml" 2>/dev/null +FT50="$( root_est "$TMP/p15_ft50.xml" ctx )" +ft50_why="" +{ [ -n "$FT50" ] && [ "$FT50" -gt 50 ]; } 2>/dev/null || ft50_why="$ft50_why est_tokens='${FT50:-absent}'-not-past-50" +[ -z "$( root_attr "$TMP/p15_ft50.xml" ctx over_ceiling )" ] || ft50_why="$ft50_why over_ceiling-on-a-soft-target" +grep -aqF ". SWEPT across the usable range like arm #5 (the small end is where the -# fixed floor dominates): the root carries est_tokens=; delivered bytes sit inside the ladder's own -# allowance (N x kMinBytesPerToken x kCeilingFirstEntryTolerance = N x 2.36 x 1.15, serialize.h) OR the -# root says over_ceiling="1"; and a labelled root's est_tokens exceeds N (the label and the number agree). -# verify-wave2 F2: the byte half above cannot see the defect it was written for. At --token-budget=1600 -# the bundle is 3,485 B against a 4,342 B allowance — so the allowance arm PASSES — while the very same -# root says budget_tokens="1600" est_tokens="1665" and withholds the one attribute that reconciles them. -# over_ceiling= fired only on the ladder's LAST RUNG, so a bundle that overshoots by a little (the common -# case) was silently unlabelled while one that overshoots by a lot was labelled. The property is about the -# two NUMBERS the root itself prints, in the unit it prints them in: est_tokens <= N OR over_ceiling="1", -# on EVERY rung. 1600/1700 are in the sweep because they are the rungs that were red. +# fixed floor dominates): the root carries est_tokens= — KEPT, unchanged. +# REPLACED (forbudget-soft (rule 4)): the two halves that read the old hard ceiling — "delivered <= N x 2.36 x 1.15 OR the +# root says over_ceiling=1" and verify-wave2 F2's "est_tokens > N <=> over_ceiling=1" — assert the deleted contract: an +# explicit --for budget is a soft target, est_tokens may exceed N and nothing labels it (decision 2). The two numbers the +# root prints no longer contradict each other because the legend reads budget_tokens= as a target the rows stop at, not a +# ceiling the document fits. Each rung now pins, on the same src corpus and budgets: NOLBL (no over_ceiling= on the root); +# ST (the rows fit T = N x 2.36 x 0.90 or are the rank 1..4 floor, and a cut is maximal against the no-budget answer +# A7/A8 captured); RP (rows(N) a rank prefix of the next rung's rows); NX (a cut carries next=). MEASURED on +# d099ec21: 4 rows at 100..600, 5 at 800, 10 at 1200, 20 at 2000, every rung unlabelled with est_tokens > N. +# (#398 pool: #11's $TMP/f_for.out is written in another chain, so this section asks the same no-budget question itself — +# rw src --for="$FOR_TASK", the call the pool pre-warms — into its own file: the same bytes, no cross-chain read.) +rw src --for="$FOR_TASK" >"$TMP/p15_for_ref.xml" 2>/dev/null +f15_prev="" for N in 100 300 400 600 800 1200 1500 1600 1700 2000 2500 3000; do rw src --for="$FOR_TASK" --token-budget=$N >"$TMP/p15_for$N.xml" 2>/dev/null FB="$( bytes_of "$TMP/p15_for$N.xml" )" FE="$( root_est "$TMP/p15_for$N.xml" ctx )" FO="$( root_attr "$TMP/p15_for$N.xml" ctx over_ceiling )" - FLIM="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" if [ -z "$FE" ]; then no "#15 --for --token-budget=$N: root carries no est_tokens= ($FB B delivered; the price is not where a parser reads it)" else ok "#15 --for --token-budget=$N: root est_tokens=\"$FE\" ($FB B)" fi - if [ "$FB" -le "$FLIM" ] 2>/dev/null; then - ok "#15 --for --token-budget=$N: $FB B within the $FLIM B ladder allowance" - elif [ "$FO" = "1" ]; then - ok "#15 --for --token-budget=$N: $FB B over the $FLIM B allowance and the ROOT says so (over_ceiling=\"1\")" - { [ -n "$FE" ] && [ "$FE" -gt "$N" ]; } 2>/dev/null \ - && ok "#15 --for --token-budget=$N: est_tokens=$FE > $N — the label and the number agree" \ - || no "#15 --for --token-budget=$N: root over_ceiling=\"1\" but est_tokens='$FE' does not exceed the budget it is labelled over" - else - no "#15 --for --token-budget=$N: $FB B EXCEEDS the $FLIM B allowance with NO over_ceiling=\"1\" on the root (over_ceiling on root='$FO')" - fi - # F2 — the TOKEN property, independent of the byte allowance above: the root prints budget_tokens= and - # est_tokens= in the same unit, so a reader can subtract them. Whenever it does and the answer is - # positive, the root must say over_ceiling="1"; silence there means "inside the budget". - if [ -n "$FE" ] && [ "$FE" -gt "$N" ] 2>/dev/null; then - [ "$FO" = "1" ] \ - && ok "#15 --for --token-budget=$N: est_tokens=$FE > $N and the root says over_ceiling=\"1\"" \ - || no "#15 --for --token-budget=$N: root prints budget_tokens=$N est_tokens=$FE (over by $(( FE - N ))) with NO over_ceiling= — the two numbers contradict each other on one root" - elif [ -n "$FE" ]; then - [ "$FO" = "1" ] \ - && no "#15 --for --token-budget=$N: root says over_ceiling=\"1\" but est_tokens=$FE is inside the $N budget — the label is a lie in the other direction" \ - || ok "#15 --for --token-budget=$N: est_tokens=$FE <= $N and the root is silent (absent = inside the budget)" + soft_rows "$TMP/p15_for$N.xml" >"$TMP/p15_for$N.rows" 2>/dev/null + f15_sigs="$( grep -aoE ']*>' "$TMP/p15_for$N.xml" | head -1 )" + f15_sh="$( printf '%s' "$f15_sigs" | sed -nE 's/.* shown="([0-9]+)".*/\1/p' )"; f15_tot="$( printf '%s' "$f15_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" + f15_why="" + [ -n "$( ctx_root "$TMP/p15_for$N.xml" )" ] || f15_why="$f15_why no-root" + [ -z "$FO" ] || f15_why="$f15_why over_ceiling=\"$FO\"-on-a-soft-target" + f15_st="$( soft_st "$TMP/p15_for$N.xml" "$N" "$TMP/p15_for_ref.xml" )" || f15_why="$f15_why ST:[$f15_st]" + [ -z "$f15_prev" ] || rows_prefix "$TMP/p15_for$f15_prev.rows" "$TMP/p15_for$N.rows" || f15_why="$f15_why rows-not-a-superset-of-N=$f15_prev's" + if [ -n "$f15_sh" ] && [ -n "$f15_tot" ] && [ "$f15_sh" -lt "$f15_tot" ] 2>/dev/null; then + [ "${f15_sigs#* next=\"--for=}" != "$f15_sigs" ] || f15_why="$f15_why cut-without-next" fi + [ -z "$f15_why" ] \ + && ok "#15 --for --token-budget=$N (forbudget-soft): unlabelled (est_tokens=${FE:-?}), $f15_st, rows grow monotonically" \ + || no "#15 --for --token-budget=$N (forbudget-soft) —$f15_why" + f15_prev="$N" done # the two root attributes must be DEFINED by the legend of the document that carries them (§B7 class) perl -0pe 's##\1#gs' "$TMP/p15_for3000.xml" | grep -qE '(^|[^[:alnum:]_:.-])est_tokens\s*=' \ && ok "#15 --for: the legend defines est_tokens=" \ || no "#15 --for: est_tokens= rides the root but no legend clause defines it" -grep -o '' "$TMP/p15_for100.xml" | grep -qE '(^|[^[:alnum:]_:.-])over_ceiling\s*=' \ - && ok "#15 --for --token-budget=100: the legend defines over_ceiling=" \ - || no "#15 --for --token-budget=100: over_ceiling= rides the root but no legend clause defines it" +# REPLACED (forbudget-soft (rule 4)): was "the legend at --token-budget=100 defines over_ceiling=" — the soft target never emits +# that label, so the attribute to define is budget_tokens= itself, read as the soft row target the rows stop at. +[ -n "$( root_attr "$TMP/p15_for100.xml" ctx budget_tokens )" ] && grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/p15_for100.xml" \ + && ok "#15 --for --token-budget=100 (forbudget-soft): the legend defines budget_tokens= as the soft row target the rows stop at" \ + || no "#15 --for --token-budget=100 (forbudget-soft): budget_tokens= rides the root but the legend does not read it as a soft target" # (c) --handoff: priced, labelled, and withheld= is a boolean with the count beside it rw . --handoff --token-budget=100 >"$TMP/p15_ho100.xml" 2>/dev/null rw . --handoff --token-budget=100000 >"$TMP/p15_hobig.xml" 2>/dev/null @@ -2186,7 +2337,8 @@ est_18(){ # >= E, by the root's own arithmetic — so this arm READS E off a wide run where nothing is dropped and probes # just above it. No pinned byte count: if the corpus or the legend moves, E moves with it and the probe follows. # Both guards against an empty pass are asserted rather than assumed — the wide run must carry the clauses (else -# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). +# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). forbudget-soft (rule 4): +# the soft target deleted rung zero on purpose, so that control now asserts the opposite law (no clause is ever dropped). # GIT-LESS and relative, the #11 A7 sweep's discipline: no at=, no churn, a fixed root=, nothing from the live # repo. --detail=1 is what puts bytes at the BODY rate, which is the half of the defect a bodiless bundle cannot # see; the fixture's one long body exceeds the tight budget's residual, so the first-entry-whole floor emits a @@ -2195,21 +2347,7 @@ est_18(){ # est_tokens=1069 with no over_ceiling=, and the pre-fix rung dropped all three clauses and delivered 715. RZ="$TMP/rungzero" mkdir -p "$RZ/corpus" -python3 - "$RZ/corpus" <<'PYRZ' -import os, sys -out = sys.argv[ 1 ] -lines = [ "def widgetPingBoxRouter( alpha, beta ):", - ' """Widget ping box router: route every alpha reading onto the beta box."""', - " total = 0" ] -for j in range( 26 ): - lines.append( f" total = total + alpha * {j} - beta * {j} + widgetPingStep{j % 4}( total, {j} )" ) -lines.append( " return total" ) -with open( os.path.join( out, "router.py" ), "w" ) as fh: - fh.write( "\n".join( lines ) + "\n" ) -with open( os.path.join( out, "steps.py" ), "w" ) as fh: - for j in range( 4 ): - fh.write( f'def widgetPingStep{j}( total, step ):\n """Step {j}."""\n return total + step\n\n' ) -PYRZ +python3 "$TMP/rz_corpus.py" "$RZ/corpus" # the git-less corpus writer: SHARED DEFINITIONS # L1 (2026-09-19): #18 counts the FULL legend's droppable clauses (confidence=/tail:/route= prose), so rz_run asks for the full legend. rz_run(){ ( cd "$RZ" && rwcall 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'; } @@ -2218,7 +2356,10 @@ rz_note(){ grep -acF '[legend clauses:' "$RZ/o.xml"; } # 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 +# forbudget-soft (rule 4): 1400 -> 4000. Under the soft target the rows' text tier grows with N and nothing in the header is +# charged, so the document at 1400 prices at est_tokens=1770 and the widest form (e= extents on the rows) at 1875 from ~4000 +# (d099ec21); the probe E+5 must sit below the wide control, so the control moved up. Measured: no budget prices past 1875. +RZ_WIDE=4000 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. @@ -2230,7 +2371,7 @@ rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" RZ_CLAUSE_ROUTE='route= name-exact(X)|subtoken+body' RZ_CLAUSES=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) -grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) +grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) if [ -z "$RZ_E" ] || [ "$RZ_WIDE_NOTE" != "0" ] || [ "$RZ_CLAUSES" != "3" ]; then no "#18 rung zero: the wide control (--token-budget=$RZ_WIDE) does not carry its legend (est='${RZ_E:-unreadable}' dropped-note=$RZ_WIDE_NOTE clauses=$RZ_CLAUSES/3) — there is no price to probe against; re-anchor the fixture" @@ -2242,7 +2383,7 @@ else rz_run "$RZ_PROBE"; RZ_PE="$( rz_est )"; RZ_PN="$( rz_note )"; RZ_PB="$( bytes_of "$RZ/o.xml" )" RZ_PC=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) + grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) # the ROOT's verdict only (read off the open tag): since lane/cutfix-bodies a marks # a --detail body whose first line alone exceeds the BODY allowance, which is not the root's est_tokens claim, @@ -2254,21 +2395,26 @@ else no "#18 rung zero at --token-budget=$RZ_PROBE dropped a legend it could afford: dropped-note=$RZ_PN clauses=$RZ_PC/3 est_tokens=${RZ_PE:-unreadable} over_ceiling=$RZ_PO ($RZ_PB B) — the same document prices at $RZ_E at --token-budget=$RZ_WIDE, so it fits every budget >= $RZ_E" fi fi - # …and the rung must still FIRE where the kept document genuinely does not fit. Without this, deleting - # rung zero outright would turn the arm above green. - RZ_CTRL=$(( RZ_E - 200 )) - rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" - # …and the tight control asserts the ABSENCE of the same three, route= included: a present-in-wide / - # unchecked-in-tight assertion is the one-sided shape that let the missing clause through. - RZ_CC=0 - grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - if [ "$RZ_CN" != "0" ] && [ "$RZ_CC" = "0" ]; then - ok "#18 rung zero control at --token-budget=$RZ_CTRL (200 under the $RZ_E the full document prices at): all three clauses dropped and the note says so (est_tokens=$RZ_CE) — the rung still fires when the drop is real" - else - no "#18 rung zero control at --token-budget=$RZ_CTRL: dropped-note=$RZ_CN clauses still riding=$RZ_CC/3 (est_tokens=${RZ_CE:-unreadable}) — rung zero no longer fires at all, so the arm above is green for the wrong reason" - fi + # …the CONTROL — REPLACED (forbudget-soft (rule 4)). Was: 200 under E the rung must still FIRE (all three clauses dropped + # and the '[legend clauses:' note), or deleting rung zero would turn the arm above green. The soft target deletes rung zero + # by design (decision 2: no header ladder — no legend clause is ever dropped for a budget), so the control now asserts the + # law that replaced it, HL, at the same E-200 and at a budget far under the floor (400: base d4325a8c dropped all three + # there, est_tokens=1050, over_ceiling="1"): all three clauses ride, no drop note, no over_ceiling= on the root. The arm + # above then cannot be green for the wrong reason either: it pins the three clauses at E+5, this one below every price. + rz_hl_bad="" + for RZ_CTRL in $(( RZ_E - 200 )) 400; do + rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" + RZ_CC=0 + grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + RZ_CO=0; grep -aoE '^]*>' "$RZ/o.xml" | grep -qF 'over_ceiling=' && RZ_CO=1 + { [ -n "$RZ_CE" ] && [ "$RZ_CN" = "0" ] && [ "$RZ_CC" = "3" ] && [ "$RZ_CO" = "0" ]; } \ + || rz_hl_bad="$rz_hl_bad @$RZ_CTRL:[note=$RZ_CN clauses=$RZ_CC/3 over_ceiling=$RZ_CO est_tokens=${RZ_CE:-unreadable}]" + done + [ -z "$rz_hl_bad" ] \ + && ok "#18 no rung zero (forbudget-soft): at --token-budget=$(( RZ_E - 200 )) and 400 (under the $RZ_E the document prices at) all three clauses ride, no drop note, no over_ceiling=" \ + || no "#18 no rung zero (forbudget-soft): a soft target dropped a legend clause or labelled the root —$rz_hl_bad" fi } @@ -2283,7 +2429,7 @@ est_19(){ # this arm's fixture: --signatures-only printed est_tokens=2616 over 8183 B (round(bytes/2.50) = 3273), and at # --token-budget=1500 the root said est_tokens=1360 with no over_ceiling= over a 5043 B document (2017 tokens). # WHAT IS ASSERTED: the #11 identities (markup at 2.50, bodies at 3.80), exact, on three shapes of a document -# that really carries six rows — and under the explicit ceiling, est_tokens > budget iff over_ceiling="1". +# that really carries six rows — and under the explicit budget (a soft target since forbudget-soft) no over_ceiling=. # Git-less and relative (#18's discipline): nothing from the live repo, a fixed root=. HX="$TMP/hdrprice" mkdir -p "$HX" @@ -2321,13 +2467,17 @@ for hx_mode in "--signatures-only" "" "--token-budget=1500"; do hx_out="$( hx_identity "$HX/o.xml" 2>/dev/null || true )" no "#19 rows unpriced ($hx_label): bytes/span/est/expected = ${hx_out:-unreadable} (six rows required; a gap of ~bytes-of-rows/2.50 is the defect)" fi + # REPLACED (forbudget-soft (rule 4)): was "over_ceiling= agrees with est_tokens vs 1500, both ways" — the ceiling verdict + # had to see the rows. A soft target emits no ceiling verdict (the rows are header, never charged against T), + # so the arm pins NOLBL on the same document: the identity above prices the rows, and the root carries no over_ceiling= + # although est_tokens exceeds 1500 (d099ec21: 3726; base d4325a8c labelled it). Read off the root only. if [ "$hx_mode" = "--token-budget=1500" ]; then - HX_E="$( grep -aoE 'est_tokens="[0-9]+"' "$HX/o.xml" | head -1 | tr -dc '0-9' )" - HX_O=0; grep -aqF 'over_ceiling="1"' "$HX/o.xml" && HX_O=1 - if [ -n "$HX_E" ] && { { [ "$HX_E" -gt 1500 ] && [ "$HX_O" = 1 ]; } || { [ "$HX_E" -le 1500 ] && [ "$HX_O" = 0 ]; }; }; then - ok "#19 --token-budget=1500: over_ceiling=$HX_O agrees with est_tokens=$HX_E (the ceiling verdict sees the rows)" + HX_ROOT="$( ctx_root "$HX/o.xml" )" + HX_E="$( printf '%s' "$HX_ROOT" | sed -nE 's/.* est_tokens="([0-9]+)".*/\1/p' )" + if [ -n "$HX_ROOT" ] && [ -n "$HX_E" ] && [ "${HX_ROOT#* over_ceiling=}" = "$HX_ROOT" ]; then + ok "#19 --token-budget=1500 (forbudget-soft): est_tokens=$HX_E with the rows priced, no over_ceiling= on the soft target's root" else - no "#19 --token-budget=1500: over_ceiling=$HX_O disagrees with est_tokens=${HX_E:-unreadable} — the ceiling verdict was made without the rows" + no "#19 --token-budget=1500 (forbudget-soft): root '${HX_ROOT:0:120}' — want est_tokens= present and no over_ceiling= on a soft target" fi fi done diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index e4f04c655..cde22604c 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -280,7 +280,7 @@ 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" ): 6, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge. + ( "src/serialize.h", "nb" ): 7, # 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. @@ -288,6 +288,8 @@ NUMERIC_ONLY = { # (19 B + 20 digits = 39 B). A count, 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. + # forbudget-soft (2026-10-14): +1 in the same nb[96] — ' past_target="{}"' (15 B + 20 + # digits = 35 B). A count, no %s, nothing escaped. ( "src/serialize.h", "precAttr" ): 1, ( "src/serialize.h", "rankAttr" ): 1, ( "src/serialize.h", "rc" ): 1, @@ -620,7 +622,7 @@ if not bad: # 2026-10-08 (lane/cli-perf-068): -1 call/-1 mention/-1 site, rows unchanged (the lane's own delta on its base: # 351/241/241/105 -> 350/240/240/105). # train 26c (cli-perf merged onto the train's 363/248/248/109): re-derived on the merged tree. -EXPECTED = { "mentions": 362, "calls": 247, "sites": 247, "rows": 109, "widthforms": 0 } +EXPECTED = { "mentions": 363, "calls": 248, "sites": 248, "rows": 109, "widthforms": 0 } # forbudget-soft: +1 nb site (past_target=) # 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/forbodyslotcheck.sh b/test/forbodyslotcheck.sh new file mode 100755 index 000000000..ec398f41a --- /dev/null +++ b/test/forbodyslotcheck.sh @@ -0,0 +1,817 @@ +#!/usr/bin/env bash +# forbodyslotcheck.sh — the ONE owner body of an explanatory --for question (the "body slot"), on top of the +# stop-when-complete budget semantics (lane/for-body-slot, train 28; design D of lane/for-noise-body-slot +# re-expressed: the body is a SECTION of a complete how-does answer, never paid by dropping sig rows). +# +# THE CONTRACT THIS PINS (arms tagged [P] were RED on the pre-change binary 974dd5b1; [G] arms are twins and +# near-miss negatives that hold on both binaries and must keep holding — PROCESS rule 4, no gate erosion): +# +# TRIGGER CLI --for (XML, full and compact legends) and MCP `for`, on the compact posture of the +# subtoken+body route (root bundle="compact"), when the task carries an explanatory cue +# (taskroute.h kExplanatoryCues, word-bounded: how does/do/is/are, what does, implementation of). +# No slot: no cue, --signatures-only, --json (the JSON dialect carries no bodies in any posture — +# a named floor), --auto-bodies and --detail=N (the caller chose the bodies), --format=candidates, +# the name-exact route (the auto bundle already serves the anchor's body). +# OWNER owner-hop's predicate (verbs_for.h forOwnerHopPlan): a callable definition (Function/Method, +# isDefinitionNotDeclaration, so never a .d.ts / header / overload / pure-virtual / interface-method +# declaration), not a test symbol, inside the ranked head, whose whole folded name equals a question +# word; common words after the rest. Among owners the one with a call edge (resolved or by name) to +# an owner named by ANOTHER question word wins (the entry point; a self-edge, a recursion or a +# same-named callee such as super().refresh() never makes one), else the best rank (pick="name"). +# No name owner: the MEMBER owner (Phase B, owner ruling Q1) — the first head-tier class/struct/ +# interface row (rank <= 24) with a callable definition that is its member (the parser's scope= equals +# the class name, or the class's own byte span contains it in the same file) and whose folded name is +# a non-common question word (pick="member"); Go receiver methods are a named floor (no scope, outside +# the type's span). None: no slot — the answer is the slot-off answer byte for byte (FIX1, owner ruling +# 2026-10-15: the subtoken fallback, pick="subtoken", is DROPPED; a callable that only shares a word with +# the question is not its owner). Classes/structs/types are never the owner. +# RENDER one in the --expand shape (packBodies), kept. A body over +# kForBodySlotMaxBytes (6000) is head-cut at a line end: truncated="1" lines="lo-hi/T" +# next="--expand=P:L:N:A-B" (the cutfix-bodies vocabulary), bodies capped="1"; a FIRST line longer +# than the cap is served whole with over_ceiling="1" (packBodies' existing rule). Root bodies="1". +# BUDGETS default (no explicit budget): the body is a required section of the complete answer. , +# , are byte-identical to the slot-off answer (RIPWIRE_NO_BODY_SLOT=1, the A/B handle, +# as RIPWIRE_NO_OWNER_HOP); est_tokens prices the body; the runaway guard (30,000 B) is unchanged +# and never fires because of the body. Explicit --token-budget=N / MCP budget_tokens: +# reserve-sections-first — the body is reserved with the trailing sections at the cut, rows past +# the cut fill what is left; rows stay a rank prefix, monotone in N, never more than slot-off; +# the body never shrinks as N grows and equals the default body at a wide N; when its share cannot +# hold it the slot is skipped with bodies="0" reason="budget" and the owner stays recoverable +# through a next= naming --expand of it. +# MCP `for` serves the same body (parity decision, design D open question 4); budget_tokens as above. +# +# Arms (colour on 974dd5b1 in brackets): +# D1 [P] T_D: bodies=1 reason=owner, , holds getCookie / HTTPException(401 / +# ctx.set('jwtPayload'; with verify +# D2 [G] T_D: not verify's body (jwt is r1 here, so this is NOT an entry-point discriminator — L1 and EP1 are) +# D3 [P] est_tokens prices the body at no less than 0.2 token per body byte (the body rate is 3.80 B/token) +# D4 [G] sigs/hops/tail byte-identical slot vs slot-off on every fixture question (checklist 23); slot-off has no body +# D5 [P] legends: full answer explains bodies=1; compact legend defines b t= n= p= l= when a body rides; R3 (FIX1) [P] the +# compact clause reads pick=name / pick=member and says reason=owner is not necessarily the subject nor the whole answer +# D6 [G] no explanatory cue (where / show documentation / bare noun phrase): bodies=0, no 10%), so a cap counted in +# characters fails the <= 6000 B check (review R7) +# D11 [P] one 7.7 KB line of multibyte text: served whole, over_ceiling="1" on the , valid UTF-8 +# L1 [P] Rust: entry point index (calls walk) beats walk at r1 L2 [P] C++: Builder::Build DEFINITION (.cc), not the r1 header declaration +# L3 [P] Go: method Package, never the type Packager +# N1 [P] TS: negotiate's definition, not the r1 .d.ts declaration; [G] a .d.ts-only name never yields a .d.ts body +# N2 [G] the only name owner is a tests/ helper: NO slot (no fallback since FIX1 — by-design break, rule 4: through +# 66cc4b63 the subtoken fallback served a non-test body here; red on 66cc4b63), never a tests/ body +# N3 [P] two equal owners in two files: ONE body, the better-ranked one (p= and l= of the r1 row) +# N4 [P] two same-named methods of two classes in ONE file: ONE body, the r1 row's (entity identity, checklist 16) +# N5 [G] a struct named by the question is never the body; every is fn/method +# E1 [P] --token-budget=5000: the body rides; rows a rank prefix of the default rows, >= 4, no over_ceiling (re-anchored +# from 3000 in Phase B: the reserve-first share the rows at the cut leave holds the 2.7 KB jwt body from N=4800 on +# this fixture; below it the slot is skipped and disclosed — E2, E3) +# E2 [P] --token-budget=400: bodies=0 reason=budget; the skipped slot discloses itself on +# E2b [P] the same skip under a directory whose name holds a space: next= is quoted (nextFlag) and splits to one argument +# E3 [G/P] N sweep: rows monotone + rank prefix + <= slot-off; body bytes monotone; [P] at 1000000 body == default body +# E4 [P] T_REF at --token-budget=800 and MCP budget_tokens=800 (slot-off serves rows past the cut there, and the share +# the rows at the cut leave holds the small refresh body): the body is paid INSIDE N — rows(slot) < rows(off), +# names the displaced rows, est_tokens(slot) - est_tokens(off) <= the room slot-off +# left under N + (legend + root delta + one row) / 2.5 (a body appended after the row cut fails both; M5) +# MC1-MC4 MCP for: [P] body parity with the CLI; [P] budget_tokens=400 recoverable; [G] no cue, no body +# MC5 [P] MCP for on the cut owner (T_BIG: same truncated=/lines=/next= and CDATA as the CLI) and on the Rust +# entry point (T_RS: index) — MCP/CLI drift on the cut or on the pick +# EV1 [P] RIPWIRE_NO_OWNER_HOP=1 leaves T_D's byte-identical (the owner-hop env never reaches the slot) +# EP1 [P] namesake: ModalScreen.refresh (r4) calls Screen.refresh (r3) through super(): not an entry point, +# the r3 row's body is served; EP2 [P] a recursive owner (flatten r2) never outranks normalize (r1) +# DS1-DS4 [P] the definition, never the declaration row that outranks it: TS overload signatures, C++ pure +# virtual + in-class override declaration, Go interface method spec, TS abstract method +# Q1 [P] member owner, the head-tier class's member defined outside the head tier: C++ out of line (sc=), +# Rust impl (sc=), Python (sc=), TS class (span containment); pick="member" +# [G] floor and near misses: Go receiver method (no member body — the named floor, until Go carries a +# receiver scope); a common-word member (run); a class past the head tier (r31); another class's +# same-named method that outranks the member is not served (entity identity); never the class/type +# M1-M4 mutation controls (canned pre-change shapes are rejected) X1-X3 determinism, xmllint, no '--' in comments +# I0-I10 [optional, RIPWIRE_BASE_BIN=] byte identity where the slot must not fire (I0: slot off +# == base; I10: --token-budget=3000 with RIPWIRE_NO_BODY_SLOT=1 == base — no reserve without the slot) +# +# Usage: bash test/forbodyslotcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forbodyslotcheck.sh +# BIN is $1, else RIPWIRE_BIN, else build/ripwire; the first output line names the binary used. +# Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" +unset RIPWIRE_NO_BODY_SLOT RIPWIRE_NO_OWNER_HOP +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "forbodyslotcheck: 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; } +skip(){ printf ' SKIP %s\n' "$*"; return 0; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +cd "$ROOT" +FIX="$ROOT/test/forbodyslotfix" +[ -d "$FIX/src" ] || { echo "fixture $FIX missing"; exit 2; } + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT + +# ── the checker: one Python reader of a --for answer, one verdict per call (exit 0 = holds) ───────────────── +cat > "$TMP/chk.py" <<'PY' +import html, json, os, re, sys +cmd, args = sys.argv[1], sys.argv[2:] +def load(p): + return open(p, "rb").read().decode("utf-8", "replace") +def attrs(s): + return dict((k, html.unescape(v)) for k, v in re.findall(r'(\w+)="([^"]*)"', s)) +def root(t): + m = re.search(r']*)>', t) + return attrs(m.group(1)) if m else {} +def sigs(t): + m = re.search(r']*)>', t) + return attrs(m.group(1)) if m else {} +def rows(t): + m = re.search(r']*>(.*?)', t, re.S) + out = [] + for d in re.finditer(r']*)>', m.group(1) if m else ""): + a = attrs(d.group(1)) + out.append((int(a.get("r", "0") or 0), a.get("n", ""), a.get("p", ""), a)) + return sorted(out, key=lambda x: x[0]) +def bodies(t): + """every …: (attrs, cdata bytes, the whole element)""" + out = [] + for m in re.finditer(r']*)>(.*?)', t, re.S): + cd = "".join(re.findall(r'', m.group(2), re.S)) + out.append((attrs(m.group(1)), cd, m.group(0))) + return out +def legend(t): + return " ".join(re.findall(r'', t, re.S)) +def fail(msg): + print(msg); sys.exit(1) +def first_row(t, name): + for r, n, p, a in rows(t): + if n == name: + return a + return None +def defstart(a, cd): + """the def's first source line: l= is the NAME's line, and the CDATA starts at the def (a C return type sits above it)""" + lines = cd.split("\n") + k = next((i for i, x in enumerate(lines) if a.get("n", "") in x), 0) + return int(a["l"]) - k +def strip_slot(t): + """the answer minus everything the body slot may add: legend comments, the element, root bodies=/reason=/est_tokens=""" + t = re.sub(r'', '', t, flags=re.S) + t = re.sub(r'', '', t, flags=re.S) + t = re.sub(r']*/>', '', t) + t = re.sub(r' (bodies|reason|est_tokens)="[^"]*"', '', t) + return t +def est(t): + v = root(t).get("est_tokens", "") + if not v.isdigit(): fail("est_tokens missing or not numeric: %r" % v) + return int(v) +if cmd == "rootattr": # rootattr FILE ATTR REGEX + t = load(args[0]); v = root(t).get(args[1]) + if v is None or not re.fullmatch(args[2], v): fail("root %s=%r, wanted %s" % (args[1], v, args[2])) + print("%s=%s" % (args[1], v)) +elif cmd == "noroot": # noroot FILE ATTR : the root carries no ATTR= + t = load(args[0]); v = root(t).get(args[1]) + if v is not None: fail("root carries %s=%r" % (args[1], v)) +elif cmd == "bodycount": # bodycount FILE N : exactly N and root bodies=N + t = load(args[0]); n = len(bodies(t)); v = root(t).get("bodies") + if n != int(args[1]) or v != args[1]: fail(" count=%d root bodies=%r, wanted %s" % (n, v, args[1])) + print("bodies=%s" % v) +elif cmd == "nobody": # nobody FILE : no whose CDATA holds every STR + t = load(args[0]); bs = bodies(t) + hit = [b for b in bs if b[0].get("n") == args[1] and b[0].get("p") == args[2]] + if not hit: fail("no (bodies: %s)" % (args[1], args[2], [(b[0].get("n"), b[0].get("p")) for b in bs])) + a, cd, el = hit[0] + miss = [s for s in args[3:] if s not in cd] + if miss: fail("body %s lacks %s" % (args[1], miss)) + print(" %d B" % (args[1], args[2], a.get("l"), len(cd.encode()))) +elif cmd == "nobodyof": # nobodyof FILE NAME : no + t = load(args[0]) + if any(b[0].get("n") == args[1] for b in bodies(t)): fail("a rides" % args[1]) +elif cmd == "nobodyat": # nobodyat FILE REGEX : no + t = load(args[0]); bad = [b[0].get("p") for b in bodies(t) if re.search(args[1], b[0].get("p", ""))] + if bad: fail(" matched %s: %s" % (args[1], bad)) +elif cmd == "callsin": # callsin FILE NAME CALLEE : the carries with + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + el = hit[0][2] + if " has no naming %s" % (args[1], args[2])) +elif cmd == "bodyattr": # bodyattr FILE NAME ATTR REGEX + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + v = hit[0][0].get(args[2]) + if v is None or not re.search(args[3], v): fail(", wanted %s" % (args[1], args[2], v, args[3])) + print("" % (args[1], args[2], v[:90])) +elif cmd == "bodyisrow": # bodyisrow FILE NAME : the one has the p= and l= of the best-ranked NAME row + t = load(args[0]); hit = [b[0] for b in bodies(t) if b[0].get("n") == args[1]] + if len(hit) != 1: fail("%d , wanted exactly 1" % (len(hit), args[1])) + r = first_row(t, args[1]) + if r is None: fail("no row to compare against" % args[1]) + rp = r.get("p", ""); bp = hit[0].get("p", "") + if (bp, hit[0].get("l")) != (rp, r.get("l")): fail(" is not the best-ranked row p=%s l=%s r=%s" % (bp, hit[0].get("l"), rp, r.get("l"), r.get("r"))) + print(" = r%s" % (bp, hit[0].get("l"), r.get("r"))) +elif cmd == "bodykinds": # bodykinds FILE : every is a callable kind + t = load(args[0]); bad = [(b[0].get("n"), b[0].get("t")) for b in bodies(t) if b[0].get("t") not in ("fn", "method")] + if bad: fail("non-callable bodies: %s" % bad) +elif cmd == "rowis": # rowis FILE R NAME PATH : the row at rank R is NAME in PATH (an arm's premise) + t = load(args[0]); hit = [x for x in rows(t) if x[0] == int(args[1])] + if not hit or hit[0][1] != args[2] or hit[0][2] != args[3]: fail("premise: r%s is %s, wanted %s %s" % (args[1], hit[0][1:3] if hit else None, args[2], args[3])) +elif cmd == "cutatline": # cutatline FILE NAME MAXB : a head-cut body that is the def's first K whole source lines, <= MAXB + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + a, cd, el = hit[0] + if len(cd.encode()) > int(args[2]): fail("CDATA %d B > %s" % (len(cd.encode()), args[2])) + m = re.fullmatch(r'(\d+)-(\d+)/(\d+)', a.get("lines", "")) + if not m: fail("lines=%r is not lo-hi/T" % a.get("lines")) + lo, hi, tot = map(int, m.groups()) + src = open(os.path.join(os.environ["FIX"], a["p"]), encoding="utf-8").read().split("\n") + l0 = defstart(a, cd) + want = "\n".join(src[l0 - 1: l0 - 1 + (hi - lo + 1)]) + if cd.rstrip("\n") != want.rstrip("\n"): fail("CDATA is not source lines %d..%d of %s (a cut inside a line?)" % (l0, l0 + hi - lo, a["p"])) + if not (lo == 1 and hi < tot): fail("lines=%s does not describe a head cut" % a.get("lines")) + print("cut %s lines=%s %d B" % (args[1], a["lines"], len(cd.encode()))) +elif cmd == "defline": # defline FILE NAME : print the def's first source line of the + hit = [b for b in bodies(load(args[0])) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + print(defstart(hit[0][0], hit[0][1])) +elif cmd == "nextof": # nextof FILE NAME : print the value + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit or not hit[0][0].get("next"): fail("no " % args[1]) + print(hit[0][0]["next"]) +elif cmd == "startsat": # startsat FILE PATH LINE : the first CDATA begins with source line LINE of PATH + t = load(args[0]); bs = bodies(t) + if not bs: fail("the continuation served no body") + src = open(os.path.join(os.environ["FIX"], args[1]), encoding="utf-8").read().split("\n") + want = src[int(args[2]) - 1] + if not bs[0][1].startswith(want): fail("continuation begins %r, wanted source line %s %r" % (bs[0][1][:60], args[2], want[:60])) +elif cmd == "utf8whole": # utf8whole FILE NAME TAIL : the CDATA is valid UTF-8 and ends with TAIL + raw = open(args[0], "rb").read() + m = re.search(rb']*n="' + re.escape(args[1].encode()) + rb'"[^>]*>', raw, re.S) + if not m: fail("no CDATA" % args[1]) + try: cd = m.group(1).decode("utf-8", "strict") + except UnicodeDecodeError as e: fail("CDATA is not valid UTF-8: %s" % e) + if args[2] not in cd: fail("the line is not whole: %r missing" % args[2]) + print("%s %d B valid UTF-8" % (args[1], len(m.group(1)))) +elif cmd == "sectionsame": # sectionsame FILE_SLOT FILE_OFF : identical once the slot's own bytes are removed + a, b = strip_slot(load(args[0])), strip_slot(load(args[1])) + if a != b: + i = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), min(len(a), len(b))) + fail("sections differ at byte %d: slot %r | off %r" % (i, a[max(0, i - 40): i + 60], b[max(0, i - 40): i + 60])) +elif cmd == "estdelta": # estdelta FILE_SLOT FILE_OFF : est_tokens(slot) - est_tokens(off) >= 0.2 x body element bytes + ts, to = load(args[0]), load(args[1]) + es, eo = est(ts), est(to) + m = re.search(r'', ts, re.S) + eb = len(m.group(0).encode()) if m else 0 + if eb == 0: fail("no element to price (est %s vs %s)" % (es, eo)) + if es - eo < eb // 5: fail("est_tokens %s - %s < %d B x 0.2 (the body rate is 3.80 B/token)" % (es, eo, eb)) + print("est_tokens %s vs %s (+%d for %d B)" % (es, eo, es - eo, eb)) +elif cmd == "paidinside": # paidinside FILE_SLOT FILE_OFF N : E4 — the body is paid inside the target N, the displaced rows named + ts, to = load(args[0]), load(args[1]) + if not bodies(ts): fail("premise: no body rides the budgeted answer") + ns, no_ = len(rows(ts)), len(rows(to)) + if not ns < no_: fail("rows(slot)=%d is not below rows(off)=%d: the body was not paid inside the target" % (ns, no_)) + sa = sigs(ts) + if not (sa.get("shown", "").isdigit() and sa.get("total", "").isdigit()): fail(" lacks numeric shown=/total=: %s" % sa) + if int(sa["shown"]) != ns or int(sa["total"]) <= ns: fail(" with %d rows: the cut is not disclosed" % (sa["shown"], sa["total"], ns)) + # the CLI names the continuation as an argv (next=), the MCP twin as the re-call's budget (next_budget_tokens=); both resume at next_offset= + if sa.get("next_offset") != str(ns) or not (sa.get("next") or sa.get("next_budget_tokens", "").isdigit()): + fail(": the displaced rows are not recoverable" % (sa.get("next_offset"), sa.get("next"), sa.get("next_budget_tokens"))) + es, eo = est(ts), est(to) + lg = len(legend(ts).encode()) - len(legend(to).encode()) + rd = len(re.search(r']*>', ts).group(0).encode()) - len(re.search(r']*>', to).group(0).encode()) + mrow = max(len(d.group(0).encode()) for d in re.finditer(r']*>.*?', to, re.S)) + room = max(0, int(args[2]) - eo) # what slot-off left unused under N: a body may fill it without displacing a row + bound = room + (max(lg, 0) + max(rd, 0) + mrow) / 2.5 + 4 + if es - eo > bound: fail("est_tokens %d - %d = %d > %.0f (room %d, legend %+d B, root %+d B, one row %d B): the body rides on top of the target" % (es, eo, es - eo, bound, room, lg, rd, mrow)) + print("rows %d < %d, , est %d vs %d (bound +%.0f)" % (ns, no_, sa["shown"], sa["next_offset"], es, eo, bound)) +elif cmd == "bodyat": # bodyat FILE NAME PATH LINE : exactly one , it is n=NAME p=PATH l=LINE + bs = bodies(load(args[0])) + if len(bs) != 1: fail("%d elements, wanted 1: %s" % (len(bs), [(b[0].get("n"), b[0].get("p"), b[0].get("l")) for b in bs])) + a = bs[0][0] + if (a.get("n"), a.get("p"), a.get("l")) != (args[1], args[2], args[3]): fail(", wanted n=%s p=%s l=%s" % (a.get("n"), a.get("p"), a.get("l"), args[1], args[2], args[3])) + print("" % (args[1], args[2], args[3])) +elif cmd == "bodiesattr": # bodiesattr FILE ATTR REGEX : the element's ATTR + m = re.search(r']*)>', load(args[0])) + if not m: fail("no element") + v = attrs(m.group(1)).get(args[1]) + if v is None or not re.fullmatch(args[2], v): fail(", wanted %s" % (args[1], v, args[2])) + print("" % (args[1], v)) +elif cmd == "rowline": # rowline FILE R : print the l= of the row at rank R ("" when none) + hit = [a for r, n, p, a in rows(load(args[0])) if r == int(args[1])] + print(hit[0].get("l", "") if hit else "") +elif cmd == "rankof": # rankof FILE NAME PATH : print the rank of the row n=NAME p=PATH (0 = not among the rows) + hit = [r for r, n, p, a in rows(load(args[0])) if n == args[1] and p == args[2]] + print(hit[0] if hit else 0) +elif cmd == "bodiessec": # bodiessec FILE : the … element verbatim + m = re.search(r'', load(args[0]), re.S) + if not m: fail("no element") + sys.stdout.write(m.group(0)) +elif cmd == "mbcut": # mbcut FILE NAME PCT : the CDATA's UTF-8 bytes exceed its characters by > PCT % + hit = [b for b in bodies(load(args[0])) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + cd = hit[0][1]; nb = len(cd.encode()); nc = len(cd) + if nb == 0 or 100.0 * (nb - nc) / nb <= float(args[2]): fail("CDATA %d B vs %d characters: not > %s%% multibyte" % (nb, nc, args[2])) + print("CDATA %d B = %d characters (+%.1f%%)" % (nb, nc, 100.0 * (nb - nc) / nb)) +elif cmd == "legend": # legend FILE STR... : every STR appears inside the XML comments + lg = legend(load(args[0])); miss = [s for s in args[1:] if s not in lg] + if miss: fail("legend lacks %s" % miss) +elif cmd == "nodashes": # nodashes FILE : no "--" inside any XML comment + bad = [c[:60] for c in re.findall(r'', load(args[0]), re.S) if "--" in c] + if bad: fail("'--' inside an XML comment: %s" % bad) +elif cmd == "rowkeys": # rowkeys FILE : print "n|p" per row in rank order (one per line) + print("\n".join("%s|%s" % (n, p) for r, n, p, a in rows(load(args[0])))) +elif cmd == "prefix": # prefix FILE_A FILE_B : A's rows are a rank prefix of B's, A has >= 4 rows + ra = [(n, p) for r, n, p, a in rows(load(args[0]))]; rb = [(n, p) for r, n, p, a in rows(load(args[1]))] + if len(ra) < 4: fail("only %d rows (the rank 1..4 floor)" % len(ra)) + if ra != rb[:len(ra)]: fail("rows are not a rank prefix of the default answer (first diff at %d)" % next(i for i in range(len(ra)) if i >= len(rb) or ra[i] != rb[i])) + print("%d rows, a prefix of %d" % (len(ra), len(rb))) +elif cmd == "nrows": # nrows FILE + print(len(rows(load(args[0])))) +elif cmd == "bodybytes": # bodybytes FILE : CDATA bytes of every + print(sum(len(b[1].encode()) for b in bodies(load(args[0])))) +elif cmd == "cdata": # cdata FILE NAME : the CDATA, verbatim + hit = [b for b in bodies(load(args[0])) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + sys.stdout.write(hit[0][1]) +elif cmd == "bodiesnext": # bodiesnext FILE REGEX : a element (the slot's own disclosure) carries next=~REGEX + m = re.search(r']*)>', load(args[0])) + if not m: fail("no element (a skipped slot discloses itself there)") + a = attrs(m.group(1)) + if not re.search(args[1], a.get("next", "")): fail(" has no next= matching %s" % (m.group(1)[:120], args[1])) + print("" % a.get("next")) +elif cmd == "over": # over FILE : the root carries no over_ceiling="1" + if root(load(args[0])).get("over_ceiling") == "1": fail("over_ceiling=\"1\" on the root") +elif cmd == "sigsopen": # sigsopen FILE : the answer has a ") +else: + fail("unknown check " + cmd) +PY +export FIX +chk(){ python3 "$TMP/chk.py" "$@"; } +run(){ "$BIN" "$FIX" --cache="$TMP/rw.cache" "$@" 2>/dev/null; } # a fresh cache per gate run: this binary's own parse +runoff(){ RIPWIRE_NO_BODY_SLOT=1 "$BIN" "$FIX" --cache="$TMP/rw.cache" "$@" 2>/dev/null; } +strip_at(){ sed -e 's/ at="[^"]*"//g' -e 's/"at":"[^"]*",\{0,1\}//g'; } +cat > "$TMP/mcptext.py" <<'PY' +import json, sys +for line in sys.stdin: + line = line.strip() + if not line: continue + try: d = json.loads(line) + except Exception: continue + for c in (d.get("result") or {}).get("content") or []: + if c.get("type") == "text": sys.stdout.write(c["text"]) +PY +mcpfor(){ # mcpfor TASK [EXTRA_JSON_MEMBERS] > FILE + python3 -c 'import json,sys; a={"path":sys.argv[1],"task":sys.argv[2]}; a.update(json.loads(sys.argv[3] or "{}")); print(json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":a}}))' "$FIX" "$1" "${2:-}" \ + | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py" +} +mcpforoff(){ RIPWIRE_NO_BODY_SLOT=1 mcpfor "$@"; } # the exported env reaches the --mcp process inside mcpfor + +# ── the runtime roots (written under $TMP: outside the indexed tree, so the repository's own --for corpus is unchanged) ── +# Q1 member owner — the ninja shape: a head-tier class whose member that the question names is defined OUTSIDE the head tier. +# 45 helpers named by two question words, each body naming the member word, lower that word's weight; the member's long body +# names no question word. EP / DS: entry-point and declaration-sibling near misses (premises checked below, never assumed). +cat > "$TMP/gen.py" <<'PY' +import itertools, os, sys +out = sys.argv[1] +W = ["kiln", "glaze", "firing", "queue", "batch", "track", "schedule", "record", "result", "job"] +def w(p, s): + os.makedirs(os.path.dirname(p), exist_ok=True) + open(p, "w").write(s) +def fillers(root, verb, alt=None): + xs = [] + for k, (a, b) in enumerate(itertools.combinations(W, 2)): + xs.append("int %s_%s_%d( int x )\n{\n // %s the %s %s\n return x + %d;\n}" % (a, b, k, alt if alt and k % 3 == 0 else verb, a, b, k)) + w(root + "/src/fillers.c", "\n".join(xs) + "\n") +def longbody(ind, n=90): + return "\n".join("%stotal += step_value( left, %d ) * weight_of( %d );" % (ind, i, i) for i in range(n)) +def cpp(root, verb="Assemble", other=None): + fillers(root, verb.lower()) + w(root + "/src/forge.h", "// the forge: schedules kiln jobs, tracks firing queues and glaze batches\nstruct KilnJob;\nstruct GlazeBatch;\nclass Forge\n{\npublic:\n" + " // %s every queued kiln job: schedule glaze batches, track firing queues, record kiln results\n bool %s( KilnJob* job );\n" + " void ScheduleGlaze( GlazeBatch* batch );\n void TrackFiring( KilnJob* job );\n KilnJob* NextKilnJob();\n" + " int kiln_slots_;\n int glaze_batches_;\n int firing_queues_;\n};\n" % (verb.lower(), verb)) + w(root + "/src/forge.cc", '#include "forge.h"\nint step_value( int a, int b );\nint weight_of( int a );\nbool Forge::%s( KilnJob* job )\n{\n int left = 3;\n int total = 0;\n%s\n return job != nullptr && total > 0;\n}\n' % (verb, longbody(" "))) + if other: + w(root + "/src/smelter.cc", "class %s\n{\npublic:\n bool %s( int job );\n};\nint step_value( int a, int b );\nbool %s::%s( int job )\n{\n int total = 0;\n%s\n return total > job;\n}\n" % (other, verb, other, verb, longbody(" ", 40))) +def deep(root): + fillers(root, "assemble", alt="assemble forge") + w(root + "/src/forge.h", "class Forge\n{\npublic:\n bool Assemble( int a );\n int width_ = 0;\n};\n") + w(root + "/src/forge.cc", '#include "forge.h"\nint step_value( int a, int b );\nint weight_of( int a );\nbool Forge::Assemble( int a )\n{\n int left = 3;\n int total = 0;\n%s\n return total > a;\n}\n' % longbody(" ")) +def rs(root): + fillers(root, "assemble") + w(root + "/src/forge.rs", "/// the forge: schedules kiln jobs, tracks firing queues and glaze batches, records kiln results\npub struct Forge {\n pub kiln_slots: u32,\n pub glaze_batches: u32,\n pub firing_queues: u32,\n pub kiln_results: u32,\n}\n\nimpl Forge {\n pub fn assemble(&mut self, job: u32) -> bool {\n let left = 3;\n let mut total = 0;\n%s\n total > job\n }\n}\n" % longbody(" ")) +def py(root): + fillers(root, "assemble") + body = "\n".join(" total += step_value(left, %d) * weight_of(%d)" % (i, i) for i in range(90)) + w(root + "/src/forge.py", 'class Forge:\n """the forge: schedules kiln jobs, tracks firing queues and glaze batches, records kiln results"""\n\n kiln_slots = 0\n glaze_batches = 0\n firing_queues = 0\n\n def track_firing(self, job):\n return job\n\n def assemble(self, job):\n left = 3\n total = 0\n%s\n return total > job\n' % body) +def ts(root): + fillers(root, "assemble") + w(root + "/src/forge.ts", "// the forge: schedules kiln jobs, tracks firing queues and glaze batches, records kiln results\nexport class Forge {\n kilnSlots = 0;\n glazeBatches = 0;\n firingQueues = 0;\n trackFiring(job: number): number {\n return job;\n }\n assemble(job: number): boolean {\n const left = 3;\n let total = 0;\n%s\n return total > job;\n }\n}\n" % longbody(" ")) +def go(root): + fillers(root, "assemble") + w(root + "/src/forge.go", "package forge\n\n// Forge schedules kiln jobs, tracks firing queues and glaze batches, records kiln results\ntype Forge struct {\n\tKilnSlots int\n\tGlazeBatches int\n\tFiringQueues int\n\tKilnResults int\n}\n\nfunc stepValue(a, b int) int { return a + b }\nfunc weightOf(a int) int { return a }\n\nfunc (f *Forge) Assemble(job int) bool {\n\tleft := 3\n\ttotal := 0\n%s\n\treturn total > job\n}\n" % "\n".join("\ttotal += stepValue(left, %d) * weightOf(%d)" % (i, i) for i in range(90))) +cpp(out + "/cpp"); rs(out + "/rs"); py(out + "/py"); ts(out + "/ts"); go(out + "/go") +cpp(out + "/common", verb="Run"); deep(out + "/deep"); cpp(out + "/other", other="Smelter") +# EP1 namesake (super().refresh() is a by-name edge to a same-named owner) and EP2 recursion +w(out + "/name/src/screens.py", '"""Screens that repaint their regions."""\n\n\nclass Screen:\n """A full screen: repaint every region of the screen, then refresh the cursor."""\n\n def refresh(self):\n """Repaint the screen: every region of the screen, top to bottom, then the cursor."""\n self.painted = True\n return self\n\n\nclass ModalScreen(Screen):\n """A modal dialog drawn over another screen."""\n\n def refresh(self):\n """Repaint the dialog, then its parent."""\n super().refresh()\n self.dialog = True\n return self\n') +w(out + "/self/src/tree.py", '"""Tree helpers: normalize a parsed tree, flatten nested nodes."""\n\n\ndef normalize(tree):\n """Normalize the tree: strip blank labels and lower-case every tag."""\n tree["tag"] = tree.get("tag", "").lower()\n tree["label"] = tree.get("label", "").strip()\n return tree\n\n\ndef flatten(tree):\n """Flatten nested children into one list, depth first."""\n out = [tree]\n for child in tree.get("children", []):\n out.extend(flatten(child))\n return out\n') +# DS1-DS4: a declaration row (documented, so it outranks) and its definition (undocumented) +w(out + "/tsover/src/format.ts", '/** format renders the value: a number value is grouped for display */\nexport function format(value: number): string;\n/** format renders the value: a string value is trimmed for display */\nexport function format(value: string): string;\nexport function format(v: number | string): string {\n if (typeof v === "number") {\n return v.toLocaleString("en-US");\n }\n return v.trim();\n}\n') +w(out + "/cppvirt/src/dispatch.h", "struct Edge;\nclass Dispatcher\n{\npublic:\n virtual ~Dispatcher() {}\n /// dispatch: the pool dispatch of one ready edge to an idle pool worker; false when the pool has no idle worker\n virtual bool Dispatch( Edge* edge ) = 0;\n};\nclass PoolDispatcher : public Dispatcher\n{\npublic:\n /// dispatch: the pool dispatch of one ready edge to an idle pool worker\n bool Dispatch( Edge* edge ) override;\n int idle_ = 4;\n};\n") +w(out + "/cppvirt/src/dispatch.cc", '#include "dispatch.h"\nbool PoolDispatcher::Dispatch( Edge* e )\n{\n if( idle_ == 0 || e == nullptr )\n {\n return false;\n }\n --idle_;\n return true;\n}\n') +w(out + "/goiface/src/packer.go", "package packer\n\ntype Packer interface {\n\t// Pack: the tar packer packs the files into one archive, every file in order\n\tPack(files []string) error\n}\n\ntype tarStream struct{ count int }\n\nfunc (t *tarStream) Pack(xs []string) error {\n\tfor range xs {\n\t\tt.count++\n\t}\n\treturn nil\n}\n") +w(out + "/tsabs/src/printer.ts", 'export abstract class Base {\n /** print: the html printer prints a document as html */\n abstract print(doc: string): string;\n}\nexport class Out extends Base {\n print(d: string): string {\n return "

" + d + "

";\n }\n}\n') +PY +python3 "$TMP/gen.py" "$TMP/rt" || { echo "the runtime-root generator failed"; exit 2; } +rrun(){ local r="$1"; shift; "$BIN" "$TMP/rt/$r" --cache="$TMP/rt.$r.cache" "$@" 2>/dev/null; } # one cache per runtime root + +T_D="How does the JWT middleware verify a token?" +T_PY="How does Stylesheet apply the parsed rules to a widget?" +T_C="How does cmd_find_target resolve the session, window and pane of a command?" +T_SUB="How does tmux resolve which session, window and pane a command targets (the -t target)?" +T_BIG="How does render_screen draw the screen?" +T_HYD="How does hydrate restore the state?" +T_RS="How does the indexer walk the module tree breadth-first to index the crate source paths?" +T_CPP="How does the C++ builder build the plan and run its commands?" +T_GO="How does the packager package the files into an archive?" +T_DTS="How does negotiate pick the content type?" +T_DTS2="How does handshake upgrade the socket?" +T_TEST="How does roundtrip encode a token string?" +T_TWO="How does render produce the view markup?" +T_REF="How does refresh repaint a widget?" +T_STRUCT="How is the screen struct laid out?" +T_WHERE="Where does the JWT middleware verify a token?" +T_SHOW="show documentation of the JWT middleware token check" +T_NOCUE="JWT middleware token verification" + +# key|task — every question the gate asks, answered with the slot on and with RIPWIRE_NO_BODY_SLOT=1 +QUESTIONS="d|$T_D +py|$T_PY +c|$T_C +sub|$T_SUB +big|$T_BIG +hyd|$T_HYD +rs|$T_RS +cpp|$T_CPP +go|$T_GO +dts|$T_DTS +dts2|$T_DTS2 +test|$T_TEST +two|$T_TWO +ref|$T_REF +struct|$T_STRUCT +where|$T_WHERE +show|$T_SHOW +nocue|$T_NOCUE" +# fail fast: a binary that cannot answer the first question answers none of the ~90 below (binoverridecheck's sentinel) +run --for="$T_D" >"$TMP/probe.xml" +if ! grep -q ' on the fixture — every arm would be vacuous"; exit 1; fi +while IFS='|' read -r k q; do + run --for="$q" >"$TMP/$k.xml" + runoff --for="$q" >"$TMP/$k.off.xml" + for f in "$k" "$k.off"; do + if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(0) $f.xml: $r — the fixture answered nothing, every arm reading it would be vacuous"; fi + done +done <<<"$QUESTIONS" +run --for="$T_D" --legend=compact >"$TMP/d.c.xml" + +# ── D: the body slot in the default regime ───────────────────────────────────────────────────────────────── +if r="$( chk rootattr "$TMP/d.xml" bundle compact )"; then ok "(D1) T-D premise: $r"; else no "(D1) T-D premise: $r"; fi +if r="$( chk bodycount "$TMP/d.xml" 1 )"; then ok "(D1) T-D: exactly one body, root $r"; else no "(D1) T-D: $r"; fi +if r="$( chk body "$TMP/d.xml" jwt src/middleware/jwt/jwt.ts "getCookie" "HTTPException(401" "ctx.set('jwtPayload'" )"; then ok "(D1) T-D: $r holds the three kill strings"; else no "(D1) T-D: $r"; fi +if r="$( chk callsin "$TMP/d.xml" jwt verify )"; then ok "(D1) T-D: the body keeps (names verify)"; else no "(D1) T-D: $r"; fi +if r="$( chk rootattr "$TMP/d.xml" reason owner )"; then ok "(D1) T-D: root $r (bodies=1 is never read beside reason=compact-route)"; else no "(D1) T-D: $r"; fi +if r="$( chk bodiesattr "$TMP/d.xml" pick name )"; then ok "(D1) T-D: $r"; else no "(D1) T-D: $r"; fi +if r="$( chk nobodyof "$TMP/d.xml" verify )"; then ok "(D2) T-D: not verify's body (jwt is r1: a rank fact, not the entry-point discriminator)"; else no "(D2) T-D: $r"; fi +if r="$( chk estdelta "$TMP/d.xml" "$TMP/d.off.xml" )"; then ok "(D3) T-D: $r"; else no "(D3) T-D: $r"; fi +while IFS='|' read -r k q; do + if r="$( chk sectionsame "$TMP/$k.xml" "$TMP/$k.off.xml" )"; then ok "(D4) $k: sigs/hops/tail identical slot vs RIPWIRE_NO_BODY_SLOT=1 (not paid in rows)"; else no "(D4) $k: $r"; fi + if r="$( chk nobody "$TMP/$k.off.xml" )"; then ok "(D4) $k: RIPWIRE_NO_BODY_SLOT=1 serves no body"; else no "(D4) $k: RIPWIRE_NO_BODY_SLOT=1: $r"; fi +done <<<"$QUESTIONS" +if r="$( chk legend "$TMP/d.xml" 'bodies=1' )"; then ok "(D5) full legend explains bodies=1 (the owner body)"; else no "(D5) full legend: $r"; fi +if r="$( chk bodycount "$TMP/d.c.xml" 1 )"; then ok "(D5) compact dialect: the body rides too"; else no "(D5) compact dialect: $r"; fi +if r="$( chk legend "$TMP/d.c.xml" 'b t= n= p= l=' )"; then ok "(D5) compact legend defines b t= n= p= l= when a body rides"; else no "(D5) compact legend: $r"; fi +# R3 (FIX1, checklist 3): the compact reading says what each pick= means and what reason=owner does NOT mean — a pick=name +# body (d) and a pick=member body (Q1 C++) alike; red on 66cc4b63 ("ONE owner body, by name not resolved (pick=…)" only) +R3_STRS=( 'pick=name: its name is a question word' 'pick=member: a method so named of a ranked class' 'not necessarily the subject' 'not a claim it is the whole answer' ) +if r="$( chk legend "$TMP/d.c.xml" "${R3_STRS[@]}" )"; then ok "(D5/R3) pick=name body: the compact legend reads pick= and says what reason=owner does not mean"; else no "(D5/R3) pick=name body: $r"; fi +for k in where show nocue; do + if r="$( chk rootattr "$TMP/$k.xml" bodies 0 )"; then ok "(D6) $k (no explanatory cue): $r"; else no "(D6) $k: $r"; fi + if r="$( chk nobody "$TMP/$k.xml" )"; then ok "(D6) $k: no element"; else no "(D6) $k: $r"; fi +done + +# postures that never take the slot: identical with the slot on and off +run --for="$T_D" --signatures-only >"$TMP/p.sig.xml" +if r="$( chk nobody "$TMP/p.sig.xml" )" && r2="$( chk noroot "$TMP/p.sig.xml" bodies )"; then ok "(D7) --signatures-only: no body, no bodies="; else no "(D7) --signatures-only: ${r:-} ${r2:-}"; fi +run --for="$T_D" --json >"$TMP/p.json" +if python3 -c "import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if 'bodies' not in d and len(d.get('sigs',[]))>=1 else 1)" "$TMP/p.json"; then + ok "(D7) --json: no bodies key, sigs served (the JSON dialect carries no bodies: named floor)" +else + no "(D7) --json: a bodies key rides the JSON dialect (or no sigs)" +fi +for a in --auto-bodies --detail=1 --format=candidates; do + run --for="$T_D" "$a" >"$TMP/p.on.xml"; runoff --for="$T_D" "$a" >"$TMP/p.off.xml" + if [ -s "$TMP/p.on.xml" ] && cmp -s "$TMP/p.on.xml" "$TMP/p.off.xml"; then ok "(D7) $a: byte-identical with and without the slot"; else no "(D7) $a: differs from RIPWIRE_NO_BODY_SLOT=1 (or empty)"; fi +done +if r="$( chk rootattr "$TMP/p.on.xml" bodies '.*' 2>/dev/null )"; then no "(D7) --format=candidates carries bodies="; else ok "(D7) --format=candidates carries no bodies="; fi +run --for=verify >"$TMP/p.ne.xml"; runoff --for=verify >"$TMP/p.ne.off.xml" +if grep -q 'route="name-exact' "$TMP/p.ne.xml" && cmp -s "$TMP/p.ne.xml" "$TMP/p.ne.off.xml"; then ok "(D7) name-exact --for=verify: auto bundle, identical with and without the slot"; else no "(D7) name-exact: not name-exact, or differs from RIPWIRE_NO_BODY_SLOT=1"; fi + +# languages, owners, near misses +if r="$( chk rowis "$TMP/py.xml" 1 Stylesheet src/textual/css/stylesheet.py )"; then :; else no "(D8) premise: $r"; fi +if r="$( chk body "$TMP/py.xml" apply src/textual/css/stylesheet.py "_check_rule(" "widget.refresh()" )"; then ok "(D8) Python: $r (the method)"; else no "(D8) Python: $r"; fi +if r="$( chk nobodyof "$TMP/py.xml" Stylesheet )"; then ok "(D8) Python: the class Stylesheet (r1) is never the body"; else no "(D8) Python: $r"; fi +if r="$( chk body "$TMP/c.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" "cmd_find_valid_state(" )"; then ok "(D9) C: $r"; else no "(D9) C: $r"; fi +# D9b premise: the subtoken match the dropped fallback served IS a ranked row here — so "no slot" is the rule, not a miss +if grep -q ']*n="cmd_find_target"' "$TMP/sub.xml"; then :; else no "(D9b) premise: cmd_find_target is not a ranked row on T_SUB"; fi +if r="$( chk nobody "$TMP/sub.xml" )" && cmp -s "$TMP/sub.xml" "$TMP/sub.off.xml"; then ok "(D9b) only a subtoken match (cmd_find_target ~ 'target'): no slot, byte-identical to RIPWIRE_NO_BODY_SLOT=1"; else no "(D9b) only a subtoken match: a body rides or the answer differs from slot-off ${r:-}"; fi +if grep -q 'pick="subtoken"' "$TMP/sub.xml"; then no "(D9b) pick=\"subtoken\" still rides"; else ok "(D9b) no pick=\"subtoken\""; fi +if r="$( chk bodyattr "$TMP/big.xml" render_screen truncated '^1$' )"; then ok "(D10) oversized owner: $r"; else no "(D10) oversized: $r"; fi +if r="$( chk cutatline "$TMP/big.xml" render_screen 6000 )"; then ok "(D10) oversized owner: $r (whole lines, <= 6000 B)"; else no "(D10) oversized: $r"; fi +if nx="$( chk nextof "$TMP/big.xml" render_screen )" && printf '%s' "$nx" | grep -q '^--expand=src/c/render.c:'; then + ok "(D10) oversized owner: next=$nx" + hi="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o 'lines="[0-9]*-[0-9]*' | sed 's/.*-//' )" + l0="$( chk defline "$TMP/big.xml" render_screen )" # the def's first line (l= names the name's line, one below 'void') + if [ -n "$hi" ] && [ -n "$l0" ]; then + run "$nx" >"$TMP/big.next.xml" + if r="$( chk startsat "$TMP/big.next.xml" src/c/render.c $(( l0 + hi )) )"; then ok "(D10) the next= serves the rest, from source line $(( l0 + hi ))"; else no "(D10) next=: $r"; fi + else + no "(D10) could not read lines=/l= off the cut body" + fi +else + no "(D10) oversized: no (${nx:-none})" +fi +if r="$( chk over "$TMP/big.xml" )"; then ok "(D10) oversized owner: no over_ceiling on the root (the guard never fires for the body)"; else no "(D10) $r"; fi +if r="$( chk mbcut "$TMP/big.xml" render_screen 10 )"; then ok "(D10) the kept lines are multibyte: $r — a cap counted in characters would pass 6000 B"; else no "(D10) multibyte premise: $r"; fi +if r="$( chk bodyattr "$TMP/hyd.xml" hydrate over_ceiling '^1$' )"; then ok "(D11) one line over the cap: $r (served whole)"; else no "(D11) one line over the cap: $r"; fi +if r="$( chk utf8whole "$TMP/hyd.xml" hydrate 'restored: true' )"; then ok "(D11) one line over the cap: $r, the whole line"; else no "(D11) $r"; fi +if r="$( chk rowis "$TMP/rs.xml" 1 walk src/rs/indexer.rs )"; then :; else no "(L1) premise: $r"; fi +if r="$( chk body "$TMP/rs.xml" index src/rs/indexer.rs "let order = walk(" )"; then ok "(L1) Rust: $r — the entry point (index calls walk) beats walk at r1"; else no "(L1) Rust: $r"; fi +if r="$( chk nobodyof "$TMP/rs.xml" walk )"; then ok "(L1) Rust: not walk's body"; else no "(L1) Rust: $r"; fi +if r="$( chk rowis "$TMP/cpp.xml" 1 Build src/cpp/build.h )"; then :; else no "(L2) premise: $r"; fi +if r="$( chk body "$TMP/cpp.xml" Build src/cpp/build.cc "cannot make progress" )"; then ok "(L2) C++: $r — the out-of-line definition, not the r1 header declaration"; else no "(L2) C++: $r"; fi +if r="$( chk nobodyat "$TMP/cpp.xml" '\.h$' )" && r2="$( chk nobodyof "$TMP/cpp.xml" Builder )"; then ok "(L2) C++: no header declaration, no class body"; else no "(L2) C++: ${r:-} ${r2:-}"; fi +if r="$( chk body "$TMP/go.xml" Package src/go/packager.go "guessing packager from target file extension" )"; then ok "(L3) Go: $r (the method)"; else no "(L3) Go: $r"; fi +if r="$( chk nobodyof "$TMP/go.xml" Packager )"; then ok "(L3) Go: the type Packager is never the body"; else no "(L3) Go: $r"; fi +if r="$( chk rowis "$TMP/dts.xml" 1 negotiate src/types/global.d.ts )"; then :; else no "(N1) premise: $r"; fi +if r="$( chk body "$TMP/dts.xml" negotiate src/negotiate.ts "negotiate fallback" )"; then ok "(N1) TS: $r — the definition, not the r1 .d.ts declaration"; else no "(N1) TS: $r"; fi +for k in dts dts2; do + if r="$( chk nobodyat "$TMP/$k.xml" '\.d\.ts$' )"; then ok "(N1) $k: no body from a .d.ts declaration"; else no "(N1) $k: $r"; fi +done +if r="$( chk nobody "$TMP/test.xml" )" && cmp -s "$TMP/test.xml" "$TMP/test.off.xml"; then ok "(N2) the only name owner is a tests/ helper: no slot, byte-identical to RIPWIRE_NO_BODY_SLOT=1"; else no "(N2) a body rides, or the answer differs from slot-off ${r:-}"; fi +if r="$( chk nobodyat "$TMP/test.xml" '(^|/)tests?/' )" && r2="$( chk nobodyof "$TMP/test.xml" roundtrip )"; then ok "(N2) no body from tests/ (roundtrip is a test symbol)"; else no "(N2) ${r:-} ${r2:-}"; fi +if r="$( chk bodycount "$TMP/two.xml" 1 )"; then ok "(N3) two equal owners: one body"; else no "(N3) $r"; fi +if r="$( chk bodyisrow "$TMP/two.xml" render )"; then ok "(N3) two equal owners: $r (the better-ranked)"; else no "(N3) $r"; fi +run --for="$T_TWO" >"$TMP/two2.xml" +if cmp -s "$TMP/two.xml" "$TMP/two2.xml"; then ok "(N3) two equal owners: deterministic x2"; else no "(N3) two equal owners: two runs differ"; fi +if r="$( chk bodycount "$TMP/ref.xml" 1 )"; then ok "(N4) two refresh methods in one file: one body"; else no "(N4) $r"; fi +if r="$( chk bodyisrow "$TMP/ref.xml" refresh )"; then ok "(N4) $r (entity identity: the ranked row's own definition)"; else no "(N4) $r"; fi +if r="$( chk nobodyof "$TMP/struct.xml" screen )"; then ok "(N5) the struct screen (r1) is never the body"; else no "(N5) $r"; fi +while IFS='|' read -r k q; do + if r="$( chk bodykinds "$TMP/$k.xml" )"; then :; else no "(N5) $k: $r"; fi +done <<<"$QUESTIONS" +ok "(N5) every on the fixture answers is fn/method (FAIL lines above otherwise)" + +# ── EP / DS / Q1: the runtime roots (each premise read off this binary's own answer, never assumed) ───────────── +Q_FORGE="How does the forge assemble a kiln job: scheduling glaze batches, tracking firing queues and recording kiln results?" +Q_RUN="How does the forge run a kiln job: scheduling glaze batches, tracking firing queues and recording kiln results?" +for r in cpp rs py ts go deep other; do rrun "$r" --for="$Q_FORGE" >"$TMP/q1.$r.xml"; done +rrun common --for="$Q_RUN" >"$TMP/q1.common.xml" +rrun cpp --for="$Q_FORGE" --legend=compact >"$TMP/q1.cpp.c.xml" +if r="$( chk bodiesattr "$TMP/q1.cpp.c.xml" pick member )" && r2="$( chk legend "$TMP/q1.cpp.c.xml" "${R3_STRS[@]}" )"; then + ok "(D5/R3) pick=member body (C++): the compact legend reads pick= and says what reason=owner does not mean" +else no "(D5/R3) pick=member body (C++): ${r:-} ${r2:-}"; fi +rrun name --for="How does refresh repaint the screen?" >"$TMP/ep.name.xml" +rrun self --for="How does normalize flatten the tree?" >"$TMP/ep.self.xml" +rrun tsover --for="How does format render the value?" >"$TMP/ds.tsover.xml" +rrun cppvirt --for="How does the pool dispatch an edge?" >"$TMP/ds.cppvirt.xml" +rrun goiface --for="How does the tar packer pack the files?" >"$TMP/ds.goiface.xml" +rrun tsabs --for="How does the html printer print a document?" >"$TMP/ds.tsabs.xml" +for f in q1.cpp q1.rs q1.py q1.ts q1.go q1.deep q1.other q1.common ep.name ep.self ds.tsover ds.cppvirt ds.goiface ds.tsabs; do + if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(0) $f.xml: $r — the runtime root answered nothing"; fi +done +# EP1 namesake: ModalScreen.refresh's only edge is super().refresh() — a same-named owner, never an entry point +if r="$( chk rowis "$TMP/ep.name.xml" 3 refresh src/screens.py )" && [ "$( chk rankof "$TMP/ep.name.xml" refresh src/screens.py )" = 3 ] \ + && "$BIN" "$TMP/rt/name" --no-cache --callees=ModalScreen.refresh 2>/dev/null | grep -q ''; then + if r="$( chk bodyat "$TMP/ep.name.xml" refresh src/screens.py 7 )"; then ok "(EP1) namesake: $r — the r3 row; r4's super().refresh() edge names the same word"; else no "(EP1) namesake: $r"; fi +else + no "(EP1) premise: r3 Screen.refresh (l=7) and ModalScreen.refresh's edge to it — ${r:-not read}" +fi +# EP2 recursion: flatten (r2) calls only itself (the graph carries no self-edge: --callees count=0), normalize (r1) calls nothing +if r="$( chk rowis "$TMP/ep.self.xml" 1 normalize src/tree.py )" && r2="$( chk rowis "$TMP/ep.self.xml" 2 flatten src/tree.py )"; then + if r="$( chk bodyat "$TMP/ep.self.xml" normalize src/tree.py 4 )"; then ok "(EP2) recursion: $r — flatten's recursion makes no entry point"; else no "(EP2) recursion: $r"; fi +else + no "(EP2) premise: ${r:-} ${r2:-}" +fi +# DS1-DS4: the declaration outranks its definition; the body is the definition's +ds(){ # ds KEY ARM DECL_NAME DECL_PATH DECL_RANK DEF_PATH DEF_LINE + local k="$1" arm="$2" n="$3" p="$4" dr="$5" dp="$6" dl="$7" + if ! r="$( chk rowis "$TMP/ds.$k.xml" "$dr" "$n" "$p" )"; then no "($arm) premise: $r"; return; fi + if [ "$( chk rowline "$TMP/ds.$k.xml" "$dr" )" = "$dl" ] && [ "$p" = "$dp" ]; then no "($arm) premise: the r$dr row IS the definition (l=$dl): no declaration outranks it"; return; fi + if r="$( chk bodyat "$TMP/ds.$k.xml" "$n" "$dp" "$dl" )"; then ok "($arm) $k: $r — the definition, not the r$dr declaration"; else no "($arm) $k: $r"; fi +} +ds tsover DS1 format src/format.ts 1 src/format.ts 5 +ds cppvirt DS2 Dispatch src/dispatch.h 1 src/dispatch.cc 2 +ds goiface DS3 Pack src/packer.go 1 src/packer.go 10 +ds tsabs DS4 print src/printer.ts 1 src/printer.ts 6 +# Q1 member owner: the head-tier class's member, defined past the head tier (no name owner exists) +q1(){ # q1 KEY CLASS_RANK_MAX CLASS CLASS_PATH MEMBER MEMBER_PATH MEMBER_LINE LANG + local k="$1" cmax="$2" c="$3" cp="$4" m="$5" mp="$6" ml="$7" lang="$8" cr mr + cr="$( chk rankof "$TMP/q1.$k.xml" "$c" "$cp" )"; mr="$( chk rankof "$TMP/q1.$k.xml" "$m" "$mp" )" + case "$cr$mr" in *[!0-9]*|'') no "(Q1) $k premise: unreadable ranks class=$cr member=$mr"; return;; esac + if [ "$cr" -lt 1 ] || [ "$cr" -gt "$cmax" ] || { [ "$mr" -ge 1 ] && [ "$mr" -le 24 ]; }; then + no "(Q1) $k premise: class $c r$cr (wanted 1..$cmax), member $m r$mr (wanted past the head tier)"; return + fi + if r="$( chk bodyat "$TMP/q1.$k.xml" "$m" "$mp" "$ml" )" && r2="$( chk bodiesattr "$TMP/q1.$k.xml" pick member )"; then + ok "(Q1) $lang: $r ($r2; class r$cr, member r${mr/#0/>40})" + else + no "(Q1) $lang: ${r:-} ${r2:-}" + fi + if r="$( chk nobodyof "$TMP/q1.$k.xml" "$c" )"; then :; else no "(Q1) $lang: the class/type is served: $r"; fi +} +q1 cpp 24 Forge src/forge.h Assemble src/forge.cc 4 "C++ out of line (sc=)" +q1 rs 24 Forge src/forge.rs assemble src/forge.rs 10 "Rust impl (sc=)" +q1 py 24 Forge src/forge.py assemble src/forge.py 11 "Python (sc=)" +q1 ts 24 Forge src/forge.ts assemble src/forge.ts 9 "TS class (span)" +# the Go floor: the type ranks, its receiver method does not and carries no scope — no member body (flips when Go has a receiver scope) +gr="$( chk rankof "$TMP/q1.go.xml" Forge src/forge.go )"; gm="$( chk rankof "$TMP/q1.go.xml" Assemble src/forge.go )" +if [ "$gr" = 1 ] && { [ "$gm" = 0 ] || [ "$gm" -gt 24 ]; }; then + if r="$( chk nobodyof "$TMP/q1.go.xml" Assemble )" && r2="$( chk nobodyof "$TMP/q1.go.xml" Forge )"; then ok "(Q1) Go floor: type Forge r1, receiver Assemble r$gm: no member body, never the type (the named floor)"; else no "(Q1) Go floor: ${r:-} ${r2:-}"; fi +else + no "(Q1) Go floor premise: Forge r$gr Assemble r$gm" +fi +# near misses +cr="$( chk rankof "$TMP/q1.common.xml" Forge src/forge.h )"; mr="$( chk rankof "$TMP/q1.common.xml" Run src/forge.cc )" +if [ "$cr" = 1 ] && [ "$mr" -gt 24 ] 2>/dev/null; then + if r="$( chk nobodyof "$TMP/q1.common.xml" Run )"; then ok "(Q1) near miss: a common-word member (Run, 'run') is never the member owner"; else no "(Q1) common-word member: $r"; fi +else + no "(Q1) common-word premise: Forge r$cr Run r$mr" +fi +cr="$( chk rankof "$TMP/q1.deep.xml" Forge src/forge.h )" +if [ "$cr" -gt 24 ] 2>/dev/null; then + if r="$( chk nobodyof "$TMP/q1.deep.xml" Assemble )"; then ok "(Q1) near miss: class Forge r$cr (in the pool, past the head tier) donates no member"; else no "(Q1) class past the head tier: $r"; fi +else + no "(Q1) deep premise: Forge r$cr, wanted 25..40" +fi +sr="$( chk rankof "$TMP/q1.other.xml" Assemble src/smelter.cc )"; fr="$( chk rankof "$TMP/q1.other.xml" Assemble src/forge.cc )" +if [ "$sr" -gt 24 ] 2>/dev/null && [ "$fr" -gt "$sr" ] 2>/dev/null && [ "$( chk rankof "$TMP/q1.other.xml" Forge src/forge.h )" = 1 ]; then + if r="$( chk bodyat "$TMP/q1.other.xml" Assemble src/forge.cc 4 )"; then ok "(Q1) entity identity: $r — Forge's member, not Smelter::Assemble (r$sr, outranks it at r$fr)"; else no "(Q1) entity identity: $r"; fi +else + no "(Q1) other-class premise: Smelter::Assemble r$sr, Forge::Assemble r$fr" +fi + +# ── E: explicit targets (reserve-sections-first) ─────────────────────────────────────────────────────────── +run --for="$T_D" --token-budget=5000 >"$TMP/e5000.xml" +if r="$( chk body "$TMP/e5000.xml" jwt src/middleware/jwt/jwt.ts "getCookie" )"; then ok "(E1) --token-budget=5000: $r"; else no "(E1) --token-budget=5000: $r"; fi +if r="$( chk prefix "$TMP/e5000.xml" "$TMP/d.xml" )"; then ok "(E1) --token-budget=5000: $r"; else no "(E1) --token-budget=5000: $r"; fi +if r="$( chk over "$TMP/e5000.xml" )"; then ok "(E1) --token-budget=5000: no over_ceiling"; else no "(E1) $r"; fi +run --for="$T_D" --token-budget=400 >"$TMP/e400.xml" +if r="$( chk rootattr "$TMP/e400.xml" bodies 0 )" && r2="$( chk rootattr "$TMP/e400.xml" reason budget )"; then ok "(E2) --token-budget=400: bodies=0 reason=budget"; else no "(E2) --token-budget=400: ${r:-} ${r2:-}"; fi +if r="$( chk bodiesnext "$TMP/e400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(E2) --token-budget=400: the skipped slot discloses itself, recoverable: $r"; else no "(E2) --token-budget=400: $r"; fi +if r="$( chk prefix "$TMP/e400.xml" "$TMP/d.xml" )"; then ok "(E2) --token-budget=400: $r"; else no "(E2) --token-budget=400: $r"; fi +# E2b (CodeRabbit, #413): the skipped slot's next= is built by nextFlag, so a path a shell would split pastes as ONE argument +rm -rf "$TMP/spc"; cp -R "$FIX" "$TMP/spc" && mv "$TMP/spc/src/middleware/jwt" "$TMP/spc/src/middleware/j wt" +"$BIN" "$TMP/spc" --no-cache --for="$T_D" --token-budget=400 >"$TMP/e400spc.xml" 2>/dev/null +if r="$( chk bodiesnext "$TMP/e400spc.xml" "^--expand='src/middleware/j wt/jwt\.ts:[0-9]+:jwt'\$" )" \ + && [ "$( python3 -c 'import html,re,shlex,sys; m=re.search(r"]*\snext=\"([^\"]*)\"", open(sys.argv[1]).read()); print(len(shlex.split(html.unescape(m.group(1)))) if m else 0)' "$TMP/e400spc.xml" )" = 1 ]; then + ok "(E2b) a path with a space: the skipped slot's next= is quoted and splits to one argument: $r" +else no "(E2b) a path with a space: ${r:-the next= splits into more than one argument}"; fi +prev_rows=0; prev_b=0; mono=1; nflag="" +for n in 300 500 800 1200 2000 3000 5000 8000 1000000; do + run --for="$T_D" --token-budget=$n >"$TMP/s$n.xml"; runoff --for="$T_D" --token-budget=$n >"$TMP/s$n.off.xml" + rws="$( chk nrows "$TMP/s$n.xml" )"; rwo="$( chk nrows "$TMP/s$n.off.xml" )"; bb="$( chk bodybytes "$TMP/s$n.xml" )" + case "$rws$rwo$bb" in *[!0-9]*|'') no "(E3) N=$n: unreadable counts rows=$rws off=$rwo body=$bb"; mono=0; continue;; esac + if ! chk prefix "$TMP/s$n.xml" "$TMP/d.xml" >/dev/null; then no "(E3) N=$n: rows are not a rank prefix of the default rows"; mono=0; fi + if ! chk over "$TMP/s$n.xml" >/dev/null; then no "(E3) N=$n: over_ceiling on the root"; mono=0; fi + [ "$rws" -lt "$prev_rows" ] && { no "(E3) N=$n: rows $rws < $prev_rows at the smaller N"; mono=0; } + [ "$bb" -lt "$prev_b" ] && { no "(E3) N=$n: body $bb B < $prev_b B at the smaller N"; mono=0; } + [ "$rws" -gt "$rwo" ] && { no "(E3) N=$n: rows $rws > slot-off $rwo (the body is reserved first, it never adds rows)"; mono=0; } + prev_rows=$rws; prev_b=$bb; nflag="$nflag $n:$rws/$rwo/${bb}B" +done +if [ "$mono" = 1 ]; then ok "(E3) sweep rows(slot)/rows(off)/body:$nflag — rank prefix, monotone, <= slot-off, no over_ceiling"; fi +cdw=0; cdd=0 +chk cdata "$TMP/s1000000.xml" jwt >"$TMP/cd.wide" 2>/dev/null || cdw=1 +chk cdata "$TMP/d.xml" jwt >"$TMP/cd.default" 2>/dev/null || cdd=1 +if [ "$cdw$cdd" = "00" ] && [ -s "$TMP/cd.wide" ] && cmp -s "$TMP/cd.wide" "$TMP/cd.default"; then ok "(E3) N=1000000: the body equals the default answer's body"; else no "(E3) N=1000000: no body, or it differs from the default answer's"; fi + +# E4: T_REF at N=800 — slot-off serves rows past the cut there and the share the rows at the cut leave holds the small body: +# the body is reserved first and the rows give way (review R3). (T_D's 2.7 KB jwt body is skipped below N=4800: its own arm, E2/E3.) +run --for="$T_REF" --token-budget=800 >"$TMP/e4.xml"; runoff --for="$T_REF" --token-budget=800 >"$TMP/e4.off.xml" +if r="$( chk paidinside "$TMP/e4.xml" "$TMP/e4.off.xml" 800 )"; then ok "(E4) --token-budget=800: $r"; else no "(E4) --token-budget=800: $r"; fi + +# ── MC: MCP for parity ───────────────────────────────────────────────────────────────────────────────────── +mcpfor "$T_D" >"$TMP/m.d.xml"; mcpfor "$T_NOCUE" >"$TMP/m.nocue.xml"; mcpfor "$T_D" '{"budget_tokens":400}' >"$TMP/m.d400.xml" +for f in m.d m.nocue m.d400; do + if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(MC0) MCP $f answered nothing: $r"; fi +done +if r="$( chk body "$TMP/m.d.xml" jwt src/middleware/jwt/jwt.ts "getCookie" "HTTPException(401" "ctx.set('jwtPayload'" )"; then ok "(MC1) MCP for: $r"; else no "(MC1) MCP for: $r"; fi +cdm=0; chk cdata "$TMP/m.d.xml" jwt >"$TMP/cd.mcp" 2>/dev/null || cdm=1 +if [ "$cdm$cdd" = "00" ] && [ -s "$TMP/cd.mcp" ] && cmp -s "$TMP/cd.mcp" "$TMP/cd.default"; then ok "(MC2) MCP for: the body equals the CLI body byte for byte"; else no "(MC2) MCP for: no body, or it differs from the CLI's"; fi +if r="$( chk nobody "$TMP/m.nocue.xml" )"; then ok "(MC3) MCP for, no cue: no body"; else no "(MC3) $r"; fi +if r="$( chk nobodyof "$TMP/m.d400.xml" jwt )" && r2="$( chk bodiesnext "$TMP/m.d400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(MC4) MCP budget_tokens=400: no body, the owner recoverable by next=--expand"; else no "(MC4) MCP budget_tokens=400: ${r:-} ${r2:-}"; fi + +# MC5: MCP/CLI drift on the cut and on the pick (review R4) +mcpfor "$T_BIG" >"$TMP/m.big.xml"; mcpfor "$T_RS" >"$TMP/m.rs.xml" +for f in m.big m.rs; do if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(MC0) MCP $f answered nothing: $r"; fi; done +mc5=0 +for a in truncated lines next; do + vc="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o " $a=\"[^\"]*\"" )"; vm="$( grep -o ']*n="render_screen"[^>]*' "$TMP/m.big.xml" | grep -o " $a=\"[^\"]*\"" )" + if [ -z "$vc" ] || [ "$vc" != "$vm" ]; then no "(MC5) MCP cut: vs CLI "; mc5=1; fi +done +cbc=0; cbm=0 +chk cdata "$TMP/big.xml" render_screen >"$TMP/cd.big.cli" 2>/dev/null || cbc=1 +chk cdata "$TMP/m.big.xml" render_screen >"$TMP/cd.big.mcp" 2>/dev/null || cbm=1 +if [ "$cbc$cbm" = "00" ] && [ -s "$TMP/cd.big.cli" ] && cmp -s "$TMP/cd.big.cli" "$TMP/cd.big.mcp"; then :; else no "(MC5) MCP cut body differs from the CLI's (or is absent)"; mc5=1; fi +[ "$mc5" = 0 ] && ok "(MC5) MCP T_BIG: the same head cut as the CLI (truncated=/lines=/next= and CDATA)" +r=""; r2="" +if r="$( chk body "$TMP/m.rs.xml" index src/rs/indexer.rs "let order = walk(" )" && r2="$( chk nobodyof "$TMP/m.rs.xml" walk )"; then ok "(MC5) MCP T_RS: $r — the entry point, as the CLI"; else no "(MC5) MCP T_RS: ${r:-} ${r2:-}"; fi +# E4 on MCP: budget_tokens=800 pays the body inside the target too +mcpfor "$T_REF" '{"budget_tokens":800}' >"$TMP/m.e4.xml"; mcpforoff "$T_REF" '{"budget_tokens":800}' >"$TMP/m.e4.off.xml" +if r="$( chk paidinside "$TMP/m.e4.xml" "$TMP/m.e4.off.xml" 800 )"; then ok "(E4) MCP budget_tokens=800: $r"; else no "(E4) MCP budget_tokens=800: $r"; fi +# EV1: the owner-hop env never reaches the slot (review R6: the shared owner helper carries no early return) +RIPWIRE_NO_OWNER_HOP=1 "$BIN" "$FIX" --cache="$TMP/rw.cache" --for="$T_D" 2>/dev/null >"$TMP/d.noown.xml" +bs1=0; bs2=0 +chk bodiessec "$TMP/d.xml" >"$TMP/bs.default" 2>/dev/null || bs1=1 +chk bodiessec "$TMP/d.noown.xml" >"$TMP/bs.noown" 2>/dev/null || bs2=1 +if [ "$bs1$bs2" = "00" ] && cmp -s "$TMP/bs.default" "$TMP/bs.noown"; then ok "(EV1) RIPWIRE_NO_OWNER_HOP=1: byte-identical to the default"; else no "(EV1) RIPWIRE_NO_OWNER_HOP=1 changed or dropped the element"; fi + +# ── M: mutation controls — the checkers reject canned pre-change shapes ───────────────────────────────────── +printf '%s' 'x' >"$TMP/c1.xml" +if chk body "$TMP/c1.xml" jwt src/middleware/jwt/jwt.ts getCookie >/dev/null 2>&1; then no "(M1) the body checker accepted a canned no-body answer"; else ok "(M1) the body checker rejects a canned no-body answer"; fi +printf '%s' 'x' >"$TMP/c2.xml" +if chk nobodyat "$TMP/c2.xml" '(^|/)tests?/' >/dev/null 2>&1; then no "(M2) the tests/ checker accepted a canned tests/ body"; else ok "(M2) the tests/ checker rejects a canned tests/ body"; fi +printf '%s' 'x' >"$TMP/c3.xml" +if chk bodyattr "$TMP/c3.xml" render_screen truncated '^1$' >/dev/null 2>&1 || chk nextof "$TMP/c3.xml" render_screen >/dev/null 2>&1; then no "(M3) the cut checkers accepted a silent partial body"; else ok "(M3) the cut checkers reject a partial body with no truncated=/next="; fi +sed 's/"$TMP/c4.xml" +if grep -q '/dev/null 2>&1; then no "(M4) the section checker missed a changed "; else ok "(M4) the section checker sees a changed "; fi +else + no "(M4) premise: T-D has no element to mutate" +fi + +# M5: a body appended AFTER the row cut (rows unchanged, est_tokens grown by the body) is rejected by the E4 checker +python3 - "$TMP/e4.off.xml" "$TMP/c5.xml" <<'PY2' +import re, sys +t = open(sys.argv[1], encoding="utf-8", errors="replace").read() +body = '' +t = t.replace("", body + "") +t = re.sub(r'est_tokens="(\d+)"', lambda m: 'est_tokens="%d"' % (int(m.group(1)) + 790), t, count=1) +open(sys.argv[2], "w", encoding="utf-8").write(t) +PY2 +if chk paidinside "$TMP/c5.xml" "$TMP/e4.off.xml" 800 >/dev/null 2>&1; then no "(M5) the E4 checker accepted a body appended after the row cut"; else ok "(M5) the E4 checker rejects a body appended after the row cut"; fi + +# ── X: hygiene ────────────────────────────────────────────────────────────────────────────────────────────── +run --for="$T_D" >"$TMP/d2.xml" +if cmp -s "$TMP/d.xml" "$TMP/d2.xml"; then ok "(X1) T-D deterministic x2"; else no "(X1) T-D differs between two runs"; fi +if command -v xmllint >/dev/null 2>&1; then + for f in d d.c q1.cpp.c big hyd py rs cpp go m.d e400; do + if xmllint --noout "$TMP/$f.xml" 2>/dev/null; then ok "(X2) $f.xml is well-formed XML"; else no "(X2) $f.xml failed xmllint"; fi + done +else + skip "(X2) xmllint not installed — well-formedness not checked here" +fi +for f in d d.c big hyd e400; do + if r="$( chk nodashes "$TMP/$f.xml" )"; then ok "(X3) $f.xml: no '--' inside an XML comment"; else no "(X3) $f.xml: $r"; fi +done + +# ── I: optional byte identity against the pre-change binary where the slot must not fire ───────────────────── +if [ -n "${RIPWIRE_BASE_BIN:-}" ] && [ -x "${RIPWIRE_BASE_BIN}" ]; then + RIPWIRE_NO_BODY_SLOT=1 "$BIN" "$FIX" --no-cache --for="$T_D" 2>/dev/null | strip_at >"$TMP/i.head" + "$RIPWIRE_BASE_BIN" "$FIX" --no-cache --for="$T_D" 2>/dev/null | strip_at >"$TMP/i.base" + if [ -s "$TMP/i.base" ] && cmp -s "$TMP/i.base" "$TMP/i.head"; then ok "(I0) RIPWIRE_NO_BODY_SLOT=1 T-D == the base binary byte for byte"; else no "(I0) RIPWIRE_NO_BODY_SLOT=1 T-D differs from the base binary"; fi + i=0 + while IFS= read -r line; do # one argv per line, arguments separated by '|' (values may hold spaces) + i=$(( i + 1 )) + IFS='|' read -r -a argv <<<"$line" + "$RIPWIRE_BASE_BIN" "$FIX" --no-cache "${argv[@]}" 2>/dev/null | strip_at >"$TMP/i.base" + "$BIN" "$FIX" --no-cache "${argv[@]}" 2>/dev/null | strip_at >"$TMP/i.head" + if [ ! -s "$TMP/i.base" ]; then no "(I$i) the base binary answered nothing for: $line"; continue; fi + if cmp -s "$TMP/i.base" "$TMP/i.head"; then ok "(I$i) byte-identical: $line"; else no "(I$i) differs from the base binary: $line"; fi + done <<'ARGV' +--for=JWT middleware token verification +--for=Where does the JWT middleware verify a token? +--for=How does the JWT middleware verify a token?|--signatures-only +--for=How does the JWT middleware verify a token?|--json +--for=How does the JWT middleware verify a token?|--auto-bodies +--for=How does the JWT middleware verify a token?|--detail=1 +--for=verify +--callers=verify +--expand=src/middleware/jwt/jwt.ts:jwt +ARGV + RIPWIRE_NO_BODY_SLOT=1 "$BIN" "$FIX" --no-cache --for="$T_D" --token-budget=3000 2>/dev/null | strip_at >"$TMP/i.head" + "$RIPWIRE_BASE_BIN" "$FIX" --no-cache --for="$T_D" --token-budget=3000 2>/dev/null | strip_at >"$TMP/i.base" + if [ -s "$TMP/i.base" ] && cmp -s "$TMP/i.base" "$TMP/i.head"; then ok "(I10) --token-budget=3000 with RIPWIRE_NO_BODY_SLOT=1 == the base binary (no reserve without the slot)"; else no "(I10) --token-budget=3000 RIPWIRE_NO_BODY_SLOT=1 differs from the base binary"; fi +else + skip "(I) RIPWIRE_BASE_BIN unset or not executable — base-binary byte identity not run here" +fi + +exit $fail diff --git a/test/forbodyslotfix/src/c/cmd-find.c b/test/forbodyslotfix/src/c/cmd-find.c new file mode 100644 index 000000000..f8864b4e3 --- /dev/null +++ b/test/forbodyslotfix/src/c/cmd-find.c @@ -0,0 +1,112 @@ +#include +#include + +struct session { const char *name; }; +struct window { int id; }; +struct window_pane { int id; }; +struct client { struct session *session; }; + +struct cmd_find_state { + struct session *s; + struct window *w; + struct window_pane *wp; + int flags; +}; + +/* The current client of the command queue, used when no -t target is given. */ +static struct client * +cmd_find_current_client(struct cmd_find_state *fs) +{ + (void)fs; + return NULL; +} + +/* Find the session a target names. */ +static int +cmd_find_get_session(struct cmd_find_state *fs, const char *session) +{ + if (session == NULL || *session == '\0') + return (-1); + fs->s = NULL; + return (0); +} + +/* Find the window a target names, inside the session already found. */ +static int +cmd_find_get_window(struct cmd_find_state *fs, const char *window) +{ + if (window == NULL) + return (-1); + fs->w = NULL; + return (0); +} + +/* Find the pane a target names, inside the window already found. */ +static int +cmd_find_get_pane(struct cmd_find_state *fs, const char *pane) +{ + if (pane == NULL) + return (-1); + fs->wp = NULL; + return (0); +} + +/* Is the state complete: session, window and pane all resolved? */ +static int +cmd_find_valid_state(struct cmd_find_state *fs) +{ + return (fs->s != NULL && fs->w != NULL && fs->wp != NULL); +} + +/* Fill in the state from the current client. */ +static int +cmd_find_from_client(struct cmd_find_state *fs, struct client *c) +{ + if (c == NULL || c->session == NULL) + return (-1); + fs->s = c->session; + return (0); +} + +/* + * Resolve the -t target of a command into a session, window and pane: split + * the target on ':' and '.', then look each part up in turn. + */ +int +cmd_find_target(struct cmd_find_state *fs, const char *target, int flags) +{ + struct client *c; + char *copy, *colon, *period; + const char *session, *window, *pane; + + memset(fs, 0, sizeof *fs); + fs->flags = flags; + c = cmd_find_current_client(fs); + if (target == NULL || *target == '\0') { + if (cmd_find_from_client(fs, c) != 0) + return (-1); + return (cmd_find_valid_state(fs) ? 0 : -1); + } + copy = strdup(target); + colon = strchr(copy, ':'); + if (colon != NULL) + *colon++ = '\0'; + period = strchr(colon != NULL ? colon : copy, '.'); + if (period != NULL) + *period++ = '\0'; + session = copy; + window = colon; + pane = period; + if (cmd_find_get_session(fs, session) != 0) + goto error; + if (window != NULL && cmd_find_get_window(fs, window) != 0) + goto error; + if (pane != NULL && cmd_find_get_pane(fs, pane) != 0) + goto error; + free(copy); + return (cmd_find_valid_state(fs) ? 0 : -1); + +error: + free(copy); + return (-1); +} diff --git a/test/forbodyslotfix/src/c/render.c b/test/forbodyslotfix/src/c/render.c new file mode 100644 index 000000000..1d5117f72 --- /dev/null +++ b/test/forbodyslotfix/src/c/render.c @@ -0,0 +1,181 @@ +#include + +struct screen { char cells[64][256]; int rows; int cols; }; + +/* Draw the whole screen: clear every row, then paint each cell of each row in turn. */ +void +render_screen(struct screen *s) +{ + int row, col; + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 1 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 2 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 3 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 4 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 5 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 6 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 7 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 8 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 9 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 10 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 11 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 12 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 13 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 14 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 15 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 16 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 17 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 18 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 19 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 20 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 21 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 22 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 23 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 24 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 25 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 26 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 27 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 28 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 29 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 30 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 31 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 32 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 33 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 34 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 35 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 36 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 37 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 38 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 39 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 40 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 41 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 42 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 43 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 44 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 45 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 46 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 47 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 48 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 49 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 50 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 51 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 52 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 53 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 54 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 55 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 56 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 57 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 58 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 59 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 60 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 61 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 62 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 63 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 64 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 65 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 66 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 67 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 68 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 69 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 70 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 71 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 72 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 73 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 74 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 75 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 76 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 77 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 78 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 79 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 80 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 81 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 82 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 83 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 84 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 85 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 86 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 87 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 88 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 89 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 90 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 91 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 92 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 93 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 94 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 95 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 96 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 97 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 98 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 99 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 100 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 101 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 102 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 103 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 104 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 105 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 106 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 107 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 108 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 109 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 110 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 111 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 112 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 113 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 114 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 115 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 116 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 117 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 118 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 119 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 120 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 121 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 122 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 123 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 124 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 125 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 126 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 127 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 128 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 129 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 130 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 131 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 132 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 133 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 134 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 135 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 136 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 137 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 138 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 139 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 140 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 141 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 142 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 143 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 144 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 145 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 146 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 147 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 148 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 149 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 150 + col) % 90)); } } + memset(s->cells[0], 32, sizeof s->cells[0]); +} diff --git a/test/forbodyslotfix/src/context.ts b/test/forbodyslotfix/src/context.ts new file mode 100644 index 000000000..3041d029f --- /dev/null +++ b/test/forbodyslotfix/src/context.ts @@ -0,0 +1,18 @@ +export interface HonoRequest { + url: string + header(name: string): string | undefined +} + +export class Context { + req: HonoRequest + #var: Map = new Map() + constructor(req: HonoRequest) { + this.req = req + } + set(key: string, value: unknown): void { + this.#var.set(key, value) + } + get(key: string): unknown { + return this.#var.get(key) + } +} diff --git a/test/forbodyslotfix/src/cpp/build.cc b/test/forbodyslotfix/src/cpp/build.cc new file mode 100644 index 000000000..5cc5b12cc --- /dev/null +++ b/test/forbodyslotfix/src/cpp/build.cc @@ -0,0 +1,44 @@ +#include "build.h" + +Edge* Plan::FindWork() { + if (ready_.empty()) + return nullptr; + Edge* e = ready_.back(); + ready_.pop_back(); + return e; +} + +void Plan::EdgeFinished(Edge* edge, bool success) { + edge->outputs_ready = success; +} + +bool Builder::AddTarget(const std::string& name, std::string* err) { + if (name.empty()) { + *err = "unknown target ''"; + return false; + } + return true; +} + +bool Builder::RunCommand(Edge* edge) { + return !edge->command.empty(); +} + +// Run every ready edge of the plan; stop once failures_allowed commands have failed. +bool Builder::Build(std::string* err) { + int failures = 0; + while (plan_.more_to_do()) { + Edge* edge = plan_.FindWork(); + if (edge == nullptr) { + *err = "stuck [this is a bug]: cannot make progress"; + return false; + } + bool ok = RunCommand(edge); + plan_.EdgeFinished(edge, ok); + if (!ok && ++failures >= failures_allowed_) { + *err = failures_allowed_ > 1 ? "subcommands failed" : "subcommand failed"; + return false; + } + } + return true; +} diff --git a/test/forbodyslotfix/src/cpp/build.h b/test/forbodyslotfix/src/cpp/build.h new file mode 100644 index 000000000..d0bc76b57 --- /dev/null +++ b/test/forbodyslotfix/src/cpp/build.h @@ -0,0 +1,32 @@ +// Builder: runs the edges of a build plan until the requested targets are up to date. +#pragma once +#include +#include + +struct Edge { + std::string command; + std::vector outputs; + bool outputs_ready = false; +}; + +class Plan { +public: + bool more_to_do() const { return !ready_.empty(); } + Edge* FindWork(); + void EdgeFinished(Edge* edge, bool success); +private: + std::vector ready_; +}; + +class Builder { +public: + explicit Builder(int failures_allowed) : failures_allowed_(failures_allowed) {} + bool AddTarget(const std::string& name, std::string* err); + // Build the plan: run every ready edge's commands until the targets are up to date; + // the builder stops once failures_allowed commands have failed. + bool Build(std::string* err); +private: + Plan plan_; + int failures_allowed_; + bool RunCommand(Edge* edge); +}; diff --git a/test/forbodyslotfix/src/go/packager.go b/test/forbodyslotfix/src/go/packager.go new file mode 100644 index 000000000..8990d1ef5 --- /dev/null +++ b/test/forbodyslotfix/src/go/packager.go @@ -0,0 +1,38 @@ +package packager + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "strings" +) + +var errInsufficientParams = errors.New("a target file or a packager name is required") + +// Packager turns a file list into one archive format. +type Packager struct { + Format string + Files []string +} + +// Package writes the archive for info: it picks the format, then writes every file into the archive. +func (p *Packager) Package(target string, w io.Writer) error { + if target == "" && p.Format == "" { + return errInsufficientParams + } + if p.Format == "" { + fmt.Println("guessing packager from target file extension...") + p.Format = strings.TrimPrefix(filepath.Ext(target), ".") + } + return writeArchive(p.Format, p.Files, w) +} + +func writeArchive(format string, files []string, w io.Writer) error { + for _, f := range files { + if _, err := fmt.Fprintf(w, "%s:%s\n", format, f); err != nil { + return err + } + } + return nil +} diff --git a/test/forbodyslotfix/src/helper/cookie.ts b/test/forbodyslotfix/src/helper/cookie.ts new file mode 100644 index 000000000..d0c8d2666 --- /dev/null +++ b/test/forbodyslotfix/src/helper/cookie.ts @@ -0,0 +1,25 @@ +import type { Context } from '../context' + +export const getCookie = (c: Context, key: string): string | undefined => { + const cookie = c.req.header('Cookie') + if (!cookie) { + return undefined + } + const pairs = cookie.split(';') + for (const pair of pairs) { + const [k, v] = pair.trim().split('=') + if (k === key) { + return v + } + } + return undefined +} + +export const getSignedCookie = async (c: Context, secret: string, key: string): Promise => { + const value = getCookie(c, key) + if (value === undefined) { + return undefined + } + const [val, sig] = value.split('.') + return sig === `${secret.length}` ? val : false +} diff --git a/test/forbodyslotfix/src/http-exception.ts b/test/forbodyslotfix/src/http-exception.ts new file mode 100644 index 000000000..52d696d54 --- /dev/null +++ b/test/forbodyslotfix/src/http-exception.ts @@ -0,0 +1,9 @@ +export class HTTPException extends Error { + readonly res?: Response + readonly status: number + constructor(status: number = 500, options?: { res?: Response; message?: string; cause?: unknown }) { + super(options?.message, { cause: options?.cause }) + this.res = options?.res + this.status = status + } +} diff --git a/test/forbodyslotfix/src/middleware/jwt/jwt.ts b/test/forbodyslotfix/src/middleware/jwt/jwt.ts new file mode 100644 index 000000000..43e533f44 --- /dev/null +++ b/test/forbodyslotfix/src/middleware/jwt/jwt.ts @@ -0,0 +1,78 @@ +import type { Context } from '../../context' +import { getCookie, getSignedCookie } from '../../helper/cookie' +import { HTTPException } from '../../http-exception' +import type { MiddlewareHandler } from '../../types' +import * as Jwt from '../../utils/jwt/jwt' + +export type JwtVariables = { + jwtPayload: unknown +} + +/** JWT middleware: reads the token from the Authorization header or a cookie and verifies it. */ +export const jwt = (options: { + secret: string + cookie?: string | { key: string; secret?: string } + alg?: string + headerName?: string +}): MiddlewareHandler => { + if (!options || !options.secret) { + throw new Error('JWT auth middleware requires options for "secret"') + } + const headerName = options.headerName || 'Authorization' + return async function jwt(ctx, next) { + const headerToken = ctx.req.header(headerName) + let token + if (headerToken) { + const parts = headerToken.split(/\s+/) + if (parts.length !== 2) { + const errDescription = 'invalid credentials structure' + throw new HTTPException(401, { + message: errDescription, + res: unauthorizedResponse({ ctx, error: 'invalid_request', errDescription }), + }) + } + token = parts[1] + } else if (options.cookie) { + if (typeof options.cookie == 'string') { + token = getCookie(ctx, options.cookie) + } else if (options.cookie.secret) { + token = await getSignedCookie(ctx, options.cookie.secret, options.cookie.key) + } else { + token = getCookie(ctx, options.cookie.key) + } + } + if (!token) { + const errDescription = 'no authorization included in request' + throw new HTTPException(401, { + message: errDescription, + res: unauthorizedResponse({ ctx, error: 'invalid_request', errDescription }), + }) + } + let payload + let cause + try { + payload = await Jwt.verify(token, options.secret, options.alg || 'HS256') + } catch (e) { + cause = e + } + if (!payload) { + throw new HTTPException(401, { + message: 'Unauthorized', + res: unauthorizedResponse({ ctx, error: 'invalid_token', statusText: 'Unauthorized', errDescription: 'token verification failure' }), + cause, + }) + } + ctx.set('jwtPayload', payload) + await next() + } +} + +function unauthorizedResponse(opts: { ctx: Context; error: string; errDescription: string; statusText?: string }) { + return new Response('Unauthorized', { + status: 401, + statusText: opts.statusText, + headers: { + 'WWW-Authenticate': `Bearer realm="${opts.ctx.req.url}",error="${opts.error}",error_description="${opts.errDescription}"`, + }, + }) +} diff --git a/test/forbodyslotfix/src/min/hydrate.js b/test/forbodyslotfix/src/min/hydrate.js new file mode 100644 index 000000000..c05138237 --- /dev/null +++ b/test/forbodyslotfix/src/min/hydrate.js @@ -0,0 +1,2 @@ +/** Restore the persisted UI state (a minified one-line body, kept as shipped). */ +export function hydrate(state) { const table = 'é→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→ü'; return Object.assign({}, state, { table, restored: true }) } diff --git a/test/forbodyslotfix/src/negotiate.ts b/test/forbodyslotfix/src/negotiate.ts new file mode 100644 index 000000000..6d52fd377 --- /dev/null +++ b/test/forbodyslotfix/src/negotiate.ts @@ -0,0 +1,7 @@ +/** Content negotiation: pick a response type from the Accept header. */ +export function negotiate(req: Request): string { + const accept = req.headers.get('accept') || '*/*' + if (accept.includes('application/json')) return 'json' + if (accept.includes('text/html')) return 'html' + return 'text' // negotiate fallback when nothing in Accept matches +} diff --git a/test/forbodyslotfix/src/rs/indexer.rs b/test/forbodyslotfix/src/rs/indexer.rs new file mode 100644 index 000000000..ebad8e0bf --- /dev/null +++ b/test/forbodyslotfix/src/rs/indexer.rs @@ -0,0 +1,57 @@ +//! Indexer: walks a crate's module tree and records every item it finds. + +use std::collections::HashMap; + +pub struct Item { + pub name: String, + pub kind: &'static str, + pub line: u32, +} + +pub struct Indexer { + items: HashMap, + skipped: u32, +} + +impl Indexer { + pub fn new() -> Self { + Indexer { items: HashMap::new(), skipped: 0 } + } + + /// Index a crate: walk its modules, keep the public items, count what was skipped. + pub fn index(&mut self, root: &str, sources: &[(&str, &str)]) -> usize { + let order = walk(root, sources); + for path in order { + let text = sources.iter().find(|(p, _)| *p == path).map(|(_, t)| *t).unwrap_or(""); + for (n, line) in text.lines().enumerate() { + if let Some(rest) = line.trim().strip_prefix("pub fn ") { + let name = rest.split('(').next().unwrap_or("").to_string(); + self.items.insert(name.clone(), Item { name, kind: "fn", line: n as u32 + 1 }); + } else if line.trim().starts_with("fn ") { + self.skipped += 1; // private items are not part of the crate surface + } + } + } + if self.items.is_empty() { + eprintln!("indexer: crate {} has no public surface", root); + } + self.items.len() + } +} + +/// Walk the module tree breadth-first from the crate root and return the source paths in visit order. +pub fn walk(root: &str, sources: &[(&str, &str)]) -> Vec { + let mut out = vec![root.to_string()]; + let mut i = 0; + while i < out.len() { + let here = out[i].clone(); + let text = sources.iter().find(|(p, _)| *p == here).map(|(_, t)| *t).unwrap_or(""); + for line in text.lines() { + if let Some(m) = line.trim().strip_prefix("mod ") { + out.push(format!("{}/{}.rs", here.trim_end_matches(".rs"), m.trim_end_matches(';'))); + } + } + i += 1; + } + out +} diff --git a/test/forbodyslotfix/src/textual/css/stylesheet.py b/test/forbodyslotfix/src/textual/css/stylesheet.py new file mode 100644 index 000000000..4c00f19b9 --- /dev/null +++ b/test/forbodyslotfix/src/textual/css/stylesheet.py @@ -0,0 +1,97 @@ +"""Stylesheet: parses CSS sources into rule sets and applies them to widgets.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class RuleSet: + selector: str + declarations: dict[str, str] = field(default_factory=dict) + specificity: int = 0 + + +def tokenize(css: str) -> list[str]: + """Split CSS text into tokens.""" + tokens: list[str] = [] + current = "" + for ch in css: + if ch in "{}:;": + if current.strip(): + tokens.append(current.strip()) + tokens.append(ch) + current = "" + else: + current += ch + if current.strip(): + tokens.append(current.strip()) + return tokens + + +def parse(css: str) -> list[RuleSet]: + """Parse CSS text into rule sets (parse_selectors + parse_declarations).""" + rules: list[RuleSet] = [] + tokens = tokenize(css) + i = 0 + while i < len(tokens): + selector = tokens[i] + i += 1 + if i < len(tokens) and tokens[i] == "{": + i += 1 + declarations: dict[str, str] = {} + while i < len(tokens) and tokens[i] != "}": + name = tokens[i] + if i + 2 < len(tokens) and tokens[i + 1] == ":": + declarations[name] = tokens[i + 2] + i += 3 + else: + i += 1 + if i < len(tokens) and tokens[i] == ";": + i += 1 + i += 1 + rules.append(RuleSet(selector, declarations, specificity=len(selector))) + return rules + + +class Stylesheet: + """Holds the rules parsed from every CSS source and applies them to widgets.""" + + def __init__(self) -> None: + self.rules: list[RuleSet] = [] + self.sources: list[str] = [] + + def add_source(self, css: str) -> None: + self.sources.append(css) + + def parse(self) -> None: + self.rules = [] + for source in self.sources: + self.rules.extend(parse(source)) + + def apply(self, widget: Widget) -> None: + """Apply the parsed rules to one widget: match selectors, order by specificity, set styles.""" + matched = [rule for rule in self.rules if self._check_rule(rule, widget)] + matched.sort(key=lambda rule: rule.specificity) + for rule in matched: + for name, value in rule.declarations.items(): + widget.styles[name] = value + widget.refresh() + + def _check_rule(self, rule: RuleSet, widget: Widget) -> bool: + return rule.selector == widget.css_type or rule.selector in widget.classes + + def update_nodes(self, widgets: list[Widget]) -> None: + for widget in widgets: + self.apply(widget) + + +class Widget: + css_type = "Widget" + + def __init__(self) -> None: + self.styles: dict[str, str] = {} + self.classes: set[str] = set() + + def refresh(self) -> None: + pass diff --git a/test/forbodyslotfix/src/textual/widgets.py b/test/forbodyslotfix/src/textual/widgets.py new file mode 100644 index 000000000..28f4dbe5d --- /dev/null +++ b/test/forbodyslotfix/src/textual/widgets.py @@ -0,0 +1,27 @@ +"""Widgets that repaint themselves when their content changes.""" + + +class Label: + """A single line of text.""" + + def __init__(self, text: str) -> None: + self.text = text + self.dirty = True + + def refresh(self) -> None: + """Repaint the label: clear the dirty flag and redraw the text.""" + self.dirty = False + print("label repaint", self.text) + + +class Button: + """A clickable label with a border.""" + + def __init__(self, text: str) -> None: + self.text = text + self.dirty = True + + def refresh(self) -> None: + """Repaint the button: redraw the border, then the text.""" + self.dirty = False + print("button repaint border", self.text) diff --git a/test/forbodyslotfix/src/two/alpha.ts b/test/forbodyslotfix/src/two/alpha.ts new file mode 100644 index 000000000..79a5911b7 --- /dev/null +++ b/test/forbodyslotfix/src/two/alpha.ts @@ -0,0 +1,7 @@ +export interface View { title: string; items: string[] } + +/** Render a view as a list: one
  • per item under a heading. */ +export function render(view: View): string { + const items = view.items.map((i) => `
  • ${i}
  • `).join('') + return `

    ${view.title}

      ${items}
    ` + '' +} diff --git a/test/forbodyslotfix/src/two/beta.ts b/test/forbodyslotfix/src/two/beta.ts new file mode 100644 index 000000000..8e2a2f32a --- /dev/null +++ b/test/forbodyslotfix/src/two/beta.ts @@ -0,0 +1,7 @@ +import type { View } from './alpha' + +/** Render a view as a table: one per item under a caption. */ +export function render(view: View): string { + const rows = view.items.map((i) => `${i}`).join('') + return `${rows}
    ${view.title}
    ` + '' +} diff --git a/test/forbodyslotfix/src/types/global.d.ts b/test/forbodyslotfix/src/types/global.d.ts new file mode 100644 index 000000000..bfc5de7ad --- /dev/null +++ b/test/forbodyslotfix/src/types/global.d.ts @@ -0,0 +1,4 @@ +/** Ambient declarations for the server runtime. */ +/** negotiate: pick the content type for a response from the request's Accept header (json, html or text). */ +export function negotiate(req: Request): string +export declare function handshake(socket: WebSocket, protocol: string): Promise diff --git a/test/forbodyslotfix/src/utils/encode.ts b/test/forbodyslotfix/src/utils/encode.ts new file mode 100644 index 000000000..6af83169f --- /dev/null +++ b/test/forbodyslotfix/src/utils/encode.ts @@ -0,0 +1,2 @@ +export const encodeBase64Url = (s: string): string => btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +export const decodeBase64Url = (s: string): string => atob(s.replace(/-/g, '+').replace(/_/g, '/')) diff --git a/test/forbodyslotfix/src/utils/jwt/jwt.ts b/test/forbodyslotfix/src/utils/jwt/jwt.ts new file mode 100644 index 000000000..fed22acb8 --- /dev/null +++ b/test/forbodyslotfix/src/utils/jwt/jwt.ts @@ -0,0 +1,95 @@ +import { decodeBase64Url, encodeBase64Url } from '../encode' +import type { JWTPayload } from './types' +import { + JwtTokenExpired, + JwtTokenInvalid, + JwtTokenIssuedAt, + JwtTokenNotBefore, + JwtTokenSignatureMismatched, +} from './types' + +export interface TokenHeader { + alg: string + typ?: 'JWT' + kid?: string +} + +export interface VerifyOptions { + nbf?: boolean + exp?: boolean + iat?: boolean +} + +export const sign = async (payload: JWTPayload, privateKey: string, alg: string = 'HS256'): Promise => { + const encodedPayload = encodeBase64Url(JSON.stringify(payload)) + const encodedHeader = encodeBase64Url(JSON.stringify({ alg, typ: 'JWT' })) + const partialToken = `${encodedHeader}.${encodedPayload}` + const signaturePart = await signing(privateKey, alg, partialToken) + return `${partialToken}.${signaturePart}` +} + +/** Verify a token: decode it, check the algorithm, the nbf/exp/iat claims, then the signature. */ +export const verify = async ( + token: string, + publicKey: string, + algOrOptions: string | VerifyOptions = 'HS256' +): Promise => { + const alg = typeof algOrOptions === 'string' ? algOrOptions : 'HS256' + const tokenParts = token.split('.') + if (tokenParts.length !== 3) { + throw new JwtTokenInvalid(token) + } + const { header, payload } = decode(token) + if (!isTokenHeader(header)) { + throw new JwtTokenInvalid(token) + } + if (header.alg !== alg) { + throw new JwtTokenInvalid(token) + } + const now = Math.floor(Date.now() / 1000) + if (payload.nbf && payload.nbf > now) { + throw new JwtTokenNotBefore(token) + } + if (payload.exp && payload.exp <= now) { + throw new JwtTokenExpired(token) + } + if (payload.iat && now < payload.iat) { + throw new JwtTokenIssuedAt(now, payload.iat) + } + const headerPayload = token.substring(0, token.lastIndexOf('.')) + const verified = await verifying(publicKey, alg, decodeBase64Url(tokenParts[2]), headerPayload) + if (!verified) { + throw new JwtTokenSignatureMismatched(token) + } + return payload +} + +export const decode = (token: string): { header: TokenHeader; payload: JWTPayload } => { + try { + const [h, p] = token.split('.') + const header = decodeJwtPart(h) as TokenHeader + const payload = decodeJwtPart(p) as JWTPayload + return { header, payload } + } catch { + throw new JwtTokenInvalid(token) + } +} + +export const decodeHeader = (token: string): TokenHeader => { + try { + const [h] = token.split('.') + return decodeJwtPart(h) as TokenHeader + } catch { + throw new JwtTokenInvalid(token) + } +} + +const decodeJwtPart = (part: string): unknown => JSON.parse(decodeBase64Url(part)) + +const isTokenHeader = (obj: unknown): obj is TokenHeader => + typeof obj === 'object' && obj !== null && 'alg' in obj + +const signing = async (key: string, alg: string, data: string): Promise => `${alg}:${key.length}:${data.length}` + +const verifying = async (key: string, alg: string, signature: string, data: string): Promise => + signature === `${alg}:${key.length}:${data.length}` diff --git a/test/forbodyslotfix/src/utils/jwt/types.ts b/test/forbodyslotfix/src/utils/jwt/types.ts new file mode 100644 index 000000000..5d7db861b --- /dev/null +++ b/test/forbodyslotfix/src/utils/jwt/types.ts @@ -0,0 +1,54 @@ +export class JwtAlgorithmNotImplemented extends Error { + constructor(alg: string) { + super(`${alg} is not an implemented algorithm`) + this.name = 'JwtAlgorithmNotImplemented' + } +} + +export class JwtTokenInvalid extends Error { + constructor(token: string) { + super(`invalid JWT token: ${token}`) + this.name = 'JwtTokenInvalid' + } +} + +export class JwtTokenNotBefore extends Error { + constructor(token: string) { + super(`token (${token}) is being used before it's valid`) + this.name = 'JwtTokenNotBefore' + } +} + +export class JwtTokenExpired extends Error { + constructor(token: string) { + super(`token (${token}) expired`) + this.name = 'JwtTokenExpired' + } +} + +export class JwtTokenIssuedAt extends Error { + constructor(currentTimestamp: number, iat: number) { + super(`Incorrect "iat" claim must be a older than "${currentTimestamp}" (iat: "${iat}")`) + this.name = 'JwtTokenIssuedAt' + } +} + +export class JwtTokenSignatureMismatched extends Error { + constructor(token: string) { + super(`token(${token}) signature mismatched`) + this.name = 'JwtTokenSignatureMismatched' + } +} + +export enum AlgorithmTypes { + HS256 = 'HS256', + HS384 = 'HS384', + HS512 = 'HS512', +} + +export interface JWTPayload { + [key: string]: unknown + exp?: number + nbf?: number + iat?: number +} diff --git a/test/forbodyslotfix/tests/cmd-find-test.c b/test/forbodyslotfix/tests/cmd-find-test.c new file mode 100644 index 000000000..7d4132577 --- /dev/null +++ b/test/forbodyslotfix/tests/cmd-find-test.c @@ -0,0 +1,18 @@ +#include + +struct cmd_find_state; +int cmd_find_target(struct cmd_find_state *fs, const char *target, int flags); + +/* How does the command resolve the -t target: session, window and pane. */ +static void +test_cmd_find_target_resolves_session_window_pane(void) +{ + assert(cmd_find_target(0, "", 0) == -1); +} + +int +main(void) +{ + test_cmd_find_target_resolves_session_window_pane(); + return 0; +} diff --git a/test/forbodyslotfix/tests/test_stylesheet.py b/test/forbodyslotfix/tests/test_stylesheet.py new file mode 100644 index 000000000..8a47f05a1 --- /dev/null +++ b/test/forbodyslotfix/tests/test_stylesheet.py @@ -0,0 +1,18 @@ +"""Tests: how is CSS parsed and applied to widgets?""" + +from src.textual.css.stylesheet import Stylesheet, Widget, parse + + +def test_css_parsed_and_applied_to_widgets() -> None: + """How is CSS parsed and applied to widgets: parse then apply.""" + sheet = Stylesheet() + sheet.add_source("Widget { color: red; }") + sheet.parse() + widget = Widget() + sheet.apply(widget) + assert widget.styles["color"] == "red" + + +def test_parse_css_rules() -> None: + rules = parse("Widget { color: red; }") + assert rules[0].selector == "Widget" diff --git a/test/forbodyslotfix/tests/test_tokens.py b/test/forbodyslotfix/tests/test_tokens.py new file mode 100644 index 000000000..a23a890db --- /dev/null +++ b/test/forbodyslotfix/tests/test_tokens.py @@ -0,0 +1,11 @@ +"""Round-trip tests for the token encoder.""" + + +def roundtrip(text: str) -> str: + """Encode then decode a token string (test helper).""" + encoded = text.encode("utf-8").hex() + return bytes.fromhex(encoded).decode("utf-8") + + +def test_roundtrip_ascii() -> None: + assert roundtrip("abc") == "abc" diff --git a/test/forbudgetmonotoncheck.sh b/test/forbudgetmonotoncheck.sh index 2ca870521..d5934f27a 100755 --- a/test/forbudgetmonotoncheck.sh +++ b/test/forbudgetmonotoncheck.sh @@ -140,7 +140,10 @@ MISSING=$( comm -23 "$TMP/set_def" "$TMP/set_8k" | tr '\n' ' ' ) sigsblock "$TMP/def" >"$TMP/sigs_def" sigsblock "$TMP/tb8k" >"$TMP/sigs_8k" [ -s "$TMP/sigs_def" ] || no "#1 presence: could not extract a block from the default run" -diff -q "$TMP/sigs_def" "$TMP/sigs_8k" >/dev/null \ +# forbudget-soft (rule 4): the default answer discloses the rows it serves past its 7.5 KB target (); +# a soft target names none (its rows are inside its own target). Everything else — every row, its text tier, the +# continuation — must still be the default's byte for byte: the frozen-share invariant, minus that one disclosure. +diff -q <( sed 's/ past_target="[0-9]*"//' "$TMP/sigs_def" ) "$TMP/sigs_8k" >/dev/null \ && ok "#1 the sig section at the explicit ceiling is byte-identical to the default's (frozen share)" \ || no "#1 the explicit ceiling re-inflated the sig section (default $( wc -c <"$TMP/sigs_def" | tr -d ' ' ) B vs 8000 $( wc -c <"$TMP/sigs_8k" | tr -d ' ' ) B)" @@ -183,7 +186,10 @@ grep -q 'bundle="compact"' "$TMP/cdef" \ sigsblock "$TMP/cdef" >"$TMP/sigs_cdef" sigsblock "$TMP/c8k" >"$TMP/sigs_c8k" [ -s "$TMP/sigs_cdef" ] || no "#5 presence: could not extract a block from the compact default run" -diff -q "$TMP/sigs_cdef" "$TMP/sigs_c8k" >/dev/null \ +# forbudget-soft (rule 4): the default answer discloses the rows it serves past its 7.5 KB target (); +# a soft target names none (its rows are inside its own target). Everything else — every row, its text tier, the +# continuation — must still be the default's byte for byte: the frozen-share invariant, minus that one disclosure. +diff -q <( sed 's/ past_target="[0-9]*"//' "$TMP/sigs_cdef" ) "$TMP/sigs_c8k" >/dev/null \ && ok "#5 the compact route's sig section is byte-identical at the explicit ceiling (frozen share)" \ || no "#5 the explicit ceiling re-inflated the COMPACT route's sig section (default $( wc -c <"$TMP/sigs_cdef" | tr -d ' ' ) B vs 8000 $( wc -c <"$TMP/sigs_c8k" | tr -d ' ' ) B)" diff --git a/test/fordisclosurecheck.sh b/test/fordisclosurecheck.sh index 5893c5821..bab6faef8 100755 --- a/test/fordisclosurecheck.sh +++ b/test/fordisclosurecheck.sh @@ -24,14 +24,13 @@ # against a trunk where every conceptual query still took the body walk. The rule is unchanged; # only the set of shapes it has to hold across grew, and the compact shape had inherited its own # fresh copy of the silent branch, fixed at that merge.) -# 2) the ceiling-exhausted case (allowance fully consumed by the signature bundle) discloses as -# the ATTRIBUTE ALONE — bodies="0" reason="budget", no legend, no section element, because -# only the attribute has reserved bytes at a spent ceiling (D10's "trims to fit" is a contract -# too: fornotesbudgetcheck/forrootlegendcheck hold est_tokens to the stated ceiling at these -# budgets, and the ~half-KB legend has no reserve there). The block stays intact and -# byte-identical to the --signatures-only run, and est_tokens moves by at most the attribute's -# own worth. The no-fit-but-allowance-exists case (forautobodycheck #3b) keeps its richer -# shell + legend disclosure — those budgets can pay for them. +# 2) the target-exhausted case (the explicit --token-budget spent by the signature rows) discloses on +# the root — bodies="0" reason="budget" — and, since forbudget-soft (2026-10-14: an explicit +# --token-budget is a SOFT target over the rows; no header ladder, no over_ceiling=), it +# KEEPS the bundle legend and serves the empty shell (no body +# bytes): the old attribute-only shape existed to fit a hard byte ceiling that is gone. The +# block stays intact and byte-identical to the --signatures-only run, and est_tokens prices the +# extra disclosure bytes (rises, at no more than one token per kMinBytesPerToken bytes). # 3) --for --json serves no bodies BY DESIGN and says so: "bundle":"sigs" is always present # (the B1.4 rule — a reader must be able to tell "genuinely none" from "not in this dialect"). # 4) the MCP `for` verb (and batch's for sub-query — same builder, forTaskText) serves the @@ -112,8 +111,10 @@ else no "#1b compact route, ceiling exhausted: a other than the empty counted marker leaked past a spent ceiling ($ct_hops)" fi # #1c (fix round 4): the cut file tail under the same spent ceiling carries the call that lists the files it left out — -# the --for file page — and that call answers (rc 0, a page holding at least the tail's total). Negative: the -# default regime's tail (no --token-budget) is unchanged — no next= (a named deferral). +# the --for file page — and that call answers (rc 0, a page holding at least the tail's total). +# forbudget-soft (rule 4, decision D-4): a capped is continued in EVERY regime, so the old negative "the default +# regime's tail carries no next=" is replaced by its positive: the default (no --token-budget) capped tail carries the same +# file-page next=, and that page holds at least the default tail's total. RED on base (d4325a8c: the default tail has no next=). ct_tail="$( grep -oE ']*>' "$CT" | head -1 )" ct_ttot="$( printf '%s' "$ct_tail" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" "$BIN" src --for="$CONCTASK" --limit=40 --no-cache >"$TMP/cpage" 2>/dev/null; cp_rc=$? @@ -124,12 +125,30 @@ cp_tot="$( grep -oE '^]*>' "$TMP/cpage" | sed -nE 's/.* total="([0-9]+ || no "#1c compact route, ceiling exhausted: the cut has no working next= (tail '$ct_tail', page rc=$cp_rc total='${cp_tot:-none}')" "$BIN" src --for="$CONCTASK" --no-cache >"$TMP/cdef" 2>/dev/null cd_tail="$( grep -oE ']*>' "$TMP/cdef" | head -1 )" -{ [ -n "$cd_tail" ] && [ "${cd_tail#* next=}" = "$cd_tail" ]; } \ - && ok "#1c default regime: unchanged, no next= added ($cd_tail)" \ - || no "#1c default regime: changed or missing ('$cd_tail') — the explicit-budget next= leaked into the default" -grep -q 'bundle=compact:' "$CT" \ - && no "#1b compact route, ceiling exhausted: the legend leaked past a spent ceiling (no reserve there)" \ - || ok "#1b compact route, ceiling exhausted: legend dropped (only the attribute's reserved bytes are spent)" +cd_ttot="$( printf '%s' "$cd_tail" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" +if ! printf '%s' "$cd_tail" | grep -q ' capped="1"'; then + no "#1c default regime premise: the default is not capped ('$cd_tail') — this arm measures nothing; re-anchor it on a capped tail" +# FOLD C (forbudget-soft round C; rule 4): the default tail carries page 0 of the file page, so its next= is the page FROM ITS +# FIRST UNPRINTED PAGE ROW (`--limit=40 --offset=K`); that call must answer with a page whose rows start at K. +elif cd_off="$( printf '%s' "$cd_tail" | sed -nE "s/.*capped=\"1\" next=\"--for='$CONCTASK' --limit=40( --offset=([0-9]+))?\">\$/\2/p" )"; \ + printf '%s' "$cd_tail" | grep -qE "capped=\"1\" next=\"--for='$CONCTASK' --limit=40( --offset=[0-9]+)?\">\$" && [ "$cp_rc" -eq 0 ] \ + && [ -n "$cp_tot" ] && [ -n "$cd_ttot" ] && [ "$cp_tot" -ge "$cd_ttot" ] 2>/dev/null \ + && "$BIN" src --for="$CONCTASK" --limit=40 ${cd_off:+--offset=$cd_off} --no-cache 2>/dev/null | grep -qE "^]* offset=\"${cd_off:-0}\""; then + ok "#1c default regime: the capped carries the file-page next= (D-4; offset=${cd_off:-0}, FOLD C) that answers; the page holds $cp_tot files >= the tail's $cd_ttot" +else + no "#1c default regime: the capped has no working file-page next= ('$cd_tail', page rc=$cp_rc total='${cp_tot:-none}')" +fi +# forbudget-soft (rule 4): a spent SOFT target has no header ladder, so the old "legend dropped (no reserve at a spent ceiling)" +# arm (which only ever read the FULL legend's spelling, so it passed vacuously on the compact default) is replaced by HL: the +# --legend=full twin keeps the bundle=compact: clause, and neither answer drops a clause "(ceiling)" or carries over_ceiling=. +# RED on base (d4325a8c: the full twin drops route=/r=/tail "(ceiling)" and labels over_ceiling="1"). +"$BIN" src --for="$CONCTASK" --token-budget=400 --legend=full --no-cache >"$TMP/c400full" 2>/dev/null; cf_rc=$? +if [ "$cf_rc" -eq 0 ] && grep -q 'bundle=compact:' "$TMP/c400full" && ! grep -q '(ceiling)' "$TMP/c400full" "$CT" \ + && ! grep -q 'over_ceiling="1"' "$TMP/c400full" "$CT"; then + ok "#1b compact route, target exhausted: the bundle legend is kept (--legend=full carries bundle=compact:), no (ceiling) drop, no over_ceiling" +else + no "#1b compact route, target exhausted: legend/label (full rc=$cf_rc; bundle=compact: $( grep -c 'bundle=compact:' "$TMP/c400full" ), (ceiling) drops $( cat "$TMP/c400full" "$CT" | grep -c '(ceiling)' ), over_ceiling $( cat "$TMP/c400full" "$CT" | grep -c 'over_ceiling="1"' ))" +fi grep -q ' block is intact" \ || no "#1b compact route, ceiling exhausted: the block vanished" @@ -140,12 +159,21 @@ T="$TMP/n400" grep -q 'bundle="auto" bodies="0" reason="budget"' "$T" \ && ok "#2 ceiling-exhausted: bodies=\"0\" reason=\"budget\" disclosed" \ || no "#2 ceiling-exhausted: missing bodies=\"0\" reason=\"budget\" on the root" -grep -q ' element leaked past a spent ceiling (attribute-only there)" \ - || ok "#2 ceiling-exhausted: no element (the attribute IS the disclosure at a spent ceiling)" -grep -q 'bundle=auto:' "$T" \ - && no "#2 ceiling-exhausted: the ~half-KB legend leaked past a spent ceiling (it has no reserve there)" \ - || ok "#2 ceiling-exhausted: legend dropped (only the attribute's reserved bytes are spent)" +# forbudget-soft (rule 4): the attribute-only shape existed to fit a hard ceiling; on a soft target the empty shell +# rides (shown="0", capped="1", no row — the no-body-bytes arm below still holds) and the bundle legend is kept (HL, read +# on the --legend=full twin; the compact default never spelled bundle=auto:), with no (ceiling) drop and no over_ceiling. +# RED on base (d4325a8c: no shell, the full legend drops clauses "(ceiling)", over_ceiling="1"). +t_bodies="$( grep -oE ']*>' "$T" | head -1 )" +{ printf '%s' "$t_bodies" | grep -qE '^$' && ! grep -q ' shell rides ($t_bodies), no body row" \ + || no "#2 target exhausted: expected the empty shell, got '${t_bodies:-none}'" +"$BIN" src --for="$NAMETASK" --token-budget=400 --legend=full --no-cache >"$TMP/n400full" 2>/dev/null; nf_rc=$? +if [ "$nf_rc" -eq 0 ] && grep -q 'bundle=auto:' "$TMP/n400full" && ! grep -q '(ceiling)' "$TMP/n400full" "$T" \ + && ! grep -q 'over_ceiling="1"' "$TMP/n400full" "$T"; then + ok "#2 target exhausted: the bundle legend is kept (--legend=full carries bundle=auto:), no (ceiling) drop, no over_ceiling" +else + no "#2 target exhausted: legend/label (full rc=$nf_rc; bundle=auto: $( grep -c 'bundle=auto:' "$TMP/n400full" ), (ceiling) drops $( cat "$TMP/n400full" "$T" | grep -c '(ceiling)' ), over_ceiling $( cat "$TMP/n400full" "$T" | grep -c 'over_ceiling="1"' ))" +fi grep -q ']*> block is intact" \ # first (a floor document: root over_ceiling="1", the LAST ]* over_ceiling="1"' "$T" && grep -oE ']*>' "$T" | tail -1 | grep -q ' capped="1".* next="'; then +# forbudget-soft (rule 4): the premise is re-anchored on what still holds. A soft target labels nothing (no over_ceiling=), +# but the 400 document still has a capped carrying next= with the header legend's sigs/next= clauses BEFORE the real +# tag, so the two anti-forgery greps keep their teeth. Premise: no over_ceiling on the root, the LAST ]* over_ceiling=' "$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" \ @@ -167,7 +198,7 @@ if grep -qE '^]* over_ceiling="1"' "$T" && grep -oE ']*>' "$T" && 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 is no longer a floor document (root over_ceiling=\"1\", a capped with next=) — re-anchor this arm on one that is" + no "#2c premise: the --token-budget=400 document has no capped with next= (or labels over_ceiling= on a soft target) — 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" @@ -177,9 +208,14 @@ diff -q "$TMP/s_a" "$TMP/s_s" >/dev/null \ || no "#2 ceiling-exhausted: the disclosure changed the bytes" ET_T=$( grep -o 'est_tokens="[0-9]*"' "$T" | head -1 | grep -o '[0-9]*' ) ET_S=$( grep -o 'est_tokens="[0-9]*"' "$TMP/n400so" | head -1 | grep -o '[0-9]*' ) -{ [ -n "${ET_T:-}" ] && [ -n "${ET_S:-}" ] && [ $(( ET_T - ET_S )) -ge 0 ] && [ $(( ET_T - ET_S )) -le 30 ]; } \ - && ok "#2 ceiling-exhausted: est_tokens moved by only the attribute's worth ($ET_T vs $ET_S, band 0..30)" \ - || no "#2 ceiling-exhausted: est_tokens delta out of band (tight=$ET_T vs sig-only=$ET_S — the disclosure leaked more than its reserve)" +# forbudget-soft (rule 4): the old band (0..30 tokens: the attribute's reserved worth) priced a disclosure squeezed under a hard +# ceiling; the legend clause and the shell now ride, so the band is replaced by honest pricing of the extra bytes: +# est_tokens rises (>= 1) and by no more than one token per kMinBytesPerToken (2.36) bytes of the byte delta (+1 rounding). +BY_T=$( wc -c <"$T" | tr -d ' ' ); BY_S=$( wc -c <"$TMP/n400so" | tr -d ' ' ) +ET_MAX=$( python3 -c 'import math,sys; print(math.ceil(max(0,int(sys.argv[1])-int(sys.argv[2]))/2.36)+1)' "${BY_T:-0}" "${BY_S:-0}" 2>/dev/null ) +{ [ -n "${ET_T:-}" ] && [ -n "${ET_S:-}" ] && [ -n "${ET_MAX:-}" ] && [ "$BY_T" -gt "$BY_S" ] && [ $(( ET_T - ET_S )) -ge 1 ] && [ $(( ET_T - ET_S )) -le "$ET_MAX" ]; } \ + && ok "#2 target exhausted: est_tokens prices the disclosure bytes ($ET_T vs $ET_S for +$(( BY_T - BY_S )) B, band 1..$ET_MAX)" \ + || no "#2 target exhausted: est_tokens delta out of band (tight=${ET_T:-?} vs sig-only=${ET_S:-?}, bytes ${BY_T:-?} vs ${BY_S:-?}, max ${ET_MAX:-?})" # ── #3: the --json dialect names its own posture ──────────────────────────────────────────────────────── "$BIN" src --for="$NAMETASK" --json --no-cache >"$TMP/j1" 2>/dev/null; rc=$? diff --git a/test/formaxtokenscheck.sh b/test/formaxtokenscheck.sh index 505510902..6ab9af22b 100755 --- a/test/formaxtokenscheck.sh +++ b/test/formaxtokenscheck.sh @@ -47,8 +47,11 @@ # (D) LEGEND. The attribute is defined by the legend of the document that carries it, against the # ceiling ACTUALLY on that root — max_tokens, budget_tokens, or both when both were asked for — and # a document inside its ceiling pays none of those bytes. -# (E) --token-budget INTEROP. The pre-existing budget contract is unchanged, and with both flags set the -# label answers to EITHER ceiling. +# (E) --token-budget INTEROP. forbudget-soft (rule 4, owner decisions 2026-10-14): an explicit --token-budget on +# --for is a SOFT row target, never a ceiling this root labels against — so alone it carries no over_ceiling= +# (est_tokens may exceed budget_tokens), and beside --max-tokens the label answers to max_tokens ONLY and its +# legend says so. The served rows are a rank prefix that grows with the target, untouched by --max-tokens. +# (Was: "the budget contract is unchanged, and with both flags set the label answers to EITHER ceiling".) # (F) DETERMINISM across the band, and (G) MUTATION CONTROLS: every judge above is re-run over a # deliberately corrupted copy of a real document and must go RED. An arm that cannot fail is not a # gate, and three of these four judges are cheap to write in an always-true form. @@ -99,8 +102,9 @@ QUERY='processEntry input trim' # the same code path the arm did. Reads the ROOT ELEMENT through a real XML parser (the :719 trap), and # re-derives est_tokens from the delivered bytes. Prints one diagnostic line per violation; exit 0 clean. # -# judge.py DOC MAX_TOKENS TOKEN_BUDGET +# judge.py DOC MAX_TOKENS TOKEN_BUDGET [soft] # MAX_TOKENS / TOKEN_BUDGET: the value passed on the command line, or 0 for "flag not given". +# soft: TOKEN_BUDGET names a soft target (forbudget-soft) — stated on the root, never a ground for the label. JUDGE="$TMP/judge.py" cat > "$JUDGE" <<'PY_EOF' import re, sys, xml.etree.ElementTree as ET @@ -114,6 +118,7 @@ def tokens( byteCount, rate ): return int( byteCount / rate + 0.5 ) # tokensForEmittedBytes path, wantMax, wantBudget = sys.argv[1], int( sys.argv[2] ), int( sys.argv[3] ) +softBudget = len( sys.argv ) > 4 and sys.argv[4] == 'soft' raw = open( path, 'rb' ).read() bad = [] @@ -138,7 +143,8 @@ est = int( attrs['est_tokens'] ) # ── (A/B/E) THE BICONDITIONAL, in the unit the root prints both numbers in ─────────────────────────────── overMax = wantMax > 0 and est > wantMax -overBudget = wantBudget > 0 and est > wantBudget +# forbudget-soft (rule 4): a soft target is never "over" — only max_tokens= is a ceiling on such a root +overBudget = wantBudget > 0 and est > wantBudget and not softBudget labelled = attrs.get( 'over_ceiling' ) == '1' if ( overMax or overBudget ) and not labelled: bad.append( 'est_tokens=%d exceeds the ceiling on its own root (max_tokens=%d budget_tokens=%d) ' @@ -178,6 +184,8 @@ if labelled: else: named = set( re.findall( r'(?:budget|max)_tokens', clause.group( 1 ) ) ) onRoot = set( k for k in ( 'budget_tokens', 'max_tokens' ) if k in attrs ) + if softBudget: + onRoot.discard( 'budget_tokens' ) # forbudget-soft: the label must not be defined against a soft target if named != onRoot: bad.append( 'the legend defines over_ceiling against %s, but this root carries %s' % ( sorted( named ) or [ 'nothing' ], sorted( onRoot ) or [ 'nothing' ] ) ) @@ -241,23 +249,73 @@ python3 "$JUDGE" "$TMP/nomax" 0 0 \ || no "(B) --detail=30 with no --max-tokens still emits a ceiling attribute or a verdict" # ══ (E) --token-budget INTEROP ═══════════════════════════════════════════════════════════════════════════ -# The pre-existing budget contract is unchanged (this fix must not move it), and with BOTH ceilings on one -# root the single label answers to either — which is also the case whose legend has two names to get right. -INTEROP=0; INTEROP_BAD=0 +# forbudget-soft (rule 4) — REPLACES "the pre-existing budget contract is unchanged, and with both ceilings on one root the +# single label answers to either" (the JUDGE's biconditional against budget_tokens=). An explicit --token-budget is now a +# SOFT row target (owner decisions 2026-10-14): the JUDGE runs with `soft`, so at every point +# · alone: NOLBL — no over_ceiling=, though est_tokens may exceed budget_tokens (asserted reachable below); +# · beside --max-tokens: the label answers to max_tokens ONLY, and the legend defines it against max_tokens alone; +# · (C) byte accounting and budget_tokens= on the root are judged exactly as before. +# Plus RP: rows(N) is a rank prefix of rows(N') for N < N' and of the default answer's rows, and --max-tokens (a body +# ceiling) never moves a row. The --max-tokens band (A) above is this arm's hard-ceiling twin. +INTEROP=0; INTEROP_BAD=0; SOFT_PAST=0; BOTH_LABELLED=0 +rowsOf(){ python3 -c " +import re, sys +s = open( sys.argv[1] ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = dict( re.findall( r'(\w+)=\"([^\"]*)\"', d.group( 1 ) ) ) + r = a.get( 'r', '' ) + rows.append( ( int( r ) if r.isdigit() else 10**9, a.get( 'p', '?' ) + ':' + a.get( 'l', '?' ) + ':' + a.get( 'n', '?' ) ) ) +rows.sort() +print( '\n'.join( x[1] for x in rows ) )" "$1"; } +isPrefix(){ [ -s "$1" ] && [ "$( wc -l < "$1" )" -le "$( wc -l < "$2" )" ] && [ "$( head -n "$( wc -l < "$1" )" "$2" )" = "$( cat "$1" )" ]; } +rowsOf "$TMP/nomax" > "$TMP/rows.nomax" || no "(E) the default --detail=30 answer has no block — RP has no reference" +PREV="" for TB in 800 2000 20000; do "$BIN" "$CORPUS" --no-cache "--for=$QUERY" --detail=30 --token-budget="$TB" >"$TMP/tb.$TB" 2>/dev/null INTEROP=$(( INTEROP + 1 )) - python3 "$JUDGE" "$TMP/tb.$TB" 0 "$TB" || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " ^ at --token-budget=$TB"; } + python3 "$JUDGE" "$TMP/tb.$TB" 0 "$TB" soft || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " ^ at --token-budget=$TB"; } + EST="$( python3 -c " +import sys, xml.etree.ElementTree as ET +print( ET.parse( sys.argv[1] ).getroot().attrib.get( 'est_tokens', '' ) )" "$TMP/tb.$TB" 2>/dev/null )" + case "$EST" in ''|*[!0-9]*) ;; *) [ "$EST" -gt "$TB" ] && SOFT_PAST=$(( SOFT_PAST + 1 )) ;; esac + if rowsOf "$TMP/tb.$TB" > "$TMP/rows.$TB"; then + isPrefix "$TMP/rows.$TB" "$TMP/rows.nomax" \ + && ok "(E) RP: rows(--token-budget=$TB) — $( wc -l < "$TMP/rows.$TB" | tr -d ' ' ) row(s) — are a rank prefix of the default answer's $( wc -l < "$TMP/rows.nomax" | tr -d ' ' )" \ + || no "(E) RP: rows(--token-budget=$TB) are not a rank prefix of the default answer's rows" + if [ -n "$PREV" ]; then + isPrefix "$TMP/rows.$PREV" "$TMP/rows.$TB" \ + && ok "(E) RP: rows(--token-budget=$PREV) are a prefix of rows(--token-budget=$TB) (monotone in the target)" \ + || no "(E) RP: rows(--token-budget=$PREV) are not a prefix of rows(--token-budget=$TB) — not monotone" + fi + PREV="$TB" + else + no "(E) --token-budget=$TB: no block — no rows to judge" + fi for MT in 300 20000; do "$BIN" "$CORPUS" --no-cache "--for=$QUERY" --detail=30 --token-budget="$TB" --max-tokens="$MT" >"$TMP/both.$TB.$MT" 2>/dev/null INTEROP=$(( INTEROP + 1 )) - python3 "$JUDGE" "$TMP/both.$TB.$MT" "$MT" "$TB" \ + python3 "$JUDGE" "$TMP/both.$TB.$MT" "$MT" "$TB" soft \ || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " ^ at --token-budget=$TB --max-tokens=$MT"; } + grep -o ']*>' "$TMP/both.$TB.$MT" | head -1 | grep -q ' over_ceiling="1"' && BOTH_LABELLED=$(( BOTH_LABELLED + 1 )) + rowsOf "$TMP/both.$TB.$MT" > "$TMP/rows.$TB.$MT" 2>/dev/null + cmp -s "$TMP/rows.$TB.$MT" "$TMP/rows.$TB" && [ -s "$TMP/rows.$TB" ] \ + || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " --token-budget=$TB --max-tokens=$MT: the rows differ from --token-budget=$TB alone — the body ceiling moved a row"; } done done [ "$INTEROP_BAD" = "0" ] \ - && ok "(E) --token-budget alone and beside --max-tokens: $INTEROP point(s), one label answering to either ceiling" \ + && ok "(E) --token-budget alone and beside --max-tokens: $INTEROP point(s), no label for the soft target, the label answering to max_tokens only, same rows either way" \ || no "(E) VIOLATED at $INTEROP_BAD of $INTEROP --token-budget point(s)" +# NON-VACUITY for both halves: a soft target the answer really exceeds (so NOLBL was tested, not assumed), and a +# combined point really over max_tokens (so the max_tokens-only label was read, not absent). +[ "$SOFT_PAST" -gt 0 ] \ + && ok "(E) non-vacuity: $SOFT_PAST --token-budget point(s) deliver est_tokens past the soft target (the no-label half is exercised)" \ + || no "(E) VACUOUS: no --token-budget point exceeds its target — the no-label half never ran" +[ "$BOTH_LABELLED" -gt 0 ] \ + && ok "(E) non-vacuity: $BOTH_LABELLED combined point(s) carry over_ceiling=\"1\" for max_tokens" \ + || no "(E) VACUOUS: no --token-budget + --max-tokens point is labelled — the max_tokens-only legend half never ran" # ══ (F) DETERMINISM across the band ══════════════════════════════════════════════════════════════════════ # The verdict is decided inside an est_tokens fixpoint that measures the document containing its own label. @@ -308,6 +366,32 @@ d = re.sub( rb' over_ceiling=1 says est_tokens exceeds [a-z_ ]+', b'', d, count # G5 — the legend arm, other direction: the clause naming a ceiling this root does not carry. mutate "a legend naming the wrong ceiling" "$TMP/band.300" 300 0 \ "d = d.replace( b'over_ceiling=1 says est_tokens exceeds max_tokens', b'over_ceiling=1 says est_tokens exceeds budget_tokens', 1 )" +# G6/G7 — forbudget-soft (rule 4): the soft judge of (E), on the old behaviour manufactured — a label on a soft target the +# answer exceeds, and a legend defining the label against the soft target. A pasted attribute also moves the byte count, +# which (C) rejects on its own, so each control requires the judge's rejection to name the SOFT reason ($6), not just exit 1. +# On a binary whose real document already has the broken shape (the pre-soft base), the edit is a no-op and the real +# document is judged as is — the control then reads the shape the binary emitted, which must be rejected all the same. +softMutate(){ # $1 label, $2 source doc, $3 max, $4 budget, $5 python edit expr, $6 expected reason + python3 - "$2" "$TMP/mut.xml" < "$TMP/mut.why" 2>&1; then + no "(G) $1: the soft judge ACCEPTS a deliberately broken document — arm (E) is inert" + elif grep -qF -e "$6" "$TMP/mut.why"; then + ok "(G) $1: the soft judge rejects it, for that reason" + else + no "(G) $1: the soft judge rejects it only for another reason ($( head -1 "$TMP/mut.why" | cut -c1-120 )) — the soft half is inert" + fi +} +softMutate "a label on a soft target (over_ceiling pasted onto --token-budget=800 alone)" "$TMP/tb.800" 0 800 \ + "d = d.replace( b' over_ceiling=\"1\"', b'', 1 ).replace( b' est_tokens=', b' over_ceiling=\"1\" est_tokens=', 1 )" \ + 'root claims over_ceiling="1" but est_tokens=' +softMutate "a legend defining the label against the soft target too" "$TMP/both.800.300" 300 800 \ + "d = d.replace( b'over_ceiling=1 says est_tokens exceeds max_tokens', b'over_ceiling=1 says est_tokens exceeds budget_tokens or max_tokens', 1 )" \ + 'the legend defines over_ceiling against' if [ "$fail" = "0" ]; then echo "formaxtokenscheck: ALL PASS"; else echo "formaxtokenscheck: FAILURES"; fi exit "$fail" diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index f4f5c8e31..3dba68424 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -10,6 +10,33 @@ # Exits non-zero on any failure; prints PASS/FAIL per check and ALL PASS on success. # DO NOT edit regression.sh — this is a standalone gate invoked from there. # +# ── BY-DESIGN BREAK — forbudget-soft (rule 4); owner decisions 2026-10-14. READ THIS FIRST. ────────────────────────────── +# An explicit --token-budget=N on --for is now a SOFT TARGET, not a byte ceiling over the whole document: T = N x 2.36 x 0.90 +# bytes (serialize.h kMinBytesPerToken x kBudgetHeadroom) is charged against the bytes INSIDE only — the rows, each +# with its children (the JSON twin: the emitted "sigs" array, jsonSigEntryCost's exact model, notes included). Rows +# are added in rank order while they fit T, the rank 1..4 floor always ships, the cut carries next=; the header and every +# disclosure are never charged, so est_tokens MAY exceed N and nothing is labelled over_ceiling= for it. The DEFAULT --for +# (no budget) stops when complete: the ranked head ships whole, rows past the 7.5 KB target disclosed as past_target=. +# The SUBJECT of this gate survives the move intact — W3-N2: note bytes are CHARGED to the budget like the rows they ride, +# never trimmed — only the thing they are charged against moved from the whole document to the rows. So the arms +# that asserted the deleted contract ("est_tokens <= N in both dialects" at 1200/1640/3000; the 700..2000 sweep's +# "est_tokens <= N OR over_ceiling=1"; the no-notes tree's "est_tokens <= 900" / "over 900 and labelled") are REPLACED by +# the invariants that pin W3-N2 under the soft target, on the same fixture and the same rungs: +# ST the rows' bytes inside , notes included, fit T — or only the rank 1..4 floor ships — and a cut is MAXIMAL +# (the next row of the same dialect's no-budget answer would cross T; 160 B slack, as forsoftbudgetcheck's (ST)). +# A note that were NOT charged would let a cut run past T by its note bytes: that is the W3-N2 leak, measured here. +# NOTES every served row carries its note — charged, never trimmed — and a notes tree serves NO MORE rows than the same +# tree without notes at the same N (and strictly fewer somewhere: the charge is real, not vacuous). +# NOLBL no over_ceiling= on a budget_tokens answer (a soft target is never "over"). +# RP rows(N) are a rank prefix of the no-budget answer's rows, and monotone in N (rows(N-10) a prefix of rows(N)). +# NX every cut names its continuation ( / "sigs_next"). +# CF the compact and full legends serve the same rows. +# DH the no-budget answer on the note-heavy tree serves the whole ranked head, every row with its note, unlabelled. +# Unchanged and kept: notes are never trimmed (arm 3), the dialects agree on what a budget buys (arm 2), the stated budget +# is echoed as budget_tokens on every rung (sweep), est_tokens is always served, L3 inertness, determinism, G4. This gate +# never had a --max-tokens arm (that knob shapes the map, not --for). The CEILING MARGIN history below describes the RETIRED +# hard-ceiling regime; it is kept as the record of why the rungs sit where they do. +# # What the audit found: `used +=` in packSignatures added the doc and the signature and nothing else, # while appendNoteChildren streamed children straight into the writer for free. On a note-heavy # tree the XML lens therefore ran far past the ceiling its own header reports — measured est_tokens 1279 @@ -174,6 +201,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" echo "fornotesbudgetcheck: BIN=$BIN" @@ -222,34 +250,191 @@ done TASK="widget dispatcher pipeline stage" -xmlEst(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" 2>/dev/null | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9'; } -xmlRows(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" 2>/dev/null | grep -o '/dev/null \ - | python3 -c 'import sys,json; print(json.load(sys.stdin)["est_tokens"])' 2>/dev/null; } -jsonRows(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" --json 2>/dev/null \ - | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d["sigs"]))' 2>/dev/null; } # P7: flat sigs array +# ── forbudget-soft (rule 4): the soft-target reader every replaced arm below uses ───────────────────────────────────────── +# soft FILE N ROWSOUT [REF [PREV]] prints ONE line, "OK|BAD rows= total= charged= T= est= over= echo= next= notes= fits= maximal= upper= rp=": +# rows / total the served rows / the ranked head ( | "sigs_total") +# charged what the soft target is charged against. XML: the bytes inside — every WITH its children +# — plus the zero cx=/ccx=/in= the lean spelling elides (serialize.h entryCost charges them). JSON: the +# emitted "sigs" array, notes included (jsonSigEntryCost is that array's exact byte model). +# T N x 2.36 x 0.90 (kMinBytesPerToken x kBudgetHeadroom) +# est / over / echo est_tokens ("-" when absent) / over_ceiling= on the ROOT / budget_tokens == N on the root +# next a cut (rows < total) names its continuation ( | "sigs_next"); 1 when nothing was cut +# notes served rows that carry a note +# fits charged <= T, or only the rank 1..4 floor is served — and the floor is never short (rows >= min(4,total)) +# upper with REF, FIX2 C1: the maximality twin, rows past the floor stop where T less the other sections and the +# tail reserve runs out (test/lib/softbudget.py upper_bound; a tail-reserve-zero binary is RED here) +# maximal with REF (the SAME dialect's no-budget answer): a cut stops only where REF's next row would cross T +# (160 B slack, as forsoftbudgetcheck's (ST)) — a binary that stops early at a hard ceiling is not ST +# rp RP: the rows are a non-empty rank prefix of REF's rows, and the rows file PREV (when named: the next +# narrower rung's ROWSOUT) is a prefix of these rows — monotone in N. A named PREV that is missing is a 0. +# (One python3 per answer, the fields read with bash's own regex: this machine class pays ~0.1 s per process spawn, and the +# sweep below reads 393 answers — the reader does in-process what a pipe of head/wc/sed would spawn for.) +# The verdict is OK when est is numeric and fits, maximal, upper and next all hold. ROWSOUT gets the rows, p:l:n in rank (r=) order. +# exit 3 = the answer has no block / "sigs" array: a missing premise, FAILed by the caller, never skipped. +cat > "$TMP/soft.py" <<'PY' +import json, re, sys +def read( path ): + s = open( path, encoding='utf-8', errors='replace' ).read() + rows = [] + if s.lstrip().startswith( '{' ): + try: d = json.loads( s ) + except Exception: return None + i = s.find( '"sigs":[' ) + if i < 0 or not isinstance( d.get( 'sigs' ), list ): return None + _, end = json.JSONDecoder().raw_decode( s, i + 7 ) + charged = len( s[ i + 7:end ].encode() ) + for e in d[ 'sigs' ]: + r = e.get( 'r' ) + cost = len( json.dumps( e, separators=( ',', ':' ), ensure_ascii=False ).encode() ) + 1 + rows.append( ( r if isinstance( r, int ) else 10**9, "%s:%s:%s" % ( e.get( 'p' ), e.get( 'l' ), e.get( 'n' ) ), cost, + 1 if e.get( 'notes' ) else 0 ) ) + total = d.get( 'sigs_total', len( rows ) ) + nxt = bool( d.get( 'sigs_next' ) ) + over = d.get( 'over_ceiling' ) is True + est = d.get( 'est_tokens' ); est = str( est ) if isinstance( est, int ) else '-' + bt = d.get( 'budget_tokens' ) + else: + root = re.search( r']*>', s ) + m = re.search( r']*)>(.*?)', s, re.S ) + if not root or not m: return None + inner = m.group( 2 ); charged = len( inner.encode() ) + for x in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = x.group( 1 ) or x.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + elided = sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) + charged += elided + r = g( 'r' ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( 'p' ) + ":" + g( 'l' ) + ":" + g( 'n' ), len( x.group( 0 ).encode() ) + elided, + 1 if ' but + # plus the rows, less the --detail bodies (a root without bundle=); JSON, the "tail" object + if s.lstrip().startswith( "{" ): + try: return len( json.dumps( json.loads( s ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + u = ( len( s.split( "", 1 )[ 1 ].encode() ) - len( "" ) ) if "" in s else 0 + u += sum( len( h.encode() ) for h in re.findall( r"]*/>", s.split( "]*>", s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r"", s, re.S ) + u -= len( bm.group( 0 ).encode() ) if bm else 0 + return u +a = read( sys.argv[ 1 ] ) +if a is None: sys.exit( 3 ) +N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ) +# the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) +import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size +U = sections_u( open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() ) +if len( sys.argv ) > 4: + _at = open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read(); _rt = open( sys.argv[ 4 ], encoding='utf-8', errors='replace' ).read() + U += max( 0, tail_size( _rt ) - tail_size( _at ) ) +open( sys.argv[ 3 ], 'w' ).write( "".join( x[ 1 ] + "\n" for x in a[ 'rows' ] ) ) +k = len( a[ 'rows' ] ) +fits = ( a[ 'charged' ] <= X or k <= 4 ) and k >= min( 4, a[ 'total' ] ) +cut = k < a[ 'total' ] +nxt = a[ 'next' ] or not cut +maximal = upper = True +if len( sys.argv ) > 4: + ref = read( sys.argv[ 4 ] ) + if ref is None: sys.exit( 3 ) + if cut and k < len( ref[ 'rows' ] ): + maximal = a[ 'charged' ] + ref[ 'rows' ][ k ][ 2 ] > X - U - 160 + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve (rows over-served) + from softbudget import upper_bound + upper = upper_bound( _at, _rt, X, k, a[ 'total' ], U )[ 0 ] +mine = [ x[ 1 ] for x in a[ 'rows' ] ] +rp = len( sys.argv ) > 4 and k > 0 and mine == [ x[ 1 ] for x in ref[ 'rows' ] ][ :k ] +if rp and len( sys.argv ) > 5 and sys.argv[ 5 ]: + try: prev = [ x for x in open( sys.argv[ 5 ] ).read().split( "\n" ) if x ] + except OSError: prev = None + rp = prev is not None and len( prev ) > 0 and prev == mine[ :len( prev ) ] +ok = a[ 'est' ] != '-' and fits and maximal and upper and nxt +print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d echo=%d next=%d notes=%d fits=%d maximal=%d upper=%d rp=%d" + % ( "OK" if ok else "BAD", k, a[ 'total' ], a[ 'charged' ], X, a[ 'est' ], int( a[ 'over' ] ), int( a[ 'bt' ] == N ), + int( nxt ), sum( x[ 3 ] for x in a[ 'rows' ] ), int( fits ), int( maximal ), int( upper ), int( rp ) ) ) +PY +soft(){ python3 "$TMP/soft.py" "$@"; } +field(){ local re=" $2=([^ ]*)"; [[ " $1 " =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; } # field LINE KEY: one key= of a soft line +isnum(){ case "${1:-}" in ''|*[!0-9]*) return 1;; esac; return 0; } + +# The no-budget answers, one per dialect the arms below read: the REF each cut's maximality and rank prefix are measured +# against, and the DH arm's subject. +"$BIN" "$CORPUS" --for="$TASK" > "$TMP/def.xml" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" --legend=compact > "$TMP/def.compact" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" --json > "$TMP/def.json" 2>/dev/null +for dialect in xml compact json; do + dl="$( soft "$TMP/def.$dialect" 0 "$TMP/def_$dialect.rows" )" || { no "no-budget answer ($dialect) has no block — every soft arm below lacks its reference"; continue; } + dr="$( field "$dl" rows )" + isnum "$dr" && [ "$dr" -ge 5 ] || no "no-budget answer ($dialect) serves '$dr' rows — the reference cannot measure a cut past the floor" +done + +# ── arm DH (NEW, forbudget-soft (rule 4)): the default answer on a note-heavy tree serves the WHOLE head, every note intact ── +# The default --for stops when complete: kForPayloadBudgetBytes (7,500 B) is a TARGET whose row drop only runs past the +# 32,000 B runaway guard. This tree's 40-row head with a ~130 B note on every row is ~13 KB of : notes are charged +# against the target (the rows past it are DISCLOSED as past_target=) and never cost a row, never get trimmed, never +# label the answer over_ceiling=. HEAD d099ec21: 40 of 40 rows, 40 notes, past_target=20, both dialects. RED on the base +# binary (d4325a8c): 20 of 40 rows, no past_target= (the 7.5 KB ceiling's row drop). +for dialect in xml json; do + dl="$( soft "$TMP/def.$dialect" 0 /dev/null )" || { no "(DH) default ($dialect): no block"; continue; } + dr="$( field "$dl" rows )"; dt="$( field "$dl" total )"; dn="$( field "$dl" notes )"; dov="$( field "$dl" over )" + case "$dialect" in json) pt="$( python3 -c 'import sys,json; print(json.load(open(sys.argv[1])).get("sigs_past_target",""))' "$TMP/def.json" 2>/dev/null )" ;; + *) pt="$( grep -o ']*>' "$TMP/def.xml" | head -1 | sed -n 's/.*past_target="\([0-9]*\)".*/\1/p' )" ;; esac + if isnum "$dr" && isnum "$dt" && isnum "$dn" && [ "$dr" -eq "$dt" ] && [ "$dn" -eq "$dr" ] && [ "$dov" = 0 ] && isnum "$pt" && [ "$pt" -ge 1 ]; then + ok "(DH) default ($dialect): the whole head ships ($dr of $dt rows, $dn notes), $pt row(s) disclosed past_target, no over_ceiling" + else + no "(DH) default ($dialect): rows=$dr total=$dt notes=$dn over=$dov past_target='$pt' — the default must ship the whole head with every note, past the target disclosed, unlabelled" + fi +done -# ── arm 1: est_tokens must fit the ceiling the user asked for, in BOTH dialects ──────────────────── +# ── arm 1: notes are CHARGED against the budget, in BOTH dialects ───────────────────────────────── +# REPLACED — forbudget-soft (rule 4). Was: "est_tokens <= --token-budget" in XML and JSON at 1200/1640/3000 (the whole +# document fit the ceiling). Under the soft target est_tokens may exceed N by the never-charged header (HEAD d099ec21: XML +# 1564 / 1940 / 3085, JSON 1232 at 1200), so that bar asserts the deleted contract. What W3-N2 means now, asserted here at +# the same three rungs in both dialects: ST (the rows — each WITH its note — fit T = N x 2.36 x 0.90 or are the floor, and +# the cut is maximal), NOTES (every served row carries its note), NOLBL (no over_ceiling=), est_tokens still served. +# MEASURED (HEAD d099ec21, this fixture in a scratch dir), rows / charged bytes of T: 1200 -> 7 / 2338 of 2548 (JSON 7 / +# 2371), 1640 -> 10 / 3322 of 3483 (JSON 10 / 3388), 3000 -> 19 / 6280 of 6372 (JSON 18 / 6106); a note on every row. +# RED on the base binary (d4325a8c): 1200 -> XML 4 rows (1355 B), JSON 6 (2033 B); 1640 -> XML 7; 3000 -> 16 / 17 — every +# one a cut a whole row short of T (not maximal: the hard ceiling reserved the header the soft target no longer charges). # (tight budget 1100, re-anchored 2026-09-11 — see the CEILING MARGIN block above for the arithmetic) # RE-ANCHORED 2026-10-10 (train 26c): the tight rung 1100 -> 1200. lego-charge charges / at their served size # and #362's rides the bundle, so at 1100 this corpus's XML answer is the 4-row floor with its next= unpaid # (est_tokens 1213, over_ceiling="1" + the unpaid clause: disclosed, not silent — the knob-honesty contract, not the leak # this arm pins). Swept on the merged binary: 1150 / 1200 serve 4 rows at est_tokens=1121 (JSON 1098), 1250..1350 five rows # at 1246; 1200 sits mid-plateau with 79 tokens of headroom, one step clear of each edge. 1640 and 3000 unchanged. -for tb in 1200 1640 3000; do - xe="$( xmlEst "$tb" )"; je="$( jsonEst "$tb" )" - if [ -z "$xe" ] || [ -z "$je" ]; then no "budget=$tb: could not read est_tokens from one of the dialects (xml='$xe' json='$je')"; continue; fi - if [ "$xe" -le "$tb" ]; then ok "budget=$tb: XML est_tokens=$xe fits the ceiling" - else no "budget=$tb: XML est_tokens=$xe blows the ceiling by $(( 100 * ( xe - tb ) / tb ))% — note bytes are not charged"; fi - if [ "$je" -le "$tb" ]; then ok "budget=$tb: JSON est_tokens=$je fits the ceiling" - else no "budget=$tb: JSON est_tokens=$je blows the ceiling — the reference side regressed"; fi +for tb in 1200 1640 3000 900; do + "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" > "$TMP/n_$tb.xml" 2>/dev/null + "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json > "$TMP/n_$tb.json" 2>/dev/null + [ "$tb" = 900 ] && continue # 900 is read by arm 4's notes-vs-no-notes comparison (and by its CF twin), not asserted here + for dialect in xml json; do + sl="$( soft "$TMP/n_$tb.$dialect" "$tb" "$TMP/n_${tb}_$dialect.rows" "$TMP/def.$dialect" )" \ + || { no "budget=$tb ($dialect): no block in the answer or its reference — the charge cannot be measured"; continue; } + sr="$( field "$sl" rows )"; sn="$( field "$sl" notes )" + case "$sl" in OK*) ok "(ST) budget=$tb ($dialect): the rows WITH their notes fit the target — $sl" ;; + *) no "(ST) budget=$tb ($dialect): $sl — note bytes ride the rows and must be charged with them: fit T (or the floor), and stop only where the next row would cross it" ;; esac + if isnum "$sr" && isnum "$sn" && [ "$sr" -ge 1 ] && [ "$sn" -eq "$sr" ]; then ok "(NOTES) budget=$tb ($dialect): every served row carries its note ($sn of $sr) — charged, not dropped to make room" + else no "(NOTES) budget=$tb ($dialect): $sn note(s) on $sr row(s) — a note was dropped instead of charged"; fi + case "$sl" in *" over=0 "*) ok "(NOLBL) budget=$tb ($dialect): no over_ceiling= on a soft target" ;; + *) no "(NOLBL) budget=$tb ($dialect): over_ceiling= on a budget_tokens answer — a soft target is never 'over' ($sl)" ;; esac + done done # ── arm 2: the two dialects select COMPARABLE row counts (they need not be equal) ────────────────── # Before the fix the XML lens bought 2-2.4x the rows with the same budget, because notes were free. +# (forbudget-soft: the rows are read off arm 1's answers — the same argv, so one run each instead of two.) for tb in 1200 1640 3000; do - xr="$( xmlRows "$tb" )"; jr="$( jsonRows "$tb" )" - if [ -z "$jr" ] || [ "$jr" -eq 0 ]; then no "budget=$tb: JSON selected no rows — the comparison has no denominator"; continue; fi + xr="$( grep -c . "$TMP/n_${tb}_xml.rows" 2>/dev/null )"; jr="$( grep -c . "$TMP/n_${tb}_json.rows" 2>/dev/null )" + if ! isnum "$xr" || ! isnum "$jr" || [ "$jr" -eq 0 ]; then no "budget=$tb: JSON selected no rows (xml='$xr' json='$jr') — the comparison has no denominator"; continue; fi if [ "$xr" -le $(( jr * 13 / 10 + 1 )) ] && [ "$xr" -ge $(( jr * 7 / 10 )) ]; then ok "budget=$tb: XML $xr rows vs JSON $jr rows — the two dialects agree on what fits" else @@ -268,7 +453,18 @@ fullText="$( printf '%s' "$OUT800" | grep -c "stage table downstream" )" if [ "$fullText" -ge 1 ]; then ok "a surviving note carries its FULL text (no note-level truncation)" else no "a surviving note lost its tail — notes must be charged, never trimmed"; fi -# ── arm 6: an explicit ceiling is NEVER silently overshot — every dialect, every rung in the band ── +# ── arm 6: every rung of the band, every dialect: notes charged against a SOFT target — swept, not sampled ── +# REPLACED — forbudget-soft (rule 4). Was, per rung and dialect: "est_tokens <= --token-budget OR over_ceiling=1 on the +# root" (HEAD d099ec21: 322 of 393 runs over and unlabelled — by design: est_tokens may exceed N, and a soft target is never +# labelled over). The per-rung invariant now, over the SAME band (700..2000), the SAME step (10) and the SAME three dialects +# (393 runs): ST (the rows, notes included, fit T or are the floor; a cut is maximal against that dialect's no-budget answer; +# est_tokens served; the cut names its next=) + NOLBL (no over_ceiling=) + RP (rows a rank prefix of the no-budget rows, and +# rows(N-10) a prefix of rows(N): monotone) + CF (the default and --legend=compact XML dialects serve the same rows). The +# budget_tokens ECHO half of the arm is unchanged. The history below records the retired disjunction and why the band +# starts at 700; the band, the step and the dialects are kept as they were (the density is the arm's value, CHECKLIST 14). +# MEASURED: HEAD d099ec21 green on all 393 runs (every rung a cut; the 40-row head is never complete below 2000). RED on base +# (d4325a8c): 304 runs not maximal (the hard ceiling's header reserve), 87 labelled over_ceiling= (first: 700, xml); RP, CF +# and the echo are green on both. # PLACED HERE, before arm 4, deliberately: this arm needs the NOTE-BEARING corpus (notes are the exempt- # adjacent bytes that make the ladder's rungs tight enough to overshoot at all), and arm 4 deletes # .ripwire_notes to test inertness. Moving it below arm 4 would make it green for the wrong reason. @@ -320,48 +516,62 @@ else no "a surviving note lost its tail — notes must be charged, never trimmed # --legend=compact. This arm is the regression lock on that rule, swept rather than sampled, because the # rung a single sample happens to sit on is exactly what made the defect survive four earlier re-anchors of # the middle rung above: 1550 and 1600 are both inside a FLAT stretch, and the edge is at ~1610. -sweepFail=0; sweepOver=0; sweepFirstRed=""; sweepRungs=0; sweepEchoFail=0; sweepEchoFirst="" +sweepRungs=0; sweepEchoFail=0; sweepEchoFirst="" +stFail=0; stFirst=""; lblFail=0; lblFirst=""; rpFail=0; rpFirst=""; cfFail=0; cfFirst=""; sweepCut=0; upFail=0; upFirst="" tb=700 while [ "$tb" -le 2000 ]; do sweepRungs=$(( sweepRungs + 1 )) for dialect in xml compact json; do case "$dialect" in - xml) out="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" 2>/dev/null )" ;; - compact) out="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --legend=compact 2>/dev/null )" ;; - json) out="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json 2>/dev/null )" ;; - esac - case "$dialect" in - json) est="$( printf '%s' "$out" | grep -o '"est_tokens":[0-9]*' | head -1 | tr -dc '0-9' )" - case "$out" in *'"over_ceiling":true'*) lab=1 ;; *) lab=0 ;; esac - case "$out" in *"\"budget_tokens\":$tb"*) echoed=1 ;; *) echoed=0 ;; esac ;; - *) est="$( printf '%s' "$out" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" - case "$out" in *'over_ceiling="1"'*) lab=1 ;; *) lab=0 ;; esac - case "$out" in *"budget_tokens=\"$tb\""*) echoed=1 ;; *) echoed=0 ;; esac ;; + xml) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" > "$TMP/sw.$dialect" 2>/dev/null ;; + compact) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --legend=compact > "$TMP/sw.$dialect" 2>/dev/null ;; + json) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json > "$TMP/sw.$dialect" 2>/dev/null ;; esac - if [ "$echoed" -eq 0 ]; then - sweepEchoFail=$(( sweepEchoFail + 1 )) - [ -z "$sweepEchoFirst" ] && sweepEchoFirst="budget=$tb ($dialect)" - fi - if [ -z "$est" ]; then - no "sweep budget=$tb ($dialect): no est_tokens in the answer — the price a budget shapes against must always be served" - sweepFail=$(( sweepFail + 1 )) - continue - fi - if [ "$est" -gt "$tb" ]; then - sweepOver=$(( sweepOver + 1 )) - if [ "$lab" -eq 0 ]; then - sweepFail=$(( sweepFail + 1 )) - [ -z "$sweepFirstRed" ] && sweepFirstRed="budget=$tb ($dialect) est_tokens=$est, $(( est - tb )) over, NO over_ceiling" - fi + cur="$TMP/sw_$(( sweepRungs % 2 ))_$dialect.rows"; prv="$TMP/sw_$(( ( sweepRungs + 1 ) % 2 ))_$dialect.rows" + [ "$tb" -eq 700 ] && prv="" + if ! sl="$( soft "$TMP/sw.$dialect" "$tb" "$cur" "$TMP/def.$dialect" "$prv" )"; then + no "sweep budget=$tb ($dialect): no block in the answer or its reference — the price and the rows a budget shapes must always be served" + stFail=$(( stFail + 1 )); continue fi + case "$sl" in *" echo=1 "*) ;; *) + sweepEchoFail=$(( sweepEchoFail + 1 )) + [ -z "$sweepEchoFirst" ] && sweepEchoFirst="budget=$tb ($dialect)" ;; esac + case "$sl" in OK*) ;; *) + stFail=$(( stFail + 1 )) + [ -z "$stFirst" ] && stFirst="budget=$tb ($dialect) $sl" ;; esac + case "$sl" in *" upper=1 "*) ;; *) + upFail=$(( upFail + 1 )) + [ -z "$upFirst" ] && upFirst="budget=$tb ($dialect) $sl" ;; esac + case "$sl" in *" over=0 "*) ;; *) + lblFail=$(( lblFail + 1 )) + [ -z "$lblFirst" ] && lblFirst="budget=$tb ($dialect) $sl" ;; esac + case "$sl" in *" rp=1"*) ;; *) + rpFail=$(( rpFail + 1 )) + [ -z "$rpFirst" ] && rpFirst="budget=$tb ($dialect): the rows are not a rank prefix of the no-budget rows, or rows(N=$(( tb - 10 ))) are not a prefix of them — $sl" ;; esac + [ "$( field "$sl" rows )" != "$( field "$sl" total )" ] && sweepCut=$(( sweepCut + 1 )) done + if ! cmp -s "$TMP/sw_$(( sweepRungs % 2 ))_xml.rows" "$TMP/sw_$(( sweepRungs % 2 ))_compact.rows"; then + cfFail=$(( cfFail + 1 )) + [ -z "$cfFirst" ] && cfFirst="budget=$tb: the default and --legend=compact rows differ" + fi tb=$(( tb + 10 )) done -if [ "$sweepFail" -eq 0 ]; then - ok "sweep 700..2000 step 10 x 3 dialects ($(( sweepRungs * 3 )) runs): every overshoot is LABELLED ($sweepOver of $(( sweepRungs * 3 )) rungs ran over the stated ceiling, every one carrying over_ceiling)" +runs=$(( sweepRungs * 3 )) +if [ "$stFail" -eq 0 ]; then + ok "(ST) sweep 700..2000 step 10 x 3 dialects ($runs runs): the rows WITH their notes fit T = N x 2.36 x 0.90 (or are the floor), every cut maximal and naming next= ($sweepCut cut)" else - no "sweep 700..2000 step 10 x 3 dialects: $sweepFail SILENT overshoot(s) — first: $sweepFirstRed" + no "(ST) sweep 700..2000 step 10 x 3 dialects: $stFail run(s) broke stop-at-target — first: $stFirst" fi +# (UP) FIX2 C1 (rule 4): the upper-bound twin of maximality, counted on its own: a binary that ignores the tail reserve serves +# rows past T less the sections and that reserve, and both fits (rows <= T) and the relaxed maximality stay green on it. +if [ "$upFail" -eq 0 ]; then ok "(UP) sweep 700..2000 step 10 x 3 dialects ($runs runs): no cut past the floor serves rows into the tail reserve" +else no "(UP) sweep 700..2000 step 10 x 3 dialects: $upFail run(s) served rows past T less the sections and the tail reserve — first: $upFirst"; fi +if [ "$lblFail" -eq 0 ]; then ok "(NOLBL) sweep 700..2000 step 10 x 3 dialects ($runs runs): no over_ceiling= on any soft target" +else no "(NOLBL) sweep 700..2000 step 10 x 3 dialects: $lblFail run(s) labelled a soft target over_ceiling= — first: $lblFirst"; fi +if [ "$rpFail" -eq 0 ]; then ok "(RP) sweep 700..2000 step 10 x 3 dialects ($runs runs): rows are a rank prefix of the no-budget rows, monotone in N" +else no "(RP) sweep 700..2000 step 10 x 3 dialects: $rpFail run(s) broke the rank prefix — first: $rpFirst"; fi +if [ "$cfFail" -eq 0 ]; then ok "(CF) sweep 700..2000 step 10: the default and --legend=compact dialects serve the same rows on all $sweepRungs rungs" +else no "(CF) sweep 700..2000 step 10: $cfFail rung(s) where the legend changed the rows — first: $cfFirst"; fi if [ "$sweepEchoFail" -eq 0 ]; then ok "sweep 700..2000 step 10 x 3 dialects: every rung ECHOES the ceiling it was shaped against (budget_tokens on the root)" else @@ -375,24 +585,52 @@ rm -f "$CORPUS/.ripwire_notes" # postures fit — 808 / 817 — and 900 holds 865 in the full legend with margin). The notes corpus's 800 runs above are unchanged. bare="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 2>/dev/null )" case "$bare" in *" element";; *) ok "a tree with no notes emits none (L3 inertness)";; esac -# L1 (2026-09-19): the 800-token fit was calibrated in the full legend, the default when it was written, and is asked -# for by name. The DEFAULT (compact) answer on this tree carries more rows and lands over 800 at this one budget; that is -# the --for sig ledger's exemption design (disclosure clauses exempt from the sig charge, recovered by rung zero, which -# the compact dialect's shorter clauses rarely pay for). RE-MEASURED in the L1 fix round with a script, not a count typed -# here: 66 budgets (300..3550 step 50) on the P4-shape fixtures, the default is over on 15 (72 functions) and 15 (12 -# functions), --legend=full on 11 and 12; the earlier "8 of 66 vs 2, 55 before" did not reproduce. What the default MUST do -# there is say so — asserted below. -bareFull="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 --legend=full 2>/dev/null )" -bareEst="$( printf '%s' "$bareFull" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -n "$bareEst" ] && [ "$bareEst" -le 900 ]; then ok "no-notes tree also fits the ceiling in the full legend (est_tokens=$bareEst)" -else no "no-notes tree reports est_tokens='$bareEst' against a budget of 900 (full legend)"; fi -defEst="$( printf '%s' "$bare" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -z "$defEst" ]; then no "no-notes tree at the default posture carries no est_tokens=" -elif [ "$defEst" -le 900 ]; then ok "no-notes tree at the default posture fits (est_tokens=$defEst)" -else - case "$bare" in *'over_ceiling="1"'*) ok "no-notes tree at the default posture is over 900 (est_tokens=$defEst) and SAYS so (over_ceiling=\"1\")" ;; - *) no "no-notes tree at the default posture is over 900 (est_tokens=$defEst) in SILENCE" ;; esac -fi +# L1 (2026-09-19) — the retired hard-ceiling reading of this arm: the full legend fit 900, the default (compact) answer +# carried more rows and was over 900, which it had to SAY (over_ceiling="1"). +# REPLACED — forbudget-soft (rule 4). Was: "--legend=full est_tokens <= 900" and "default est_tokens <= 900, or over and +# labelled over_ceiling=1" (HEAD d099ec21: 1736 / 1304, unlabelled — by design). Now, on the no-notes tree at 900, both +# postures: ST (the rows fit T, maximal against the no-budget answer, next= on the cut) + NOLBL; and CF — the two legends +# serve the SAME rows (the retired ladder let the compact dialect's shorter clauses buy it more rows than the full legend). +# Then the W3-N2 twin this tree exists for: at every rung arm 1 measured (and 900), in both dialects, the notes tree served +# NO MORE rows than this tree without notes — and strictly fewer on at least one, so the charge is real, not vacuous. +# MEASURED (HEAD d099ec21): no-notes 900 -> 13 rows in both legends (1786 B of T 1911); notes vs no-notes rows: 900 5/13, +# 1200 7/18 (JSON 7/17), 1640 10/25 (10/24), 3000 19/40 (18/40). RED on base (d4325a8c): 7 rows default vs 6 full (CF), each a +# cut short of T (ST). The NOTES twins are green on both binaries — W3-N2 held before the soft target and holds after it. +"$BIN" "$CORPUS" --for="$TASK" --token-budget=900 --legend=full > "$TMP/b_900.full" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" > "$TMP/b_def.xml" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" --json > "$TMP/b_def.json" 2>/dev/null +printf '%s' "$bare" > "$TMP/b_900.xml" +for posture in xml full; do + if sl="$( soft "$TMP/b_900.$posture" 900 "$TMP/b_900_$posture.rows" "$TMP/b_def.xml" )"; then + case "$sl" in OK*) ok "(ST) no-notes tree, budget=900 ($posture legend): the rows fit the target — $sl" ;; + *) no "(ST) no-notes tree, budget=900 ($posture legend): $sl" ;; esac + case "$sl" in *" over=0 "*) ok "(NOLBL) no-notes tree, budget=900 ($posture legend): no over_ceiling= on a soft target" ;; + *) no "(NOLBL) no-notes tree, budget=900 ($posture legend): over_ceiling= on a soft target ($sl)" ;; esac + else + no "no-notes tree, budget=900 ($posture legend): no block in the answer or the no-budget reference" + fi +done +if [ -s "$TMP/b_900_xml.rows" ] && cmp -s "$TMP/b_900_xml.rows" "$TMP/b_900_full.rows"; then ok "(CF) no-notes tree, budget=900: the default and full legends serve the same $( grep -c . "$TMP/b_900_xml.rows" ) rows" +else no "(CF) no-notes tree, budget=900: default rows ($( grep -c . "$TMP/b_900_xml.rows" 2>/dev/null )) != full-legend rows ($( grep -c . "$TMP/b_900_full.rows" 2>/dev/null )) — the legend bought rows"; fi +fewer=0 +for tb in 900 1200 1640 3000; do + for dialect in xml json; do + if [ ! -s "$TMP/b_$tb.$dialect" ]; then + case "$dialect" in json) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json > "$TMP/b_$tb.$dialect" 2>/dev/null ;; + *) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" > "$TMP/b_$tb.$dialect" 2>/dev/null ;; esac + fi + nl="$( soft "$TMP/n_$tb.$dialect" "$tb" /dev/null )"; bl="$( soft "$TMP/b_$tb.$dialect" "$tb" /dev/null )" + nr="$( field "$nl" rows )"; br="$( field "$bl" rows )"; nn="$( field "$nl" notes )" + if ! isnum "$nr" || ! isnum "$br" || ! isnum "$nn" || [ "$nn" -lt 1 ]; then + no "(NOTES) budget=$tb ($dialect): could not read both row counts with notes on the notes tree (notes tree '$nr' rows / '$nn' notes, no-notes tree '$br')"; continue + fi + if [ "$nr" -le "$br" ]; then ok "(NOTES) budget=$tb ($dialect): the notes tree serves $nr rows, the same tree without notes $br — notes are charged like the rows they ride" + else no "(NOTES) budget=$tb ($dialect): the notes tree serves $nr rows, MORE than the same tree without notes ($br) — note bytes are not charged"; fi + [ "$nr" -lt "$br" ] && fewer=$(( fewer + 1 )) + done +done +if [ "$fewer" -ge 1 ]; then ok "(NOTES) the notes tree served strictly fewer rows on $fewer of 8 rung/dialect pairs — the charge is real" +else no "(NOTES) the notes tree never served fewer rows than the no-notes tree on any of 8 rung/dialect pairs — notes cost nothing (W3-N2)"; fi # ── arm 5: still deterministic and well-formed after the accounting change ───────────────────────── if [ "$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 2>/dev/null )" = "$bare" ]; then ok "output is byte-identical run-to-run" diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 9f0faf355..19d25cdc6 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -138,7 +138,22 @@ REPO_Q=( "rank graph teleport" "compact legend rewrite" "edit receipt post-check # replace, so the byte-shaped section fits five more signature rows, and the legend that grew 154 B (the sc= # rule, the route= code's reading and the merged-callee reading) is paid for several times over in rows. The other # nine stay at their for-widen bases: -1.11%, +0.36%, -0.45%, +1.54%, +0.02%, +1.66%, +0.18%, -0.56%, +2.30%. -REPO_BASE=( 10134 10029 10642 10102 9880 9397 10009 10070 9966 10379 ) +# q6 q7 q8 RE-PINNED 2026-10-15 (lane forbudget-soft FIX1, review rv-forbudget-soft N2; rule 4, a stated by-design break — the 4% rule +# itself is unchanged): 9397 -> 9947, 10009 -> 10496, 10070 -> 10605 B, i.e. the answer LESS its past-target rows, its hop bytes +# over the old 1,000 B allowance, its tail page rows and the tail clause growth (the allowances below), measured on this lane's +# build at this tree (+5.85% / +4.87% / +5.31% over the old pins). The review measured the same growth on the base tree (+6.42% / +# +4.81% / +5.25%) and attributed it to the lane: decision D-3 (the default serves every hop row — the part inside the old +# 1,000 B allowance is not subtracted) and the default regime's header clauses (budget_bytes= read as a target, past_target=). +# The other seven are unmoved. Train 28 re-measures attrib-multidef's --for piece against these pins. +# FIX2 N2 (review rv-forbudget-soft-fix1 N-a): those three pins were read on a +dirty tree, whose root at= stamp carries 6 B +# more than a clean one (a clean tree read -0.06% on each). Re-taken from a CLEAN committed tree (the lane head, no local edits) +# with this lane's build: 9947 -> 9941, 10496 -> 10490, 10605 -> 10599 B. The by-design reason above is unchanged. +REPO_BASE=( 10134 10458 10642 10102 9880 9941 10490 10599 9966 10379 ) +# q2 RE-PINNED 2026-10-16 (train 28, owner ruling; rule 4 by-design break, the 26c q3 precedent): 10029 -> 10458 B net of +# past-target/hop/tail-page rows (+4.28% on the merged stack), an OUTPUT change of two signed lanes that each stay inside the +# 4% alone. Measured on the merged tree: for-body-slot's binary (forbudget-soft + the owner body) 10,368 B (+3.38%); the +# attrib-multidef merge adds +90 B — the compact split reading "; c prov=split: ..." in the header and prov="split" on the +# two rows of a real split arm (an intended disclosure). q6-q8 are not touched (+0.84% / +0.99% / +3.12% on their pins). # q3 RE-PINNED 2026-10-10 (train 26c): 10080 -> 10642 B (+5.58% on the merged stack), an OUTPUT change of two signed lanes # that each stay inside the 4% alone: lego-charge serves two more signature rows (25 -> 27 of 40) because / # are charged to the budget at the size they are SERVED (a collapsing section its stub), and owner-hop adds the @@ -187,10 +202,28 @@ for q in "${REPO_Q[@]}"; do out="$( "$BIN" . --for="$q" 2>/dev/null )" now="$( printf '%s' "$out" | wc -c | tr -d ' ' )" marker="$( printf '%s' "$out" | grep -o ']*>' | head -1 )" - pct="$( python3 -c "print( '%+.2f' % ( ( $now - $base ) * 100.0 / $base ) )" )" - printf ' q%-2d %-28s %5d → %5d (%s%%) %s\n' "$i" "'$q'" "$base" "$now" "$pct" "$marker" - if [ "$now" -gt $(( base * 104 / 100 )) ]; then - no "(3) repo q$i '$q': $now B > 1.04 × $base B"; growth_fail=1 + # forbudget-soft (stop when complete; PROCESS rule 5's stair-step target): rows served past the 7.5 KB target are disclosed + # (: the last K rows) and are what the answer needs, so the band applies to the answer WITHOUT them — + # every byte over 4% must be a disclosed past-target row, or a row past the old 1,000 B allowance (decision D-3: + # the default serves every hop row), or — forbudget-soft round C, FOLD C (owner decision 2026-10-14) — a tail PAGE row + # (``: page 0 of the --for file page now rides the answer), never anything else + past="$( printf '%s' "$out" | python3 -c ' +import re, sys +s = sys.stdin.read(); m = re.search( r"]*)>(.*?)", s, re.S ) +k = re.search( r"past_target=\"([0-9]+)\"", m.group( 1 ) ) if m else None +rows = sorted( ( int( re.search( r"\br=\"([0-9]+)\"", d.group( 1 ) ).group( 1 ) ), len( d.group( 0 ).encode() ) ) + for d in re.finditer( r"]*)>.*?", m.group( 2 ) if m else "", re.S ) ) +past = sum( b for _, b in rows[ len( rows ) - int( k.group( 1 ) ): ] ) if k else 0 +h = re.search( r"", s, re.S ) # D-3: the compact is no longer cut at its 1,000 B allowance +page = sum( len( t.encode() ) for t in re.findall( r"", s ) ) # FOLD C: the tail page rows +for pat, was in ( ( r"; tail: [^\]]*?budget-trimmed row\)", 391 ), ( r"; t p= file outside sigs \(weaker\)[^;]*?r= rank \(gap = trimmed\)", 57 ) ): + lm = re.search( pat, s ) # …and the growth of the clause that now defines them (full 391 B / compact 57 B before) + page += max( 0, len( lm.group( 0 ).encode() ) - was ) if lm else 0 +print( past + page + ( max( 0, len( h.group( 0 ).encode() ) - 1000 ) if h else 0 ) )' )" + pct="$( python3 -c "print( '%+.2f' % ( ( $now - $past - $base ) * 100.0 / $base ) )" )" + printf ' q%-2d %-28s %5d → %5d - past-target/hops %5d (%s%%) %s\n' "$i" "'$q'" "$base" "$now" "$past" "$pct" "$marker" + if [ $(( now - past )) -gt $(( base * 104 / 100 )) ]; then + no "(3) repo q$i '$q': $now B - $past B past-target/hop rows > 1.04 × $base B"; growth_fail=1 fi done # nine fixture bundles measured on the pre-fix binary (git-less copies; d5ac29a7) @@ -423,11 +456,14 @@ 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. Under a tight explicit (hard) -# 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 ). +# parameter lists, past the default target. 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. JSON + MCP. Negatives: an uncapped tag (arm 8's +# fixture) carries none; --no-route is echoed. RED on 255dc199 (no next= on ). +# forbudget-soft (owner decisions 2026-10-14; rule 4 replacements): the DEFAULT ships the whole head — the target only shrinks +# row text (capped="1" with shown == total, past_target= the rows past it), so its next= serves the rows UNSHRUNK and it has no +# resume index (nothing was dropped). The row CUT this arm's resume-index and paste arms need is an explicit soft target's: +# --token-budget=1500 is exempt now (next= with its clause and next_offset=, no over_ceiling — a soft target is never "over"), +# and a wide target (8000) serves the default's rows rather than a frozen byte-identical block. mkdir -p "$TMP/fxcut" python3 - "$TMP/fxcut" <<'PY' import sys @@ -454,6 +490,7 @@ out = { "capped": "capped=\"1\"" in tag, "shown": int( sh.group( 1 ) ) if sh els "clause": "[sigs next=:" in s[ : s.find( "]*>", s ); r = root.group( 0 ) if root else "" for k in ( "est_tokens", "budget_tokens" ): @@ -462,10 +499,10 @@ 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 ))" +if [ "$( cfield "$C1" capped )" = true ] && [ "$( cfield "$C1" shown )" = "$( cfield "$C1" total )" ] && [ "$( cfield "$C1" past_target )" -gt 0 ]; then + ok "(9) presence guard: the default ships the whole head past its target, shrunk ($( cfield "$C1" shown ) of $( cfield "$C1" total ), past_target=$( cfield "$C1" past_target ))" else - no "(9) presence guard: the fixture is not cut at the default ceiling: $C1" + no "(9) presence guard: the fixture's default is not a whole head shrunk past its target: $C1" fi NX1="$( cfield "$C1" next )" case "$NX1" in @@ -473,12 +510,11 @@ case "$NX1" in *) no "(9) the capped carries next='$NX1' (want --for='gadget assembler' --signatures-only --token-budget=N)" ;; esac 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" +# nothing dropped, so no resume index (the explicit cut below carries one) +if [ "$( cfield "$C1" next_offset )" = null ] && [ "$( cfield "$C1" offset_clause )" = false ]; then + ok "(9) the default's complete head carries no next_offset= (no row was dropped)" 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 )" + no "(9) the default's complete head carries next_offset='$( cfield "$C1" next_offset )' (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 ); } @@ -491,20 +527,25 @@ 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: CHARGED — still named, and est_tokens stays within the hard ceiling the root names +# a tight explicit (SOFT) target: the row cut — named with its clause, the resume index defined, never "over" 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" 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" \ +{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] && [ "$( cfield "$C3" over )" = false ] && [ "$( cfield "$C3" clause )" = true ] \ + && [ "$( cfield "$C3" shown )" -lt "$( cfield "$C3" total )" ]; } \ + && ok "(9) --token-budget=1500: the soft target's cut ($( cfield "$C3" shown ) of $( cfield "$C3" total )) carries next= and its clause, no over_ceiling" \ || 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" +# the resume index (the #362 candidate page's next_offset): the last printed row's candidate rank, defined by its own clause +if [ "$( cfield "$C3" next_offset )" = "$( cfield "$C3" max_r )" ] && [ "$( cfield "$C3" next_offset )" = "$( cfield "$C3" shown )" ] \ + && [ "$( cfield "$C3" offset_clause )" = true ]; then + ok "(9) next_offset=\"$( cfield "$C3" next_offset )\" is the resume index: the last printed row's rank (= shown, no pseudo slot here), defined" else - no "(9) --token-budget=8000: the block differs from the default's (the exempt regime moved rows or its next=)" + no "(9) next_offset='$( cfield "$C3" next_offset )' vs last printed r=$( cfield "$C3" max_r ), shown=$( cfield "$C3" shown ), clause=$( cfield "$C3" offset_clause )" fi +# a wide explicit target serves the default's rows (stop when complete: no pad, no cap below the default) +C8="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=8000 --no-cache 2>/dev/null | cut_arm )" +python3 -c 'import json,sys; a=json.loads(sys.argv[1]); b=json.loads(sys.argv[2]); sys.exit(0 if a["names"] and a["names"]==b["names"] and not a["over"] else 1)' "$C8" "$C1" \ + && ok "(9) --token-budget=8000: serves exactly the default's rows ($( cfield "$C8" shown )), no over_ceiling" \ + || no "(9) --token-budget=8000: rows differ from the default's (or over_ceiling): $C8" 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:-}" @@ -523,8 +564,9 @@ 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 +# the resume index rides a row CUT: the explicit soft target's (the default's head is complete) +JNO="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --json --token-budget=1500 --no-cache 2>/dev/null | 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 on the soft target's cut ('$JNO')" ;; *) set -- $JNO; if [ "$1" = "$2" ]; then ok "(9) JSON twin: sigs_next_offset=$1 (the resume index of the soft target's cut)"; 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'" @@ -540,11 +582,17 @@ for line in sys.stdin: 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" + && [ "$( cfield "$M1" shown )" = "$( cfield "$M1" total )" ] && [ "$( cfield "$M1" next_offset )" = null ]; then + ok "(9) MCP twin: the whole head, shrunk; no CLI argv on ; next_budget_tokens=\"$MNB\" defined; no next_offset= (nothing dropped)" else no "(9) MCP twin: ${M1:-}" fi +M1B="$( mcpfor ',"budget_tokens":1500' )" +if [ -n "$M1B" ] && [ "$( cfield "$M1B" next_offset )" = "$( cfield "$M1B" max_r )" ] && [ "$( cfield "$M1B" next_budget_tokens )" != null ] && [ "$( cfield "$M1B" over )" = false ]; then + ok "(9) MCP twin, budget_tokens=1500: the soft cut's next_offset=\"$( cfield "$M1B" next_offset )\" and next_budget_tokens=, no over_ceiling" +else + no "(9) MCP twin, budget_tokens=1500: ${M1B:-}" +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" @@ -552,24 +600,11 @@ 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 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. -# 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: -# the answer carries next_budget_tokens= and NO over_ceiling= (a binary that never pays serves 35 rows at est 3934, over). +# ── (10) MCP budget_tokens IS A SOFT TARGET (forbudget-soft, owner decisions 2026-10-14; rule 4: replaces the C3 rulings' arm) ── +# C3 (2026-10-07) ruled when a charged handle is paid from rows under a HARD budget; an explicit budget is a soft target now, so +# nothing is paid and nothing is labelled "over". What must hold instead, at 2000 / 2600 / 3800 on (9)'s fixture: the rows are +# a rank PREFIX of the MCP default's (whole head) and monotone in the budget, a cut carries next_budget_tokens=, and no +# over_ceiling= rides. Same relative path from $TMP as before (root="fxcut"). 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: @@ -577,30 +612,28 @@ for line in sys.stdin: 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 2600:21; do - bt="${spec%%:*}"; want="${spec##*:}" - 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 +prev_names='[]' +for bt in 2000 2600 3800; do + MB="$( mcprel "$bt" )" + v="$( python3 -c 'import json,sys +b=json.loads(sys.argv[1]); d=json.loads(sys.argv[2]); p=json.loads(sys.argv[3]) +ok = b["names"] == d["names"][:len(b["names"])] and b["names"][:len(p)] == p and not b["over"] \ + and ( b["shown"] == b["total"] or b["next_budget_tokens"] is not None ) +print(("OK" if ok else "BAD"), b["shown"], b["total"], b["over"], b["next_budget_tokens"])' "${MB:-{\}}" "${M1:-{\}}" "$prev_names" 2>/dev/null )" + case "$v" in + OK*) ok "(10) MCP budget_tokens=$bt: a rank prefix of the whole head, monotone, the cut continued, no over_ceiling ($v)" ;; + *) no "(10) MCP budget_tokens=$bt: ${v:-} — want a monotone rank prefix, next_budget_tokens= on a cut, no over_ceiling" ;; + esac + prev_names="$( cfield "${MB:-{\}}" names 2>/dev/null || echo '[]' )" done -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=3800: shown/capped/handle/over/est = ${*:-} — want a paid handle inside the budget" -fi -# ── (10c) THE at= STAMP IS READ ONCE PER RUN, WHICHEVER PAYMENT MODE SERVES (CodeRabbit on #383, verbs_for.h:3843) ───────────── -# A --token-budget run renders in up to three payment modes (unpaid, paid, unpaid again) before a byte reaches stdout; each +# ── (10c) THE at= STAMP IS READ ONCE PER RUN (CodeRabbit on #383, verbs_for.h:3843) ───────────── +# A --token-budget run rendered in up to three payment modes (unpaid, paid, unpaid again) before a byte reached stdout; each # render re-read the git stamp (rev-parse + status --porcelain), so a run over its ceiling either way spawned three -# `status --porcelain` children. The ranking is computed once per run and so is the stamp now. Fixture: fxcut as a one-commit -# git repository (the stamp only reads a git root); budget 500 against an answer of ~1100 est tokens is over either way -# (over_ceiling="1" — the premise); a PATH git shim logs every git call and runs the real git. RED on the pre-fix binary -# (3 status children); GREEN: exactly 1, and at= still rides the answer. +# `status --porcelain` children. The ranking is computed once per run and so is the stamp. forbudget-soft: one render per run +# now (no payment modes); the premise is a budgeted run whose rows the target cuts. Fixture: fxcut as a one-commit git +# repository (the stamp only reads a git root); a PATH git shim logs every git call and runs the real git. RED on 255dc199's +# successor before the fix (3 status children); GREEN: exactly 1, and at= still rides the answer. FXG="$TMP/fxcutgit"; cp -R "$TMP/fxcut" "$FXG" git -C "$FXG" init -q && git -C "$FXG" add -A && git -C "$FXG" -c user.name=t -c user.email=t@t commit -qm init GSHIM="$TMP/gitshim"; mkdir -p "$GSHIM"; REALGIT="$( command -v git )" @@ -612,46 +645,37 @@ EOF chmod +x "$GSHIM/git"; : > "$GSHIM/calls.log" out="$( cd "$TMP" && PATH="$GSHIM:$PATH" "$BIN" fxcutgit --for="gadget assembler" --no-cache --token-budget=500 2>/dev/null )" st="$( grep -c 'status --porcelain' "$GSHIM/calls.log" | tr -d ' ' )" -if ! printf '%s' "$out" | grep -q 'over_ceiling="1"'; then - no "(10c) premise: --token-budget=500 on fxcutgit is not over its ceiling either way (no over_ceiling=\"1\"): $( printf '%s' "$out" | grep -o ']*>' | head -c 300 )" +if ! printf '%s' "$out" | grep -q ']*next_offset='; then + no "(10c) premise: --token-budget=500 on fxcutgit cuts no row (no ): $( printf '%s' "$out" | grep -o ']*>' | head -c 300 )" elif [ "$st" = 1 ] && printf '%s' "$out" | grep -q ' at="'; then - ok "(10c) a three-render --token-budget run reads the git stamp once (1 status --porcelain child), and at= rides the answer" + ok "(10c) a budgeted, cut --token-budget run reads the git stamp once (1 status --porcelain child), and at= rides the answer" else no "(10c) status --porcelain children: $st (want 1; one per render = 3 before the fix); at= present: $( printf '%s' "$out" | grep -c ' at="' )" 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). -# train 26c re-pin (2026-10-10, lego-charge): the lane charges / to the budget at their served size, so -# the cut alone leaves 16 rows at 2160 on this fixture (the lane's own pre-continuation binary 1cfbfdcc: 16 rows, est 1970; -# the 26c parent and the seven phase-A lanes: 17, est 1985); the merged binary serves those 16 + the unpaid next= (est 2002 -# <= 2160, no over_ceiling=). Re-pinned from the pre-continuation binary, as this comment asks; the assertions are unchanged. -# --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 +# ── (11) A SOFT TARGET NEVER SERVES FEWER ROWS THAN THE HARD CEILING DID (forbudget-soft; rule 4: replaces C3-completed) ───────── +# C3 completed (2026-10-08) ruled "pay for next= only when paying is what makes the answer fit" under a HARD budget, pinning the +# rows the cut alone left: CLI --for @2160 16 rows, --json @2000 16, MCP @4340 40. A soft target charges the rows only (never the +# header), so at the same budgets it serves AT LEAST those rows, the cut (if any) carries its continuation, and nothing is +# labelled over_ceiling. The est_tokens <= budget half is gone with the byte promise (the owner's decision); the row bound +# that replaces it is forsoftbudgetcheck's ST. +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["total"], bool(h), d["over"])' 2>/dev/null; } +c11check(){ # LABEL BUDGET WANT ROWS TOTAL HANDLE OVER 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=" + if [ "$#" -eq 4 ] && [ "$1" -ge "$want" ] && { [ "$1" = "$2" ] || [ "$3" = True ]; } && [ "$4" = False ]; then + ok "(11) $label @$bt: $1 rows (>= the hard ceiling's $want), the cut continued, 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?)" + no "(11) $label @$bt: shown/total/handle/over = ${*:-} — want >= $want rows, a continued cut, no over_ceiling=" fi } c11check "CLI --for" 2160 16 $( 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" + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["sigs"]), d.get("sigs_total", len(d["sigs"])), "sigs_next" in d, bool(d.get("over_ceiling")))' 2>/dev/null ) +if [ "$#" -eq 4 ] && [ "$1" -ge 16 ] && { [ "$1" = "$2" ] || [ "$3" = True ]; } && [ "$4" = False ]; then + ok "(11) --for --json @2000: $1 rows (>= 16), the cut continued (\"sigs_next\"), no over_ceiling" else - no "(11) --for --json @2000: rows/sigs_next/over/est = ${*:-} — want >= 16 rows, \"sigs_next\", est <= 2000, no over_ceiling" + no "(11) --for --json @2000: rows/total/sigs_next/over = ${*:-} — want >= 16 rows, a continued cut, no over_ceiling" fi [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" diff --git a/test/forrootlegendcheck.sh b/test/forrootlegendcheck.sh index b4b6fe245..79fdaa147 100755 --- a/test/forrootlegendcheck.sh +++ b/test/forrootlegendcheck.sh @@ -83,18 +83,22 @@ done # 942, 1000 six rows at 940 / 989 — 60 tokens of headroom in the full legend, the dialect this arm measures (the 2026-09-12 # re-anchor's 36-token posture or better); 1025+ seven rows at 987. The assertions are unchanged. OUT2="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=1000 --no-cache --legend=full 2>/dev/null )" -EST2="$( printf '%s' "$OUT2" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -n "$EST2" ] && [ "$EST2" -le 1000 ]; then - ok "arm2: --token-budget=1000 fits the ceiling in the full legend (est_tokens=$EST2)" +# forbudget-soft (owner decisions 2026-10-14; rule 4): --token-budget on --for is a SOFT target charged against the +# ROWS only — the header (this root= clause included) is never charged and never labelled "over". The arm's subject, "the +# clause's cost", is therefore asserted where it now lives: the clause costs NO row (the full legend, which carries it at +# length, serves exactly the rows the compact default does at the same target), and neither answer carries over_ceiling=. +OUT2D="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=1000 --no-cache 2>/dev/null )" +rows2(){ printf '%s' "$1" | python3 -c 'import re,sys; m=re.search(r"]*>(.*?)",sys.stdin.read(),re.S); print("|".join(re.findall(r"]*\bn=\"([^\"]*)\"",m.group(1))) if m else "")'; } +R2F="$( rows2 "$OUT2" )"; R2D="$( rows2 "$OUT2D" )" +if [ -n "$R2F" ] && [ "$R2F" = "$R2D" ]; then + ok "arm2: --token-budget=1000: the full legend's clause costs no row (full rows == compact rows: $( printf '%s' "$R2F" | tr '|' '\n' | grep -c . ))" else - no "arm2: --token-budget=1000 est_tokens=${EST2:-unreadable} exceeds the ceiling — the legend clause is too expensive" + no "arm2: --token-budget=1000: full rows '${R2F:0:80}' != compact rows '${R2D:0:80}' — a header clause is being charged to the rows" fi -OUT2D="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=1000 --no-cache 2>/dev/null )" -EST2D="$( printf '%s' "$OUT2D" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -n "$EST2D" ] && { [ "$EST2D" -le 1000 ] || printf '%s' "$OUT2D" | head -c 600 | grep -q ' over_ceiling="1"'; }; then - ok "arm2: the default answer at --token-budget=1000 fits or says it does not (est_tokens=$EST2D)" +if [ -n "$OUT2D" ] && ! printf '%s%s' "$OUT2" "$OUT2D" | grep -q ' over_ceiling="1"'; then + ok "arm2: --token-budget=1000: neither dialect labels a soft target over_ceiling" else - no "arm2: the default answer at --token-budget=1000 is over (est_tokens=${EST2D:-unreadable}) with no over_ceiling label" + no "arm2: --token-budget=1000: over_ceiling=\"1\" on a soft target (or the call failed)" fi if printf '%s' "$OUT2" | grep -qF "$CLAUSE_SNIPPET"; then ok "arm2: the clause SURVIVES at --token-budget=1000 (not dropped by the ceiling ladder)" diff --git a/test/forsectioncollapsecheck.sh b/test/forsectioncollapsecheck.sh index 27eb844cd..e55500e45 100644 --- a/test/forsectioncollapsecheck.sh +++ b/test/forsectioncollapsecheck.sh @@ -407,8 +407,8 @@ lc_est(){ grep -o 'est_tokens="[0-9]*"' "$1" | head -1 | grep -o '[0-9][0-9]*'; lc_sigs(){ grep -o '' "$1"; } "$BIN" test/legochargefix --no-cache --for="$LCQ" >"$TMP/lc_def.xml" 2>/dev/null; rc_def=$? "$BIN" test/legochargefix --no-cache --for="$LCQ" --sections=lego,compose >"$TMP/lc_rest.xml" 2>/dev/null; rc_rest=$? -"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=3000 >"$TMP/lc_tb.xml" 2>/dev/null; rc_tb=$? -"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=3000 --sections=lego,compose >"$TMP/lc_tbrest.xml" 2>/dev/null; rc_tbrest=$? +"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=6000 >"$TMP/lc_tb.xml" 2>/dev/null; rc_tb=$? +"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=6000 --sections=lego,compose >"$TMP/lc_tbrest.xml" 2>/dev/null; rc_tbrest=$? LC_DEF="$( lc_shown "$TMP/lc_def.xml" )"; LC_REST="$( lc_shown "$TMP/lc_rest.xml" )" LC_TB="$( lc_shown "$TMP/lc_tb.xml" )"; LC_TBREST="$( lc_shown "$TMP/lc_tbrest.xml" )"; LC_TBEST="$( lc_est "$TMP/lc_tbrest.xml" )" if [ "$rc_def$rc_rest$rc_tb$rc_tbrest" != "0000" ] || ! [[ "$LC_DEF" =~ ^[0-9]+$ && "$LC_REST" =~ ^[0-9]+$ && "$LC_TB" =~ ^[0-9]+$ && "$LC_TBREST" =~ ^[0-9]+$ && "$LC_TBEST" =~ ^[0-9]+$ ]]; then @@ -427,23 +427,38 @@ else [[ "$LC_STUBTOTAL" =~ ^[0-9]+$ ]] && [ "$LC_IFACES" -eq "$(( LC_STUBTOTAL < 12 ? LC_STUBTOTAL : 12 ))" ] \ && ok "(13c) the stub's total=$LC_STUBTOTAL matches the restored ($LC_IFACES rows, display cap 12)" \ || no "(13c) the stub's total=\"$LC_STUBTOTAL\" vs $LC_IFACES restored rows (display cap 12)" - # (13d) NEAR-MISS: under an explicit ceiling, a section --sections= opts into is charged in full — the hard bound holds - # (its is then narrowed to the few rendered rows' files — here none of stages.h — so the bound is read off ) - [ "$LC_TBEST" -le 3000 ] && [ "$LC_TBREST" -lt "$LC_TB" ] \ - && ok "(13d) --token-budget=3000 --sections=lego,compose: est_tokens=$LC_TBEST <= 3000, $LC_TBREST < $LC_TB (the full was charged)" \ - || no "(13d) --token-budget=3000 --sections=lego,compose: est_tokens=$LC_TBEST, $LC_TBREST vs $LC_TB stubbed — the opted-in section was not charged in full under an explicit ceiling" + # (13d) forbudget-soft (owner decisions 2026-10-14; rule 4): an explicit --token-budget is a SOFT target. Round C (RESERVE + # SECTIONS FIRST, owner decision 2026-10-14): the trailing sections are served first and the rows past the target cut fill + # what they leave of T — so a section --sections= opts into rides WHOLE and its bytes come out of the rows past the cut, + # never out of the section. By-design break of round B's "the same rows with and without the opt-in" (sections were funded + # from what the rows left): what holds now is the full , no over_ceiling, and rows that do not GROW with the opt-in. + # (Re-anchored from 3000 to 6000 tokens: at 3000 the rows at the cut name no implementing file, so is empty either way.) + [ "$LC_TBREST" -le "$LC_TB" ] && ! grep -q 'over_ceiling="1"' "$TMP/lc_tbrest.xml" && grep -q ' rides whole, reserved before the rows past the cut ($LC_TBREST <= $LC_TB rows), no over_ceiling (est_tokens=$LC_TBEST)" \ + || no "(13d) --token-budget=6000 --sections=lego,compose: $LC_TBREST vs $LC_TB without it (or over_ceiling, or no full ) — an opted-in section is reserved first under a soft target" fi if command -v python3 >/dev/null 2>&1; then + # lc_mcp ARGS: " " — rows from (a cut bundle) or the rows inside an uncut , + # lego 1 when the full rides, over 1 on over_ceiling="1" lc_mcp(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"test/legochargefix","task":"%s"%s}}}\n' "$LCQ" "$1" \ | "$BIN" --mcp 2>/dev/null | python3 -c " import json,sys,re t = json.loads(sys.stdin.readline())['result']['content'][0]['text'] m = re.search(r' $LCM_DEF rows by default; under budget_tokens + sections $LCM_TBREST < $LCM_TB" \ - || no "(13e) MCP for: default=$LCM_DEF budget=$LCM_TB budget+sections=$LCM_TBREST (want >= 24, and the last below the middle)" +b = re.search(r'(.*?)', t, re.S) +rows = m.group(1) if m else ( str( len( re.findall( r' at all, so "the same rows" held whether or not they were charged. What holds at + # 6000: the full rides, no over_ceiling, and the opted-in sections never ADD a row. (MCP `for` under + # budget_tokens serves the signatures bundle, uncut here, so the row counts are equal; the inequality is the contract.) + [[ "$LCM_DEF" =~ ^[0-9]+$ && "$LCM_TB" =~ ^[0-9]+$ && "$LCM_TBREST" =~ ^[0-9]+$ ]] && [ "$LCM_DEF" -ge 24 ] && [ "$LCM_TBREST" -le "$LCM_TB" ] \ + && [ "${LCM_LEGO:-0}" = 1 ] && [ "${LCM_OVER:-1}" = 0 ] \ + && ok "(13e) MCP for: $LCM_DEF rows by default; under budget_tokens=6000 the opted-in rides whole, no over_ceiling, and adds no row ($LCM_TBREST <= $LCM_TB)" \ + || no "(13e) MCP for: default=$LCM_DEF budget=$LCM_TB budget+sections=$LCM_TBREST lego=${LCM_LEGO:-?} over=${LCM_OVER:-?} (want >= 24, the last <= the middle, a full , no over_ceiling)" else printf ' SKIP %s\n' "(13e) python3 absent: the MCP twin of the lego charge is not checked" fi diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 7fe51c441..47500f7a5 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -55,13 +55,18 @@ # docs describe the context lifecycle?" keeps a docs row in its top five. # (T) a TIGHT explicit ceiling (--token-budget / MCP budget_tokens below the default signature share) gets neither the # reorder nor its uncharged note: the answer is byte-equal to the RIPWIRE_NO_DOCS_AFTER_CODE=1 twin (which is the -# pre-reorder answer), XML, JSON and MCP, at 700 and 1200 on the TS fixture; at 1200 the XML/JSON est_tokens stay -# within the budget with no over_ceiling the twin lacks (RED at 9d0ad7aa: est 1277 > 1200, over_ceiling="1"). +# pre-reorder answer), XML, JSON and MCP, at 700 and 1200 on the TS fixture. At 1200 (forbudget-soft, rule 4: an +# explicit --token-budget is a SOFT target over the rows, est_tokens may exceed N) the old "est_tokens <= 1200, +# no over_ceiling the twin lacks" half is replaced by NOLBL (no over_ceiling= on either answer) + ST (the rows' charge +# fits T = N x 2.36 x 0.9 bytes or is the rank 1..4 floor; a cut is maximal against the uncut plain-order answer and +# carries capped + next=), XML and JSON (RED on base d4325a8c: the hard ceiling cut 10 of 16 / 13 of 16 rows with +# room left under T). # 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 1660 +# ceiling: the same twin equality, est_tokens <= N, on the TS fixture at 1700 and the Python+docs fixture (pydocs) at 1760 +# (round C re-anchor: 1600 / 1660 before the tail carried the file page) # (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=. @@ -88,6 +93,7 @@ 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) +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary CORPUS="$ROOT/test/forcompletefix" @@ -418,11 +424,69 @@ if mode == "tight": bad.append( "differs from the NO_DOCS_AFTER_CODE=1 twin (the pre-reorder answer)" ) if moved or note: bad.append( "carries docs_after_code= or its reading" ) + # forbudget-soft (rule 4): an explicit --token-budget (a REF is passed) is a SOFT target over the rows (T = N x kMinBytesPerToken x + # kBudgetHeadroom bytes), so est_tokens may exceed N and nothing is labelled over_ceiling. The old "est_tokens <= N, no + # over_ceiling the twin lacks" half is replaced by NOLBL (no over_ceiling= on either answer) + ST (the rows fit T, or are + # the rank 1..4 floor; a cut is maximal against the uncut plain-order REF and carries next=). RED on base (d4325a8c): the + # hard ceiling cut the XML rows at 10 of 16 / the JSON rows at 13 with room left under T. m = re.search( r']*\best_tokens="([0-9]+)"', A ) or re.search( r'"est_tokens":([0-9]+)', A ) - if budget and not on.endswith( ".mcp.json" ): + if budget and not on.endswith( ".mcp.json" ) and len( sys.argv ) <= 7: + # a HARD body ceiling (--max-tokens; no REF passed): est_tokens stays <= N, no over_ceiling the twin lacks — unchanged if not m: bad.append( "no est_tokens to compare with the budget (premise)" ) elif int( m.group( 1 ) ) > budget: bad.append( "est_tokens %s > budget %d" % ( m.group( 1 ), budget ) ) if "over_ceiling" in A and "over_ceiling" not in B: bad.append( "over_ceiling the twin lacks" ) + elif budget and not on.endswith( ".mcp.json" ): + tb = float( sys.argv[ 8 ] ) if len( sys.argv ) > 8 and re.fullmatch( r"[0-9.]+", sys.argv[ 8 ] ) else 0.0 + if tb <= 0: bad.append( "no target bytes-per-token constant (premise: src/serialize.h kMinBytesPerToken x kBudgetHeadroom)" ) + if re.search( r'over_ceiling', A ) or re.search( r'over_ceiling', B ): + bad.append( "NOLBL: over_ceiling on a soft --token-budget answer" ) + def charged( doc ): + """(row charges in rank order, total charged bytes, cut?, next?) — XML: the bytes inside plus the zero + cx=/ccx=/in= the lean spelling elides (the ladder's entryCost); JSON: the compact bytes of each "sigs" entry.""" + if doc.lstrip().startswith( "{" ): + try: j = json.loads( doc ) + except ValueError: return None + rows = [ ( int( e.get( "r", 10**9 ) ), len( json.dumps( e, separators=( ",", ":" ), ensure_ascii=False ).encode() ) + 1 ) + for e in j.get( "sigs", [] ) ] + rows.sort() + cut = bool( j.get( "capped" ) ) or j.get( "sigs_shown", len( rows ) ) != j.get( "sigs_total", len( rows ) ) + return [ c for _, c in rows ], sum( c for _, c in rows ), cut, bool( j.get( "sigs_next" ) ) + m2 = re.search( r']*)>(.*?)', doc, re.S ) + if not m2: return None + rows = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', m2.group( 2 ), re.S ): + a = d.group( 1 ) or d.group( 2 ); r = re.search( r'\br="(\d+)"', a ); pr = len( d.group( 0 ).encode() ) + c = pr + sum( n for k, n in ( ( "cx", 7 ), ( "ccx", 8 ), ( "in", 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) + elided += c - pr; rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + rows.sort() + return [ c for _, c in rows ], len( m2.group( 2 ).encode() ) + elided, 'capped="1"' in m2.group( 1 ), " next=" in m2.group( 1 ) + a, ref = charged( A ), charged( body( sys.argv[ 7 ] ) ) if len( sys.argv ) > 7 else None + T = int( budget * tb ) + # forbudget-soft round C (owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served + # first and the rows past the target cut fill what they leave of T — maximality is judged against T less their bytes + # (U: XML, everything after but , plus the rows; JSON, the "tail" object) + if A.lstrip().startswith( "{" ): + try: U = len( json.dumps( json.loads( A ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: U = 0 + else: + U = ( len( A.split( "", 1 )[ 1 ].encode() ) - len( "" ) if "" in A else 0 ) + U += sum( len( h.encode() ) for h in re.findall( r']*/>', A.split( " 7: U += max( 0, tail_size( body( sys.argv[ 7 ] ) ) - tail_size( A ) ) + if a is None or ref is None or not ref[ 0 ]: bad.append( "ST: no rows to charge (premise: answer and uncut reference)" ) + elif tb > 0: + k = len( a[ 0 ] ); fits = a[ 1 ] <= T or k <= 4 + if not fits: bad.append( "ST: rows charged %d B > target %d B with %d rows (floor 4)" % ( a[ 1 ], T, k ) ) + if k < len( ref[ 0 ] ): + if a[ 1 ] + ref[ 0 ][ k ] <= T - U - 160: bad.append( "ST: cut at %d of %d rows with %d B charged, the next row (%d B) fits T=%d less the sections U=%d" % ( k, len( ref[ 0 ] ), a[ 1 ], ref[ 0 ][ k ], T, U ) ) + if not ( a[ 2 ] and a[ 3 ] ): bad.append( "NX: rows cut (%d of %d) without capped + next=" % ( k, len( ref[ 0 ] ) ) ) + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor + # stop where T less the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import upper_bound + up, udet = upper_bound( A, body( sys.argv[ 7 ] ), T, k, len( ref[ 0 ] ), U ) + if not up: bad.append( "UP: " + udet ) + print( " .... %s: ST rows=%d/%d charged=%d T=%d" % ( label, k, len( ref[ 0 ] ), a[ 1 ], T ) ) else: if not moved: bad.append( "no docs_after_code= (the reorder did not apply)" ) if not note and not isjson: bad.append( "no reading for docs_after_code=" ) # the JSON twin carries the count only @@ -438,13 +502,17 @@ mcpfor(){ local out="$1" twin="$2" task="$3" extra="${4:-}"; mkdir -p "$TMP/mcph 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":"'"$task"'"'"$extra"'}}}' \ | ( cd "$CORPUS/ts" && if [ "$twin" = 1 ]; then TMPDIR="$TMP/mcphome" RIPWIRE_NO_DOCS_AFTER_CODE=1 "$BIN" --mcp; else TMPDIR="$TMP/mcphome" env -u RIPWIRE_NO_DOCS_AFTER_CODE "$BIN" --mcp; fi >"$out" 2>/dev/null ); printf '%s' "$?" >"$out.rc"; } +# forbudget-soft (rule 4): the soft target's bytes per token, read from source (a missing constant FAILS the ST half, never skips) +TBPT="$( python3 -c "import re,sys; s=open(sys.argv[1]).read(); g=lambda k:float(re.search(r'inline constexpr double '+k+r' *= *([0-9.]+);',s).group(1)); print(g('kMinBytesPerToken')*g('kBudgetHeadroom'))" "$ROOT/src/serialize.h" 2>/dev/null )" for b in 700 1200; do for spec in "xml|" "json|--json"; do tag="${spec%%|*}"; extra="${spec#*|}" if [ -n "$extra" ]; then ON="$( onr ts "t$b$tag" "$Q1" "--token-budget=$b" "$extra" )"; OFF="$( offr ts "t$b$tag" "$Q1" "--token-budget=$b" "$extra" )" else ON="$( onr ts "t$b$tag" "$Q1" "--token-budget=$b" )"; OFF="$( offr ts "t$b$tag" "$Q1" "--token-budget=$b" )"; fi if ran_ok "$ON" "T/$b/$tag" && ran_ok "$OFF" "T/$b/$tag (twin)"; then - tcheck "T/$tag --token-budget=$b: byte-equal to the plain-order twin, no docs_after_code=$([ "$b" = 1200 ] && echo ", est_tokens <= 1200")" tight "$ON" "$OFF" "$([ "$b" = 1200 ] && echo 1200 || echo 0)" + REF="$( offr ts "r$tag" "$Q1" $extra )" # the uncut plain-order answer (no budget): the rank order a soft cut is a prefix of + ran_ok "$REF" "T/$b/$tag (uncut reference)" || continue + tcheck "T/$tag --token-budget=$b: byte-equal to the plain-order twin, no docs_after_code=$([ "$b" = 1200 ] && echo ", NOLBL + ST (soft target)")" tight "$ON" "$OFF" "$([ "$b" = 1200 ] && echo 1200 || echo 0)" "$REF" "$TBPT" fi done mcpfor "$TMP/t$b.on.mcp.json" 0 "$Q1" ",\"budget_tokens\":$b"; mcpfor "$TMP/t$b.off.mcp.json" 1 "$Q1" ",\"budget_tokens\":$b" @@ -457,7 +525,9 @@ done # 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 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|1660"; do +# forbudget-soft round C (FOLD C; rule 4): the default answer's now carries page 0 of the file page (+94 / +87 tokens on +# these two answers), so the windows move with it: 1600 -> 1700, 1660 -> 1760 (est 1628 / 1713 on the round C binary). +for spec in "ts|$Q1|1700" "pydocs|$QP|1760"; 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 diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh new file mode 100755 index 000000000..82bf99c28 --- /dev/null +++ b/test/forsoftbudgetcheck.sh @@ -0,0 +1,767 @@ +#!/usr/bin/env bash +# forsoftbudgetcheck.sh — the --for lens stops when its answer is complete; an explicit --token-budget on the +# answer-shaping lenses is a SOFT TARGET (owner decisions 2026-10-14). Written RED-FIRST, before the code; round 2 +# (early gates review R1-R9) added the stop-at-target, continuation, legend-set, guard-bound and twin arms. +# +# WHAT CHANGES, and the contract every arm below pins: +# +# DEFAULT --for (no budget). The ranked head the lens selected (: the relevance floor intersected with +# kForLensDefaultTopN or --pack-top-n) IS the answer. kForPayloadBudgetBytes stops being a ceiling and becomes a +# stair-step TARGET: the trim ladder's shrink steps (A..E: shorter signatures, doc excerpts) still run against it, +# but its row-drop step (F) runs only past a RUNAWAY GUARD (kForRunawayGuardBytes, far above a typical answer). So: +# (D1) a default answer serves every row of its head: shown == total, and total == kForLensDefaultTopN here; +# (D2) those rows, in rank order, are the rows an uncapped call serves; +# (D3) rows served past the target are disclosed — and a legend clause defining it — +# and the legend calls budget_bytes= a TARGET on that answer; no over_ceiling= (a target cannot be "over"); +# (D3H) the compact route's serves its whole row set (decision D-3): shown + noedge == total, not capped; +# (D4) hitting the runaway guard is the only default-regime row cut: over_ceiling="1" on the root, the +# cut carries next=, the legend names the guard (its value read from that legend), and the block is +# within the guard yet close to it (a lower bound: the guard is not the old 7.5 KB step in disguise); +# (D4b) over a --pack-top-n sweep: over_ceiling="1" exactly when the uncut head passes the guard; +# (D5) the MCP `for` twin serves its whole head too: the CLI default's rows, as a set; +# (D6) negative: a complete answer under the target carries no past-target / guard disclosure; +# (NX4) every cut element of a default answer (capped="1", or shown < total beyond noedge=) carries next= (D-4). +# +# EXPLICIT --token-budget=N / MCP budget_tokens on --for, --pack-task, MCP explore. T = N x kMinBytesPerToken x +# kBudgetHeadroom (the existing conversion) is charged against the ROWS only, never the header or a +# disclosure. Rows are added in rank order while they fit T; the rank 1..4 floor always ships; there is no byte +# promise, so nothing is labelled over a ceiling and nothing in the header is dropped to fit: +# (ST) stop at target, in the default and --signatures-only modes: the rows' bytes fit T (or are only the +# floor), and when rows were left out the next row of the complete answer would cross T less the trailing +# sections' bytes (round C: sections are reserved first, rows fill the rest; fill is maximal); +# (E1) no over_ceiling= at a tight target; the floor rows ship; the cut carries next=; +# (E2) no legend clause is dropped: at every N the legend's clauses include the wide answer's (present-only +# clauses aside), the root keeps task= and route=, the full dialect keeps its task echo; and a 900-char +# task at N=300 keeps task= verbatim with no task_echo drop; +# (E3) the served rows are a rank PREFIX that grows with N, and a prefix of the default answer's rows; +# (E4) the compact and full dialects serve the same rows at every target (rows are charged, not headers); +# (E5) stop when complete: a target wider than the answer serves exactly the default answer's rows; +# (NX1) the cut's next= RUNS and serves the whole head, the cut's rows its prefix (NX2: the guard cut's; NX3: +# the MCP next_budget_tokens= re-call); +# (E6) MCP `for` at a tight budget_tokens: no over_ceiling=, next_budget_tokens= names the continuation; +# (PT1) --pack-top-n sets the head, the target fills it: a wide target serves exactly --pack-top-n rows; +# (P1) --pack-task at a tight target: no over_ceiling=, no "(ceiling)" drop, task= verbatim; +# (P2) --pack-task --token-budget=6000 serves the default --pack-task's sections (6000 is the default value); +# (P3) MCP explore at a tight budget_tokens: no over_ceiling=. +# +# UNCHANGED (twins, must hold on base and head): the exit-3 CI-gate personality of --token-budget on the default +# map (G1), --query (G2) and --recall (G3): over budget -> exit 3, the artifact withheld. +# +# Usage: bash test/forsoftbudgetcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forsoftbudgetcheck.sh +# Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" # gitenvhermeticcheck (D): no ambient GIT_* reaches the RP10 fixture repos +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) +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 "python3 required"; exit 2; } +cd "$ROOT" +echo "forsoftbudgetcheck: BIN=$BIN" + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +CORPUS="src" # short relative root (CHECKLIST 22): header bytes shift with the root path +Q="rank symbols by pagerank" # conceptual: a flat 40-row head whose default answer the 7.5 KB ladder cut +QH="parse command line flags" # conceptual: the compact route's was cut by its 1,000 B allowance on base +CF="--cache=$TMP/c_for"; CP="--cache=$TMP/c_pack"; CM="--cache=$TMP/c_map"; CD="--cache=$TMP/c_docs" # one cache per verb class + +# constants, read from source (a binary that predates the guard has none: the arms that need it FAIL, never skip) +srcnum(){ sed -n "s/^inline constexpr [a-z:_ ]*$1 *= *\([0-9.]*\);.*/\1/p" src/serialize.h | head -1; } +GUARD_SRC="$( srcnum kForRunawayGuardBytes )" +TOPN="$( srcnum kForLensDefaultTopN )" +MINBPT="$( srcnum kMinBytesPerToken )"; HEADROOM="$( srcnum kBudgetHeadroom )" +tbytes(){ python3 -c "import sys; print(int(float(sys.argv[1]) * float(sys.argv[2]) * float(sys.argv[3])))" "$1" "$MINBPT" "$HEADROOM"; } + +# ── helpers ────────────────────────────────────────────────────────────────────────────────────────────── +cat > "$TMP/rows.py" <<'PY' +# the served rows as "p:l:n", in RANK order (r=), one per line; exit 3 when there is no block +import re, sys +s = sys.stdin.read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( "\n".join( x[ 1 ] for x in rows ) ) +PY +cat > "$TMP/rowbytes.py" <<'PY' +# rowbytes.py FILE: " " then one line per row " p:l:n" in rank order. rows-bytes = the bytes +# inside the element (between its open tag and ): what a soft target is charged against. exit 3: no . +import re, sys +s = open( sys.argv[ 1 ] ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +inner = m.group( 1 ) +rows = [] +for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, len( d.group( 0 ).encode() ), g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( len( inner.encode() ), len( rows ) ) +for r in rows: print( r[ 1 ], r[ 2 ] ) +PY +cat > "$TMP/attr.py" <<'PY' +# attr.py ELEMENT NAME: the value of NAME on the first open tag ("" when absent; exit 3 when no such tag) +import re, sys +el, name = sys.argv[ 1 ], sys.argv[ 2 ] +s = sys.stdin.read() +m = re.search( r'<' + el + r'\b([^>]*)>', s ) +if not m: sys.exit( 3 ) +v = re.search( r'\b' + name + r'="([^"]*)"', m.group( 1 ) ) +print( v.group( 1 ) if v else "" ) +PY +cat > "$TMP/prefix.py" <<'PY' +# prefix.py A B: exit 0 when the lines of file A are a prefix of the lines of file B +import sys +a = open( sys.argv[ 1 ] ).read().split( "\n" ); b = open( sys.argv[ 2 ] ).read().split( "\n" ) +a = [ x for x in a if x ]; b = [ x for x in b if x ] +sys.exit( 0 if a == b[ :len( a ) ] else 1 ) +PY +cat > "$TMP/legend.py" <<'PY' +# legend.py NARROW WIDE: the legend clauses (header comments before , split at brackets / '; ' / '. ', digits +# normalised) of WIDE that NARROW lacks, one per line — present-only clauses (each rides only with its own attribute) +# excluded. Empty output = no clause was dropped. exit 3 when either file has no header comment. +import re, sys +# Present-only = rides only with its own attribute or section on THIS answer (fewer rows can legitimately not carry it): the +# cut/target readings, e= (rows carry it only where endLinesFitCeiling says), sc=, the lego/compose stub (only when a +# section collapsed), the shrunk-not-dropped reading. +PRESENT_ONLY = re.compile( r'dropped_positive|docs_dropped|capped=[#1] with shown|sigs next|next_offset|next_budget|past_target|' + r'budget_bytes|BYTE target|over_ceiling|runaway|docs_after_code|relevance floor|this budget left out|' + r'e= on a d row|d e= its last line|extent is not known|line of the definition|the name.s line|' + r'sc=scope|cut, recoverable|bodies shown|hops shown=#|collapse to a counted stub|' + # the row readings ride only with a row that carries them: a spent target's hops cut marker + # leaves none (via="name" since forbudget-soft, attrib-multidef's prov="split" since train 28) + r'on a row|c prov=split|another arm|NOT mean the edge is false|NOT a claim the edge is false', re.I ) +def clauses( path ): + s = open( path ).read() + head = s.split( '', head, re.S ) + if not com: sys.exit( 3 ) + out = set() + for c in com: + # " est_tokens= prices…" is spliced after the last clause with a bare space (pre-existing): a clause boundary too. + # So are the two present-only row readings (FE-B's via="name", attrib-multidef's prov="split"), which open with a + # bare space after whatever clause precedes them: without the boundary the clause before one reads as missing + # wherever the one between them (e= on a d row) is absent (train 28: the split reading rides this corpus) + for seg in re.split( r'\s*(?:\[|\]|;\s|\.\s|\s—\s|\s(?=est_tokens= prices)|\s(?=via="name" on a row)|\s(?=prov="split" on a row))\s*', c ): + seg = re.sub( r'\d+', '#', re.sub( r'\s+', ' ', seg ) ).strip( ' .:;' ) + if len( seg ) >= 12: out.add( seg ) + return out +n, w = clauses( sys.argv[ 1 ] ), clauses( sys.argv[ 2 ] ) +for c in sorted( w - n ): + if not PRESENT_ONLY.search( c ): print( c ) +PY +cat > "$TMP/cuts.py" <<'PY' +# cuts.py: every element of the answer that discloses a CUT — capped="1", or shown < total beyond what noedge= explains — +# and carries no continuation (next= / next_budget_tokens=), one per line. Empty = every cut is recoverable. +import re, sys +s = sys.stdin.read() +for m in re.finditer( r'<([a-z]+)\b([^>]*)>', s ): + el, a = m.group( 1 ), m.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "" ] )[ 1 ] + num = lambda k: int( g( k ) ) if g( k ).isdigit() else None + shown, total, noedge = num( "shown" ), num( "total" ), num( "noedge" ) or 0 + cut = g( "capped" ) == "1" or ( shown is not None and total is not None and shown + noedge < total ) + if el in ( "ctx", "files" ) or not cut: continue + if g( "next" ) or g( "next_budget_tokens" ): continue + print( el, a.strip()[ :160 ] ) +PY +cat > "$TMP/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 +cli_for(){ "$BIN" "$CORPUS" --for="$Q" "$@" "$CF" 2>/dev/null; } +mcp_call(){ # mcp_call TOOL TASK EXTRA-JSON + printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s","task":"%s"%s}}}\n' \ + "$1" "$CORPUS" "$2" "${3:-}" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +attr(){ python3 "$TMP/attr.py" "$1" "$2" < "$3"; } +rows(){ python3 "$TMP/rows.py" < "$1" > "$2"; } +isnum(){ case "${1:-}" in ''|*[!0-9]*) return 1;; esac; return 0; } +lines(){ local c; c="$( grep -c . "$1" 2>/dev/null )"; echo "${c:-0}"; } +# run a pasteable next= (XML-escaped argv fragment) against the same corpus; stdout to $2 +run_next(){ + python3 - "$1" > "$TMP/next.argv" <<'PY' || return 1 +import sys, shlex, html +print( "\0".join( shlex.split( html.unescape( sys.argv[ 1 ] ) ) ), end="" ) +PY + local -a a=() + while IFS= read -r -d '' x; do a+=( "$x" ); done < <( cat "$TMP/next.argv"; printf '\0' ) + [ "${#a[@]}" -gt 0 ] || return 1 + "$BIN" "$CORPUS" "${a[@]}" "$CF" > "$2" 2>/dev/null +} + +if ! isnum "$TOPN" || [ -z "$MINBPT" ] || [ -z "$HEADROOM" ]; then + no "premise: kForLensDefaultTopN / kMinBytesPerToken / kBudgetHeadroom not found in src/serialize.h" + echo "FAILURES ABOVE"; exit 1 +fi + +# ── the default answer (premise for D1..D3, ST, E3, E5) ────────────────────────────────────────────────── +cli_for > "$TMP/def.xml"; rc=$? +if [ "$rc" -ne 0 ] || ! rows "$TMP/def.xml" "$TMP/def.rows"; then + no "premise: default --for on $CORPUS did not run (rc=$rc) or served no — every arm below would measure nothing" + echo "FAILURES ABOVE"; exit 1 +fi +DS="$( attr sigs shown "$TMP/def.xml" )"; DT="$( attr sigs total "$TMP/def.xml" )" +DN="$( lines "$TMP/def.rows" )" +# R5: the head size is pinned to the lens's own cap — a change that shrinks the head cannot pass D1 by shrinking total= +if isnum "$DT" && [ "$DT" -eq "$TOPN" ]; then + ok "premise: the default head is kForLensDefaultTopN wide: total=$DT" +else + no "premise: the default head total='$DT' != kForLensDefaultTopN=$TOPN — the head was narrowed, so 'complete' is relative to a smaller answer" +fi + +# (D1) the whole head ships +if isnum "$DT" && [ "$DN" -eq "$DT" ] && { [ -z "$DS" ] || [ "$DS" = "$DT" ]; }; then + ok "(D1) default answer serves its whole head: $DN rows of total=$DT" +else + no "(D1) default answer served $DN rows (shown='$DS') of total='$DT' — rows of the ranked head were dropped below the runaway guard" +fi + +# (D2) the default rows ARE the uncapped rows, in rank order +cli_for --signatures-only --token-budget=1000000 > "$TMP/unc.xml" +if rows "$TMP/unc.xml" "$TMP/unc.rows" && cmp -s "$TMP/def.rows" "$TMP/unc.rows"; then + ok "(D2) default rows == uncapped rows ($( lines "$TMP/unc.rows" ) rows, rank order)" +else + no "(D2) default rows ($DN) differ from the uncapped call's rows ($( lines "$TMP/unc.rows" ))" +fi + +# (D3) past-target disclosure on that answer +PT="$( attr sigs past_target "$TMP/def.xml" )"; OC="$( attr ctx over_ceiling "$TMP/def.xml" )" +sed 's/ "$TMP/def.head" +if isnum "$PT" && [ "$PT" -gt 0 ] && [ "$PT" -le $(( DN - 4 )) ]; then + ok "(D3) names the rows served past the default target (<= shown-4: the floor is never past it)" +else + no "(D3) carries no past_target=K with 0 < K <= shown-4 (got '$PT', shown=$DN) although the answer passes the 7.5 KB target" +fi +if grep -q 'past_target=' "$TMP/def.head"; then ok "(D3) the header legend defines past_target=" +else no "(D3) no legend clause defines past_target= ahead of "; fi +if grep -q 'default BYTE ceiling this ranked payload' "$TMP/def.head"; then + no "(D3) the legend still calls budget_bytes= a ceiling on an answer that passed it by design" +elif grep -q 'budget_bytes= is the default BYTE target' "$TMP/def.head"; then + ok "(D3) the legend calls budget_bytes= a TARGET on a past-target answer" +else + no "(D3) the legend neither calls budget_bytes= a target nor a ceiling — its clause is missing on an answer that carries budget_bytes=" +fi +if [ -z "$OC" ]; then ok "(D3) no over_ceiling= on a default answer under the guard" +else no "(D3) over_ceiling='$OC' on a default answer that is under the runaway guard (a target cannot be 'over')"; fi + +# (NX4) every cut element of the default answer carries a continuation (rule 5; D-4: the ) +python3 "$TMP/cuts.py" < "$TMP/def.xml" > "$TMP/def.cuts" +if [ "$( lines "$TMP/def.cuts" )" -eq 0 ]; then ok "(NX4) every cut element of the default answer carries next=" +else no "(NX4) cut elements without a continuation: $( tr '\n' '|' < "$TMP/def.cuts" | cut -c1-300 )"; fi + +# (D3H) D-3: the compact route's serves its whole row set on the default answer +"$BIN" "$CORPUS" --for="$QH" "$CF" > "$TMP/hops.xml" 2>/dev/null; hrc=$? +HS="$( attr hops shown "$TMP/hops.xml" )"; HT="$( attr hops total "$TMP/hops.xml" )"; HE="$( attr hops noedge "$TMP/hops.xml" )"; HC="$( attr hops capped "$TMP/hops.xml" )" +HE="${HE:-0}" +if [ "$hrc" -ne 0 ] || ! isnum "$HS" || ! isnum "$HT"; then + no "(D3H) premise: --for='$QH' did not run (rc=$hrc) or served no (route changed?)" +elif [ $(( HS + HE )) -eq "$HT" ] && [ "$HC" != "1" ]; then + ok "(D3H) complete: shown=$HS + noedge=$HE == total=$HT, not capped" +else + no "(D3H) cut: shown=$HS noedge=$HE total=$HT capped='$HC' — the default answer must serve every hop row" +fi +python3 "$TMP/cuts.py" < "$TMP/hops.xml" > "$TMP/hops.cuts" +if [ "$hrc" -eq 0 ] && [ "$( lines "$TMP/hops.cuts" )" -eq 0 ]; then ok "(NX4) every cut element of the '$QH' answer carries next=" +else no "(NX4) '$QH': cut elements without a continuation: $( tr '\n' '|' < "$TMP/hops.cuts" | cut -c1-300 )"; fi + +# (D4) the runaway guard: a 400-row head runs into it +cli_for --pack-top-n=400 > "$TMP/guard.xml" +GOC="$( attr ctx over_ceiling "$TMP/guard.xml" )"; GNX="$( attr sigs next "$TMP/guard.xml" )"; GCAP="$( attr sigs capped "$TMP/guard.xml" )" +GS="$( attr sigs shown "$TMP/guard.xml" )"; GT="$( attr sigs total "$TMP/guard.xml" )" +if [ "$GOC" = "1" ] && [ "$GCAP" = "1" ] && [ -n "$GNX" ]; then + ok "(D4) --pack-top-n=400: the guard cut is disclosed (over_ceiling=1, )" +else + no "(D4) --pack-top-n=400: expected over_ceiling=1 + ; got over_ceiling='$GOC' capped='$GCAP' next='${GNX:0:40}'" +fi +sed 's/ "$TMP/guard.head" +# S3: the guard's value is read from the legend of the answer that hit it, and must agree with the source's one owner +GUARD="$( sed -n 's/.*runaway guard of \([0-9][0-9]*\) B.*/\1/p' "$TMP/guard.head" | head -1 )" +if isnum "$GUARD" && [ "$GUARD" = "$GUARD_SRC" ]; then ok "(D4) the legend names the runaway guard ($GUARD B, == kForRunawayGuardBytes)" +else no "(D4) the legend names no runaway guard value ('$GUARD') or it differs from src/serialize.h kForRunawayGuardBytes ('$GUARD_SRC')"; fi +SIGB="$( python3 -c 'import re,sys; m=re.search(r"", sys.stdin.read(), re.S); print(len(m.group(0).encode()) if m else "")' < "$TMP/guard.xml" )" +if ! isnum "$GUARD"; then + no "(D4) no guard value to bound the block against" +elif ! isnum "$SIGB"; then + no "(D4) no block on the guard answer" +elif [ "$SIGB" -le "$GUARD" ] && [ "$SIGB" -gt $(( GUARD - 6000 )) ]; then + ok "(D4) block $SIGB B within the guard $GUARD B and close to it (> guard - 6000: header + one row)" +else + no "(D4) block $SIGB B is not in (guard-6000, guard] = ($(( GUARD - 6000 )), $GUARD] — the cut is not at the guard" +fi +# (NX2) the guard cut's next= runs and serves the whole 400-candidate head, the cut's rows its prefix +rows "$TMP/guard.xml" "$TMP/guard.rows" +if [ -n "$GNX" ] && run_next "$GNX" "$TMP/guard_next.xml" && rows "$TMP/guard_next.xml" "$TMP/guard_next.rows"; then + GXN="$( lines "$TMP/guard_next.rows" )" + if isnum "$GT" && [ "$GXN" -eq "$GT" ] && python3 "$TMP/prefix.py" "$TMP/guard.rows" "$TMP/guard_next.rows"; then + ok "(NX2) the guard cut's next= serves all $GXN rows of total=$GT; the cut's $( lines "$TMP/guard.rows" ) rows are its prefix" + else + no "(NX2) the guard cut's next= served $GXN rows (total=$GT) or the cut's rows are not its prefix" + fi +else + no "(NX2) the guard cut's next= ('${GNX:0:60}') is absent or did not run" +fi + +# (D4b) the guard biconditional over a --pack-top-n sweep: over_ceiling="1" <=> the uncut head passes the guard +for n in 40 100 200 400; do + cli_for --pack-top-n="$n" > "$TMP/pt_$n.xml" + cli_for --pack-top-n="$n" --signatures-only --token-budget=1000000 > "$TMP/pt_${n}_unc.xml" + o="$( attr ctx over_ceiling "$TMP/pt_$n.xml" )"; s="$( attr sigs shown "$TMP/pt_$n.xml" )"; t="$( attr sigs total "$TMP/pt_$n.xml" )" + rows "$TMP/pt_$n.xml" "$TMP/pt_$n.rows"; sn="$( lines "$TMP/pt_$n.rows" )" + # the uncut quantity the guard measures: this answer's bytes up to + the uncut block + q="$( python3 - "$TMP/pt_$n.xml" "$TMP/pt_${n}_unc.xml" <<'PY' +import re, sys +a = open( sys.argv[ 1 ] ).read(); u = open( sys.argv[ 2 ] ).read() +m = re.search( r'', u, re.S ) +print( len( a.split( ' guard -> over_ceiling=1, $sn of $t rows" + else no "(D4b) n=$n: uncut head $q B > guard $GUARD but over_ceiling='$o' rows=$sn total='$t'"; fi + else + if [ -z "$o" ] && isnum "$t" && [ "$sn" -eq "$t" ]; then ok "(D4b) n=$n: uncut head $q B <= guard -> complete ($sn of $t), no over_ceiling" + else no "(D4b) n=$n: uncut head $q B <= guard $GUARD but over_ceiling='$o' rows=$sn total='$t'"; fi + fi +done + +# (D5) the MCP twin serves its whole head: the CLI default's rows (as a set; the dialects order nothing differently, but +# the set is the contract mcpforparitycheck already pins) +mcp_call for "$Q" "" > "$TMP/mcp_def.xml" +MS="$( attr sigs shown "$TMP/mcp_def.xml" )"; MT="$( attr sigs total "$TMP/mcp_def.xml" )" +rows "$TMP/mcp_def.xml" "$TMP/mcp_def.rows"; MN="$( lines "$TMP/mcp_def.rows" )" +if isnum "$MT" && [ "$MT" = "$DT" ] && [ "$MN" = "$MT" ] && { [ -z "$MS" ] || [ "$MS" = "$MN" ]; } \ + && cmp -s <( sort "$TMP/mcp_def.rows" ) <( sort "$TMP/def.rows" ); then + ok "(D5) MCP for serves its whole head: $MN rows == the CLI default's rows (total=$MT)" +else + no "(D5) MCP for served $MN rows (shown='$MS') of total='$MT'; CLI default $DN of $DT — must be the same whole head" +fi + +# (D6) negative: an answer that is complete under the target carries no past-target or guard disclosure +# (a name-exact query: one anchor row set, well under 7.5 KB; on such answers the change is byte-identical) +"$BIN" "$CORPUS" --for="trimSigLadder" --signatures-only "$CF" > "$TMP/narrow.xml" 2>/dev/null; nrc=$? +NPT="$( attr sigs past_target "$TMP/narrow.xml" )"; NOC="$( attr ctx over_ceiling "$TMP/narrow.xml" )" +NB="$( wc -c < "$TMP/narrow.xml" | tr -d ' ' )" +if [ "$nrc" -ne 0 ] || ! isnum "$NB" || [ "$NB" -eq 0 ]; then no "(D6) the narrow name-exact answer did not run" +elif [ "$NB" -ge 7500 ]; then no "(D6) premise: the narrow answer ($NB B) is not under the 7.5 KB target — pick a narrower query" +elif [ -z "$NPT" ] && [ -z "$NOC" ] && ! grep -qi 'runaway\|past_target' "$TMP/narrow.xml"; then ok "(D6) a $NB B complete answer carries no past_target=/over_ceiling=/guard clause" +else no "(D6) a $NB B complete answer carries past_target='$NPT' over_ceiling='$NOC' or a guard/past-target clause"; fi + +# ── explicit targets ───────────────────────────────────────────────────────────────────────────────────── +cli_for --token-budget=20000 > "$TMP/wide.xml" # the default dialect (compact) +cli_for --token-budget=20000 --legend=full > "$TMP/wide_full.xml" +cli_for --token-budget=20000 --legend=compact > "$TMP/wide_compact.xml" +rows "$TMP/wide.xml" "$TMP/wide.rows" +"$BIN" "$CORPUS" --for="$Q" --signatures-only "$CF" > "$TMP/so_def.xml" 2>/dev/null +SWEEP=( 300 1000 2000 3000 6000 ) +prev="" +for n in "${SWEEP[@]}"; do + T="$( tbytes "$n" )" + for lg in full compact; do + cli_for --token-budget="$n" --legend="$lg" > "$TMP/t_${n}_$lg.xml"; trc=$? + if [ "$trc" -ne 0 ] || ! rows "$TMP/t_${n}_$lg.xml" "$TMP/t_${n}_$lg.rows"; then + no "(E) N=$n $lg: the call failed (rc=$trc) or served no block — no arm below can read it" + : > "$TMP/t_${n}_$lg.rows"; continue + fi + # (E2) the header is never shaped: no "(ceiling)" drop, and the wide answer's clauses all ride (S4: read only + # off a call that ran and produced its rows, above) + wf="$TMP/wide_$lg.xml" + python3 "$TMP/legend.py" "$TMP/t_${n}_$lg.xml" "$wf" > "$TMP/t_${n}_$lg.lost"; lrc=$? + if grep -q 'dropped (ceiling)' "$TMP/t_${n}_$lg.xml"; then + no "(E2) N=$n $lg: legend clauses dropped '(ceiling)' — the header must not be shaped by a soft target" + elif [ "$lrc" -ne 0 ]; then + no "(E2) N=$n $lg: no header comment to compare" + elif [ "$( lines "$TMP/t_${n}_$lg.lost" )" -gt 0 ]; then + no "(E2) N=$n $lg: clauses of the wide answer missing: $( head -3 "$TMP/t_${n}_$lg.lost" | tr '\n' '|' | cut -c1-240 )" + else + ok "(E2) N=$n $lg: every clause of the wide answer rides (no clause dropped)" + fi + if [ "$( attr ctx task "$TMP/t_${n}_$lg.xml" )" = "$Q" ] && [ -n "$( attr ctx route "$TMP/t_${n}_$lg.xml" )" ]; then + ok "(E2) N=$n $lg: the root keeps task= verbatim and route=" + else + no "(E2) N=$n $lg: the root lost task= or route= at this target" + fi + done + f="$TMP/t_${n}_full.xml" + if [ -s "$TMP/t_${n}_full.rows" ] && grep -qF "lens for \"$Q\"" "$f"; then ok "(E2) N=$n full: the legend echoes the task" + else no "(E2) N=$n full: the task echo is gone from the legend (or the call failed)"; fi + TOC="$( attr ctx over_ceiling "$f" )"; TN="$( lines "$TMP/t_${n}_full.rows" )" + # (E1) at the tight end of the sweep + if [ "$n" -le 1000 ]; then + TNX="$( attr sigs next "$f" )" + if [ -z "$TOC" ] && [ "$TN" -ge 4 ] && [ -n "$TNX" ]; then + ok "(E1) N=$n: no over_ceiling=, $TN floor rows, the cut carries next=" + else + no "(E1) N=$n: over_ceiling='$TOC', rows=$TN (floor 4), sigs next='${TNX:0:40}' — a soft target is never 'over' and always names its continuation" + fi + fi + # (E3) rank prefix: of the default rows, and of the next wider target + if python3 "$TMP/prefix.py" "$TMP/t_${n}_full.rows" "$TMP/def.rows"; then ok "(E3) N=$n: rows ($TN) are a rank prefix of the default rows" + else no "(E3) N=$n: rows ($TN) are not a rank prefix of the default rows ($DN)"; fi + if [ -n "$prev" ]; then + if python3 "$TMP/prefix.py" "$TMP/t_${prev}_full.rows" "$TMP/t_${n}_full.rows"; then ok "(E3) rows(N=$prev) is a prefix of rows(N=$n)" + else no "(E3) rows(N=$prev) is not a prefix of rows(N=$n) — not monotone"; fi + fi + # (E4) dialects agree on rows + if cmp -s "$TMP/t_${n}_full.rows" "$TMP/t_${n}_compact.rows"; then ok "(E4) N=$n: compact rows == full rows ($TN)" + else no "(E4) N=$n: compact rows ($( lines "$TMP/t_${n}_compact.rows" )) != full rows ($TN)"; fi + # (ST) stop at target, default and --signatures-only modes: rows fit T (or are the floor); a cut is maximal + "$BIN" "$CORPUS" --for="$Q" --signatures-only --token-budget="$n" "$CF" > "$TMP/so_$n.xml" 2>/dev/null + for mode in def so; do + if [ "$mode" = def ]; then a="$f"; ref="$TMP/def.xml"; else a="$TMP/so_$n.xml"; ref="$TMP/so_def.xml"; fi + st="$( python3 - "$a" "$ref" "$T" <<'PY' +import subprocess, sys, re +# a row's CHARGE is the ladder's (serialize.h entryCost): its printed bytes plus the zero cx=/ccx=/in= the lean spelling elides +# (SigEntry::elidedBytes — charged so the kept rows are the ones kept before the elision) +def charge( a, printed ): + return printed + sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +def rb( p ): + s = open( p ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return None + inner = m.group( 1 ); rows = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + r = re.search( r'\br="(\d+)"', a ) + printed = len( d.group( 0 ).encode() ); c = charge( a, printed ); elided += c - printed + rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + rows.sort() + return len( inner.encode() ) + elided, rows +a, ref, T = rb( sys.argv[ 1 ] ), rb( sys.argv[ 2 ] ), int( sys.argv[ 3 ] ) +# round C (owner decision 2026-10-14: RESERVE SECTIONS FIRST): the trailing sections (and the rows) are served first +# and the rows past the target cut fill what they leave of T — the maximality bound is T less the bytes of those sections (U, read +# off the answer: everything after but , plus the rows before it) +ans = open( sys.argv[ 1 ] ).read() +U = ( len( ans.split( '', 1 )[ 1 ].encode() ) - len( '' ) if '' in ans else 0 ) +U += sum( len( h.encode() ) for h in re.findall( r']*/>', ans.split( '" ); sys.exit( 0 ) +ab, ar = a; rr = ref[ 1 ] +k = len( ar ) +fits = ab <= T or k <= 4 +if k >= len( rr ): print( "OK fits=%d rows=%d/%d charged=%d T=%d complete" % ( fits, k, len( rr ), ab, T ) ); sys.exit( 0 ) +nxt = rr[ k ][ 1 ] +maximal = ab + nxt > T - U - 160 +# FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less +# the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve +from softbudget import upper_bound +upper, udet = upper_bound( ans, open( sys.argv[ 2 ] ).read(), T, k, len( rr ), U ) +print( "%s fits=%d maximal=%d upper=%d rows=%d/%d charged=%d next=%d T=%d U=%d%s" % ( "OK" if fits and maximal and upper else "BAD", fits, maximal, upper, + k, len( rr ), ab, nxt, T, U, "" if upper else " -- " + udet ) ) +PY +)" + case "$st" in + OK*) ok "(ST) N=$n $mode: $st" ;; + *) no "(ST) N=$n $mode: $st — the rows must fit the target and stop only where the next row would cross it, never past T less the sections and the tail reserve" ;; + esac + done + prev="$n" +done + +# (E2b) a 900-char task at a tight target: the task stays verbatim and its echo is never dropped +Q900="$Q $( printf 'with the symbol graph and the teleport vector over the whole index %.0s' $( seq 1 14 ) )" +Q900="${Q900:0:900}"; Q900="${Q900% }" +"$BIN" "$CORPUS" --for="$Q900" --token-budget=300 --legend=full "$CF" > "$TMP/q900.xml" 2>/dev/null; qrc=$? # the full dialect echoes the task +if [ "$qrc" -ne 0 ]; then no "(E2b) the 900-char task call failed (rc=$qrc)" +elif [ "$( attr ctx task "$TMP/q900.xml" )" = "$Q900" ] && ! grep -q 'task_echo' "$TMP/q900.xml" && grep -qF "lens for \"$Q900\"" "$TMP/q900.xml"; then + ok "(E2b) a ${#Q900}-char task at N=300: task= verbatim, the echo kept, no task_echo drop" +else + no "(E2b) a ${#Q900}-char task at N=300: task= changed, or the legend echo dropped ('task_echo' in the answer: $( grep -c task_echo "$TMP/q900.xml" ))" +fi +# …and --pack-task, whose ladder took the echo rung at 2000 on base (the rung w3fixbudgetcheck (7) pinned as reachable) +"$BIN" "$CORPUS" --pack-task="$Q900" --token-budget=2000 --legend=full "$CP" > "$TMP/q900p.xml" 2>/dev/null; qrc=$? +if [ "$qrc" -ne 0 ]; then no "(E2b) the 900-char --pack-task call failed (rc=$qrc)" +elif [ "$( attr ctx task "$TMP/q900p.xml" )" = "$Q900" ] && ! grep -q 'task_echo' "$TMP/q900p.xml"; then + ok "(E2b) a ${#Q900}-char --pack-task at N=2000: task= verbatim, no task_echo drop" +else + no "(E2b) a ${#Q900}-char --pack-task at N=2000: task= changed or the echo was dropped (a soft target never sheds the header)" +fi + +# (NX1) the tight cut's next= runs and serves the whole head; the cut's rows are its prefix +NX="$( attr sigs next "$TMP/t_300_full.xml" )" +if [ -n "$NX" ] && run_next "$NX" "$TMP/nx.xml" && rows "$TMP/nx.xml" "$TMP/nx.rows"; then + if cmp -s "$TMP/nx.rows" "$TMP/def.rows" && python3 "$TMP/prefix.py" "$TMP/t_300_full.rows" "$TMP/nx.rows"; then + ok "(NX1) N=300's next= serves the whole head ($( lines "$TMP/nx.rows" ) rows == the default's); the cut is its prefix" + else + no "(NX1) N=300's next= served $( lines "$TMP/nx.rows" ) rows, not the default's $DN in order (or the cut is not its prefix)" + fi +else + no "(NX1) N=300's sigs next= ('${NX:0:60}') is absent or did not run" +fi + +# (E5) stop when complete: a wide target serves exactly the default answer's rows +if cmp -s "$TMP/wide.rows" "$TMP/def.rows" && [ "$DN" = "$DT" ]; then + ok "(E5) --token-budget=20000 serves exactly the default (complete) rows: $DN" +else + no "(E5) --token-budget=20000 served $( lines "$TMP/wide.rows" ) rows; the default served $DN of total=$DT — a target wider than the answer must serve the complete head" +fi + +# (PT1) --pack-top-n sets the head; the target fills it (rule-4 table #4). An uncut block prints no total= (its rows are it). +cli_for --pack-top-n=10 --token-budget=20000 > "$TMP/ptn10.xml"; rows "$TMP/ptn10.xml" "$TMP/ptn10.rows" +P10T="$( attr sigs total "$TMP/ptn10.xml" )" +if { [ "$P10T" = "10" ] || [ -z "$P10T" ]; } && [ "$( lines "$TMP/ptn10.rows" )" -eq 10 ] && python3 "$TMP/prefix.py" "$TMP/ptn10.rows" "$TMP/def.rows"; then + ok "(PT1) --pack-top-n=10 --token-budget=20000: exactly the 10-row head, a prefix of the default rows" +else + no "(PT1) --pack-top-n=10 --token-budget=20000: total='$P10T' rows=$( lines "$TMP/ptn10.rows" ) — a wide target must serve exactly the --pack-top-n head" +fi + +# (E6) MCP for at a tight budget_tokens; (NX3) its continuation re-call serves the whole head +mcp_call for "$Q" ',"budget_tokens":300' > "$TMP/mcp_300.xml" +M3OC="$( attr ctx over_ceiling "$TMP/mcp_300.xml" )"; M3NB="$( attr sigs next_budget_tokens "$TMP/mcp_300.xml" )" +if [ -z "$M3OC" ] && isnum "$M3NB"; then ok "(E6) MCP for budget_tokens=300: no over_ceiling=, next_budget_tokens=$M3NB" +else no "(E6) MCP for budget_tokens=300: over_ceiling='$M3OC' next_budget_tokens='$M3NB'"; fi +rows "$TMP/mcp_300.xml" "$TMP/mcp_300.rows" +if isnum "$M3NB"; then + mcp_call for "$Q" ",\"budget_tokens\":$M3NB" > "$TMP/mcp_nx.xml"; rows "$TMP/mcp_nx.xml" "$TMP/mcp_nx.rows" + if cmp -s "$TMP/mcp_nx.rows" "$TMP/mcp_def.rows" && python3 "$TMP/prefix.py" "$TMP/mcp_300.rows" "$TMP/mcp_nx.rows"; then + ok "(NX3) MCP re-call at next_budget_tokens=$M3NB serves the whole head ($( lines "$TMP/mcp_nx.rows" ) rows); the cut is its prefix" + else + no "(NX3) MCP re-call at next_budget_tokens=$M3NB served $( lines "$TMP/mcp_nx.rows" ) rows; the MCP default serves $MN" + fi +else + no "(NX3) no next_budget_tokens= to re-call with" +fi + +# (P1) --pack-task at a tight target +"$BIN" "$CORPUS" --pack-task="$Q" --token-budget=300 "$CP" > "$TMP/pt.xml" 2>/dev/null; prc=$? +PTOC="$( attr ctx over_ceiling "$TMP/pt.xml" 2>/dev/null )"; pa=$? +if [ "$prc" -ne 0 ] || [ "$pa" -ne 0 ]; then no "(P1) --pack-task --token-budget=300 did not run (rc=$prc) or has no root" +elif [ -z "$PTOC" ] && ! grep -q '(ceiling)\|header floor' "$TMP/pt.xml" && [ "$( attr ctx task "$TMP/pt.xml" )" = "$Q" ]; then + ok "(P1) --pack-task --token-budget=300: no over_ceiling=, no '(ceiling)' drop, task= verbatim (soft target)" +else + no "(P1) --pack-task --token-budget=300: over_ceiling='$PTOC', '(ceiling)'/'header floor' notes: $( grep -o '(ceiling)\|header floor' "$TMP/pt.xml" | wc -l | tr -d ' ' ) — a soft target is never 'over' and never sheds the header" +fi + +# (P2) R9: the explicit default value serves the default's sections — the quota cascade is the one shaping rule both run +"$BIN" "$CORPUS" --pack-task="$Q" "$CP" > "$TMP/pt_def.xml" 2>/dev/null; p2a=$? +"$BIN" "$CORPUS" --pack-task="$Q" --token-budget=6000 "$CP" > "$TMP/pt_6000.xml" 2>/dev/null; p2b=$? +if [ "$p2a" -ne 0 ] || [ "$p2b" -ne 0 ] || ! grep -q ' "$TMP/mcp_explore.xml" +EOC="$( attr ctx over_ceiling "$TMP/mcp_explore.xml" 2>/dev/null )"; ea=$? +if [ "$ea" -ne 0 ]; then no "(P3) MCP explore budget_tokens=300 produced no root" +elif [ -z "$EOC" ]; then ok "(P3) MCP explore budget_tokens=300: no over_ceiling= (soft target)" +else no "(P3) MCP explore budget_tokens=300: over_ceiling='$EOC'"; fi + +# ── ROUND C (owner decisions 2026-10-14) ───────────────────────────────────────────────────────────────────── +# FOLD C: the default answer's carries the file page's first page (`--for=TASK --limit=40`): after the rank rows +# (paths only: the positive-score files outside the shown sigs rows), the page's first 40 rows not already listed, each with +# sym=; total= every positive-score file (the page's total=); a cut tail's next= is the page from its first unprinted page row. +# RESERVE SECTIONS FIRST: under an explicit --token-budget the trailing sections (, , , the tail) keep at +# least the share the retired hard ceiling gave them, and the rows past its cut fill what they leave of T (ST above reads T - U). +cat > "$TMP/tail.py" <<'PY' +# tail.py FILE MODE: the of an answer — "rank" (paths of rows without sym=), "page" (psym of rows with sym=), +# "all" (psym), "attrs" (total shown capped next). exit 3: no . +import re, sys, html +s = open( sys.argv[ 1 ] ).read() +m = re.search( r']*)>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +a = dict( re.findall( r'(\w+)="([^"]*)"', m.group( 1 ) ) ) +rows = [ ( html.unescape( p ), html.unescape( y ), bool( h ) ) for p, h, y in re.findall( r'', m.group( 2 ) ) ] +mode = sys.argv[ 2 ] +if mode == "attrs": print( a.get( "total", "" ), a.get( "shown", "" ), a.get( "capped", "" ), html.unescape( a.get( "next", "" ) ) ) +for p, y, h in rows: + if mode == "rank" and not h: print( p ) + if mode == "page" and h: print( p + "\t" + y ) + if mode == "all": print( p + "\t" + y ) +PY +cat > "$TMP/page.py" <<'PY' +# page.py RANKFILE < page: the page's rows (psym) not among RANKFILE's paths, in page order; "total=N" first +import re, sys, html +s = sys.stdin.read(); rank = set( open( sys.argv[ 1 ] ).read().split( "\n" ) ) +m = re.search( r']*\btotal="([0-9]+)"', s ); print( "total=" + ( m.group( 1 ) if m else "" ) ) +for p, y in re.findall( r']*? sym="([^"]*)"', s ): + if html.unescape( p ) not in rank: print( html.unescape( p ) + "\t" + html.unescape( y ) ) +PY +# (C1) the default answer's tail: rank rows outside the shown head, then page 0 of the file page, sym= equal +if python3 "$TMP/tail.py" "$TMP/def.xml" attrs > "$TMP/c1.attrs"; then + read -r C1T C1S C1C C1N < "$TMP/c1.attrs" + python3 "$TMP/tail.py" "$TMP/def.xml" rank > "$TMP/c1.rank"; python3 "$TMP/tail.py" "$TMP/def.xml" page > "$TMP/c1.page" + "$BIN" "$CORPUS" --for="$Q" --limit=40 "$CF" 2>/dev/null | python3 "$TMP/page.py" "$TMP/c1.rank" > "$TMP/c1.want" + C1PT="$( head -1 "$TMP/c1.want" | sed 's/total=//' )"; sed -i.bak '1d' "$TMP/c1.want" + C1DUP="$( python3 - "$TMP/def.xml" "$TMP/c1.rank" <<'PY' +import re, sys +head = set( re.findall( r']*?\bp="([^"]*)"', open( sys.argv[ 1 ] ).read() ) ) +print( sum( 1 for p in open( sys.argv[ 2 ] ).read().split( "\n" ) if p in head ) ) +PY +)" + if [ -s "$TMP/c1.page" ] && cmp -s "$TMP/c1.page" "$TMP/c1.want" && [ "$C1T" = "$C1PT" ] && [ "$C1DUP" = 0 ] && [ "$( lines "$TMP/c1.rank" )" -le 24 ]; then + ok "(C1) default tail: $( lines "$TMP/c1.rank" ) rank rows outside the head, then the page's first rows ($( lines "$TMP/c1.page" ), sym= equal); total=$C1T == the page's" + else + no "(C1) default tail: page rows $( lines "$TMP/c1.page" ) vs the page's $( lines "$TMP/c1.want" ), total=$C1T vs page $C1PT, $C1DUP rank row(s) in the head" + fi + # (C2) a capped tail's next= is the page from its first unprinted page row, and it runs (rc 0, a page at that offset) + if [ "$C1C" = 1 ] && [ "$C1S" -lt "$C1T" ]; then + C2OFF="$( printf '%s' "$C1N" | sed -nE "s/^--for='.*' --limit=40( --offset=([0-9]+))?\$/\2/p" )" + if printf '%s' "$C1N" | grep -qE "^--for='$Q' --limit=40( --offset=[0-9]+)?\$" && run_next "$C1N" "$TMP/c2.xml" \ + && grep -qE "^]* offset=\"${C2OFF:-0}\"" "$TMP/c2.xml" && [ "${C2OFF:-0}" -ge "$( lines "$TMP/c1.page" )" ]; then + ok "(C2) the capped tail's next= ('${C1N:0:70}…') runs and serves the page from offset ${C2OFF:-0} (past every page row it printed)" + else + no "(C2) the capped tail's next= ('$C1N') is not the file page from its first unprinted row, or did not run" + fi + else + no "(C2) premise: the default tail is not capped (total=$C1T shown=$C1S) — re-anchor on a capped tail" + fi + # (C3) the size is disclosed: est_tokens= on the root prices the whole answer, tail included (markup rate 2.5 B/token) + C3E="$( attr ctx est_tokens "$TMP/def.xml" )"; C3B="$( wc -c < "$TMP/def.xml" | tr -d ' ' )" + if isnum "$C3E" && [ "$(( C3E * 25 ))" -ge "$(( C3B * 10 * 97 / 100 ))" ]; then ok "(C3) est_tokens=$C3E prices the $C3B-byte answer, its tail included" + else no "(C3) est_tokens='$C3E' does not price the $C3B-byte answer (the tail rides unpriced)"; fi +else + no "(C1) the default answer carries no " +fi +# (C4) the JSON twin: the same tail rows (files + sym index-aligned), total=, next present exactly when cut +cli_for --json > "$TMP/def.json" +C4="$( python3 - "$TMP/def.json" "$TMP/def.xml" <<'PY' +import json, re, sys, html +t = json.load( open( sys.argv[ 1 ] ) ).get( "tail", {} ); x = open( sys.argv[ 2 ] ).read() +xa = dict( re.findall( r'(\w+)="([^"]*)"', re.search( r']*)>', x ).group( 1 ) ) ) +xr = [ ( html.unescape( p ), html.unescape( y ) ) for p, _, y in re.findall( r'', x ) ] +ok = list( zip( t.get( "files", [] ), t.get( "sym", [] ) ) ) == xr and str( t.get( "total" ) ) == xa.get( "total" ) \ + and ( "next" in t ) == ( xa.get( "capped" ) == "1" ) and ( t.get( "next" ) or "" ) == html.unescape( xa.get( "next", "" ) ) +print( "OK" if ok else "BAD json rows=%d xml rows=%d total %s/%s" % ( len( t.get( "files", [] ) ), len( xr ), t.get( "total" ), xa.get( "total" ) ) ) +PY +)" +if [ "$C4" = OK ]; then ok "(C4) --json tail == the XML tail (files + sym, total=, next)" +else no "(C4) --json tail differs from the XML tail: $C4"; fi +# (C5) the MCP twin serves the same tail rows as the CLI default +mcp_call for "$Q" > "$TMP/mcp_def_tail.xml" +if python3 "$TMP/tail.py" "$TMP/mcp_def_tail.xml" all > "$TMP/c5.mcp" && python3 "$TMP/tail.py" "$TMP/def.xml" all > "$TMP/c5.cli" \ + && [ -s "$TMP/c5.cli" ] && cmp -s "$TMP/c5.mcp" "$TMP/c5.cli"; then ok "(C5) MCP for serves the CLI default's tail rows ($( lines "$TMP/c5.cli" ), sym= included)" +else no "(C5) MCP for's tail rows differ from the CLI default's ($( lines "$TMP/c5.mcp" ) vs $( lines "$TMP/c5.cli" ))"; fi +# (C6) under every explicit target of the sweep the tail is present, and a cut tail names a next= that runs +for n in "${SWEEP[@]}"; do + f="$TMP/t_${n}_full.xml" + if ! python3 "$TMP/tail.py" "$f" attrs > "$TMP/c6.attrs"; then no "(C6) N=$n: no "; continue; fi + read -r T6 S6 C6 N6 < "$TMP/c6.attrs" + if [ "$C6" = 1 ]; then + if [ -n "$N6" ] && run_next "$N6" "$TMP/c6.xml" && grep -q '^ "$TMP/sec.py" <<'PY' +# sec.py SECTION < answer: the section's rows (lego: iface+impl+m; tail: t; hops: h+c) +import re, sys +tags = { "lego": ( "iface", "impl", "m" ), "tail": ( "t", ), "hops": ( "h", "c" ) }[ sys.argv[ 1 ] ] +s = sys.stdin.read(); m0 = re.search( r'<%s\b[^>]*>' % sys.argv[ 1 ], s ) +if not m0: print( 0 ); sys.exit( 0 ) +el = m0.group( 0 ) if m0.group( 0 ).endswith( "/>" ) else s[ m0.start(): s.find( "" % sys.argv[ 1 ], m0.end() ) ] +print( sum( len( re.findall( r'<%s[ >/]' % t, el ) ) for t in tags ) ) +PY +QNODE="where is the node interface implemented"; QHAND="how does the handler process a request and call the service" +for spec in "legotransitivefix|$QNODE|600|lego|9" "legotransitivefix|$QNODE|1000|lego|9" "forcompletefix|$QNODE|1500|tail|11" \ + "forcompletefix|$QNODE|3000|tail|1" "forcompletefix|$QHAND|3000|tail|1"; do + IFS='|' read -r fx q n sec want <<< "$spec" + "$BIN" "test/$fx" --no-cache --for="$q" --token-budget="$n" > "$TMP/rf.xml" 2>/dev/null; rrc=$? + got="$( python3 "$TMP/sec.py" "$sec" < "$TMP/rf.xml" )" + if [ "$rrc" -eq 0 ] && isnum "$got" && [ "$got" -ge "$want" ]; then ok "(RF1) $fx '$q' N=$n: <$sec> keeps $got rows >= the retired ceiling's $want" + else no "(RF1) $fx '$q' N=$n: <$sec> serves '$got' rows < the retired ceiling's $want (rc=$rrc) — a section lost its share to the rows"; fi +done + +# (RF2) FIX1 R2 (review rv-forbudget-soft): the --json tail is a trailing section too — at an explicit N it keeps at least the +# bytes the retired hard ceiling (d4325a8c) gave it. Lower bounds measured on frozen in-repo fixtures; 974dd5b1 served 470/412/434 +# and 514/438 B there (RED), the tail funded from what the refilled rows left. +for spec in "receiverevidencefix|$QNODE|1000|527" "receiverevidencefix|$QNODE|2000|582" "receiverevidencefix|$QNODE|3000|622" \ + "falseedgefix|$QHAND|2000|544" "falseedgefix|$QHAND|3000|526"; do + IFS='|' read -r fx q n want <<< "$spec" + got="$( "$BIN" "test/$fx" --no-cache --for="$q" --token-budget="$n" --json 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(json.dumps(d["tail"],separators=(",",":"))))' 2>/dev/null )" + if isnum "$got" && [ "$got" -ge "$want" ]; then ok "(RF2) $fx '$q' N=$n --json: the tail keeps $got B >= the retired ceiling's $want B" + else no "(RF2) $fx '$q' N=$n --json: the tail serves '$got' B < the retired ceiling's $want B — a trailing section lost its share"; fi +done +# (RP10) FIX1 R1: rows are a rank prefix and MONOTONE in N at a step of 10, at three root path lengths (the header's root= moves +# which N the cut steps at; 974dd5b1 dropped a row at 1490 with a 28-char root: the tail's reserve stepped with the cut). The +# fornotesbudgetcheck corpus shape (72 noted symbols), swept 1400..1600, default and --json dialects. +RPD="$( mktemp -d /tmp/rwrp.XXXXXX )" || RPD="$TMP/rp" +mkdir -p "$RPD/a/c" "$RPD/medium_root_path_component/c" "$TMP/a_long_root_path_component_for_the_rp10_sweep/c" +for root in "$RPD/a/c" "$RPD/medium_root_path_component/c" "$TMP/a_long_root_path_component_for_the_rp10_sweep/c"; do + mkdir -p "$root/src" + python3 - "$root" <<'PY' +import os, sys +for i in range( 12 ): + with open( os.path.join( sys.argv[ 1 ], "src", "mod%d.py" % i ), "w" ) as f: + for j in range( 6 ): + f.write( "def widgetRoutine%d_%d( alpha, beta ):\n \"\"\"Route the widget pipeline stage %d.%d through the dispatcher.\"\"\"\n return alpha + beta\n\n\n" % ( i, j, i, j ) ) +PY + ( cd "$root" && git init -q -b main . && git add -A && git -c user.email=gate@example.invalid -c user.name=gate commit -qm corpus ) >/dev/null 2>&1 + for i in 0 1 2 3 4 5 6 7 8 9 10 11; do for j in 0 1 2 3 4 5; do + "$BIN" "$root" --note-add="widgetRoutine${i}_${j}: this routine is load-bearing for the widget dispatcher and must not be reordered without rechecking the stage table downstream" >/dev/null 2>&1 + done; done + rp10="$( python3 - "$BIN" "$root" <<'PY' +import json, re, subprocess, sys +B, root = sys.argv[ 1 ], sys.argv[ 2 ]; q = "widget dispatcher pipeline stage" +def rows( s, js ): + if js: + return [ "%s:%s:%s" % ( e.get( "p" ), e.get( "l" ), e.get( "n" ) ) for e in sorted( json.loads( s ).get( "sigs", [] ), key=lambda e: e.get( "r", 10**9 ) ) ] + m = re.search( r"]*>(.*?)", s, re.S ); out = [] + for x in re.finditer( r"]*)>", m.group( 1 ) if m else "" ): + g = lambda k: ( re.search( r"\b" + k + r'="([^"]*)"', x.group( 1 ) ) or [ None, "?" ] )[ 1 ] + out.append( ( int( g( "r" ) ) if g( "r" ).isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) + return [ b for a, b in sorted( out ) ] +bad = [] +for js in ( False, True ): + extra = [ "--json" ] if js else [] + ref = rows( subprocess.run( [ B, root, "--for=" + q ] + extra, capture_output=True ).stdout.decode(), js ); prev = None + for n in range( 1400, 1601, 10 ): + r = rows( subprocess.run( [ B, root, "--for=" + q, "--token-budget=%d" % n ] + extra, capture_output=True ).stdout.decode(), js ) + if not r or r != ref[ :len( r ) ] or ( prev is not None and prev != r[ :len( prev ) ] ): + bad.append( "%s@%d:%s->%d" % ( "json" if js else "xml", n, len( prev ) if prev else "-", len( r ) ) ) + prev = r +print( "OK" if not bad else "BAD " + " ".join( bad[ :4 ] ) ) +PY +)" + case "$rp10" in + OK) ok "(RP10) root of ${#root} chars: rows are a rank prefix, monotone in N, 1400..1600 step 10, default and --json" ;; + *) no "(RP10) root of ${#root} chars: $rp10 — the rows past the cut fell as N grew (or are not a rank prefix)" ;; + esac +done +rm -rf "$RPD" + +# ── twins: the CI-gate personality of --token-budget is unchanged on the map, --query and --recall ─────── +"$BIN" "$CORPUS" --token-budget=100 "$CM" > "$TMP/map.out" 2>/dev/null; grc=$? +GW="$( attr r withheld "$TMP/map.out" 2>/dev/null )" +if [ "$grc" -eq 3 ] && [ "$GW" = "1" ] && ! grep -q ' "$TMP/query.out" 2>/dev/null; qrc=$? +QW="$( attr r withheld "$TMP/query.out" 2>/dev/null )" +if [ "$qrc" -eq 3 ] && [ "$QW" = "1" ] && ! grep -q ' "$TMP/recall.out" 2>/dev/null; rrc=$? +if [ "$rrc" -eq 3 ] && grep -q 'shown=0' "$TMP/recall.out" && grep -q 'withheld_est_tokens=' "$TMP/recall.out"; then + ok "(G3) --recall --token-budget=100: exit 3, shown=0, the withheld price disclosed" +else + no "(G3) --recall --token-budget=100: rc=$rrc — the gate personality must not move" +fi + +if [ "$fail" -ne 0 ]; then echo "FAILURES ABOVE"; exit 1; fi +echo "forsoftbudgetcheck: all arms pass" diff --git a/test/forwidencheck.sh b/test/forwidencheck.sh index eb6e1d5b9..9132c6fc6 100755 --- a/test/forwidencheck.sh +++ b/test/forwidencheck.sh @@ -14,8 +14,8 @@ # lookup is how an answer is scored complete). # (2) The page is ranked file-first by a bounded union-coverage score: the fixture's gold file, whose four # symbols each match ONE query term, sits at file-rank 8..30 on the page and is ABSENT from the default -# --for answer (its best symbol is weaker than twenty single-term files that outrank it in best-symbol -# order, which is what the default's walks). +# --for answer's head and tail RANK rows (its best symbol is weaker than twenty single-term files that outrank it +# in best-symbol order, which is what those rows walk) — FOLD C: the tail's page rows carry it. # (3) The page is deterministic and well-formed; a cut page carries the house paging vocabulary # (shown= total= capped="1" has_more= next_offset=) and a next= naming the next page; the second page # has no row in common with the first and the pages concatenate to the wider page in order. @@ -126,10 +126,26 @@ print( " ".join( sorted( set( re.findall( r"\s([a-z_]+)=\"", m.group( 2 ) ) ) ) # ── (1) the default answer does NOT carry the gold; the page does, verbatim ───────────────────────────── run --for="$THIN" >"$TMP/default.xml"; rc=$? [ "$rc" = 0 ] || no "(1) default --for exited $rc: $( head -c 200 "$TMP/err" )" -if grep -q "p=\"$GOLD\"" "$TMP/default.xml"; then - no "(1) the default --for answer already names $GOLD — the fixture does not exercise widening" +# FOLD C (forbudget-soft round C, owner decision 2026-10-14; rule 4): the default answer's now carries page 0 of the +# file page (its rows with sym=), so the widening this fixture measures is IN the default answer. The by-design break: the +# old "the default answer does not name the gold" now reads the head and the tail's RANK rows (no sym=: the best-symbol walk +# the gold falls past) — the fixture still exercises widening — and the page rows must name the gold, exactly as the page does. +if python3 - "$TMP/default.xml" "$GOLD" <<'PY' +import re, sys +s = open( sys.argv[ 1 ] ).read(); g = sys.argv[ 2 ] +head = re.findall( r']*?\bp="([^"]*)"', s ) +rank = [ p for p, sym in re.findall( r'', s ) if not sym ] +sys.exit( 1 if g in head or g in rank else 0 ) +PY +then + ok "(1) the default --for answer's head and tail rank rows do not name $GOLD (the best-symbol walk it falls past)" +else + no "(1) the default --for answer's head or rank rows already name $GOLD — the fixture does not exercise widening" +fi +if grep -q " (the file-grain surface the gold fell past)" \ || no "(1) presence guard: no in the default answer — the fixture shape changed" diff --git a/test/gitchildoptscheck.sh b/test/gitchildoptscheck.sh new file mode 100755 index 000000000..06fa4d679 --- /dev/null +++ b/test/gitchildoptscheck.sh @@ -0,0 +1,402 @@ +#!/usr/bin/env bash +# gitchildoptscheck.sh — every git child ripwire starts carries the options that neutralise repository-configured +# filter and diff programs (src/gitcmd.h, src/githarden.h). +# +# githardencheck.sh covers core.fsmonitor. This gate covers the filter drivers a repository's own config defines +# (`filter..clean/smudge/process`) and the diff programs (diff.external, diff..command/textconv). +# +# The fixture: one commit, `*.c filter=ev`, and a local filter.ev.clean whose program only appends a line to MARK and +# passes its input through, so every time git runs it is counted. A ripwire run can leave the index refreshed, +# which changes what the next run does, so EVERY behavioural row rebuilds the fixture first. +# +# Arms: +# (A) presence: on a fresh fixture, plain `git --no-optional-locks status` runs the filter — the fixture is live. +# If this git does not, the gate cannot conclude and exits 2 rather than pass vacuously. +# (B) red, only when RIPWIRE_BASE_BIN names a pre-fix binary: ` --situ` runs the filter. +# (C) green: map, --situ, --quality-delta, --hotspots, --rank-by=churn and --doctor each run the filter 0 times, +# each on a fresh fixture, and each must have answered (exit status + a stdout marker) so a run that stops +# before git cannot pass as "0 firings". +# Also --plan-lint=FILE with FILE in the fixture and the crawl root elsewhere (FILE's repository is probed late), +# red under RIPWIRE_BASE_BIN on a binary without that probe. +# (D) disclosure: stderr carries git_harden=filter-driver; stdout does not; --doctor's git-config-trust row +# reports filter_drivers="1" filter_scope="local" filter_pinned="1", and filter_drivers="0" on a plain repo. +# (E) MCP roots named only in a request: a server started with NO root drives situational_awareness, +# quality_delta, stray_content, edit_check and quality_baseline at the fixture, an `analyze` whose `paths` +# names the fixture and a second repository (a multi-root workspace), and a `batch`; a server started on the +# second repository is asked quality_delta about the fixture. Each row: 0 firings, and the request answered. +# (S) the fixture with its .git/config moved elsewhere and replaced by a symlink to it (git follows the link; the +# cheap pre-scan does not read through it): --situ and an MCP quality_delta run the filter 0 times. +# (F) a PATH shim that records each git child's argv and GIT_* environment and then execs the real git. Over the +# same verbs, on the fixture and on a plain multi-commit repository with a working-tree edit: +# F1 every child starts with --no-optional-locks -c core.fsmonitor=false -c log.showSignature=false +# (the `config --get core.fsmonitor` probe excepted, as in githardencheck K); +# F2 on the fixture, every child after the filter-driver probe carries the four filter.ev pins; +# F3 every diff/log/show/blame child carries --no-ext-diff and --no-textconv, and every status/diff child +# carries --ignore-submodules=dirty; +# F4 no child saw a GIT_EXTERNAL_DIFF, GIT_ATTR_SOURCE or GIT_CONFIG_PARAMETERS ripwire did not inherit. +# A per-verb table (children, subcommands, F1-F3 violations) is printed. Controls: a synthetic log with one +# child lacking each option must be flagged by the same checker, and with RIPWIRE_BASE_BIN the base binary's +# logs must fail F2 and F3. +# (G) structural: every source line that spells a git log/diff/show/blame invocation carries --no-textconv, and +# every `status --porcelain` carries --ignore-submodules=dirty; a mutation control on a COPY of src/ must be +# found by the same scan. +# +# Not armed: two callers racing to probe the same root (githarden.h hardenLateRoot holds its lock across the probe). +# Both servers dispatch requests one at a time, so no request sequence can make two probes of one root overlap. +# +# Usage: RIPWIRE_BIN=build/ripwire [RIPWIRE_BASE_BIN=/path/to/pre-fix/ripwire] bash test/gitchildoptscheck.sh + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +# shellcheck source=lib/clean-env.sh +. "$ROOT/test/lib/clean-env.sh" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +BASE="${RIPWIRE_BASE_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; return 0; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +REALGIT="$( command -v git )" +[ -n "$REALGIT" ] || { echo "git not on PATH — this gate needs it"; exit 2; } +echo "gitchildoptscheck: BIN=$BIN git=$( "$REALGIT" --version )${BASE:+ BASE=$BASE}" + +TMP="$( mktemp -d )" +trap 'rm -rf "$TMP"' EXIT +TMP="$( cd "$TMP" && pwd -P )" +export XDG_CACHE_HOME="$TMP/cache"; mkdir -p "$XDG_CACHE_HOME" +MARK="$TMP/MARK" +CMD="$TMP/cmd.sh" +printf '#!/bin/sh\necho "cmd $* in $PWD" >> %s\ncat\n' "$MARK" > "$CMD"; chmod +x "$CMD" +FX="$TMP/f12b" +fired(){ [ -f "$MARK" ] && wc -l < "$MARK" | tr -d ' ' || echo 0; } + +# The fixture: clean filter, content unchanged, only the stat data stale. +mkfx(){ + rm -rf "$FX" "$MARK" "$TMP/cfgstore" + mkdir -p "$FX" + ( cd "$FX" && "$REALGIT" init -q && printf 'int f(void){return 1;}\n' > f.c && printf '*.c filter=ev\n' > .gitattributes \ + && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm i \ + && "$REALGIT" config filter.ev.clean "$CMD clean" && touch -t 202001010000 f.c ) >/dev/null 2>&1 \ + || { echo "fixture build failed"; exit 2; } +} + +# ── (A) presence ──────────────────────────────────────────────────────────────────────────────────────── +mkfx +"$REALGIT" --no-optional-locks -C "$FX" status --porcelain >/dev/null 2>&1 +if [ "$( fired )" -ge 1 ]; then + ok "(A) presence: plain git status runs the fixture's clean filter ($( fired ) call) — the fixture is live" +else + echo " SKIP (A) this git does not run the clean filter on a stale-stat file here — the gate cannot conclude (exit 2)"; exit 2 +fi + +# run_row TAG RCS MARKER CMD…: fresh fixture, run, count. Sets RVERDICT (ok|rc|marker|fired) and RFIRED. +run_row(){ + rtag="$1"; rrcs="$2"; rmarker="$3"; shift 3 + mkfx + "$@" > "$TMP/r-$rtag.out" 2> "$TMP/r-$rtag.err" + RRC=$? + RFIRED="$( fired )" + case " $rrcs " in *" $RRC "*) ;; *) RVERDICT=rc; return 0 ;; esac + if [ -n "$rmarker" ] && ! grep -qF -- "$rmarker" "$TMP/r-$rtag.out"; then RVERDICT=marker; return 0; fi + if [ "$RFIRED" -eq 0 ]; then RVERDICT=ok; else RVERDICT=fired; fi + return 0 +} + +# ── (B) red on the pre-fix binary, when one is named ──────────────────────────────────────────────────── +if [ -n "$BASE" ]; then + run_row "base-situ" "0" "" "$BASE" "$FX" --situ + if [ "$RVERDICT" = fired ]; then + ok "(B) red: the pre-fix binary's --situ ran the clean filter $RFIRED time(s)" + else + no "(B) the pre-fix binary did not run the filter (verdict $RVERDICT) — the red cannot be shown" + fi +else + echo " NOTE (B) RIPWIRE_BASE_BIN not set — the red arm is skipped" +fi + +# ── (C) green across the git-backed verbs ─────────────────────────────────────────────────────────────── +VERBS='map||0|< +situ|--situ|0|situational-awareness +quality-delta|--quality-delta|0 2|< +hotspots|--hotspots|0|< +churn|--rank-by=churn|0|< +doctor|--doctor|0 1| "$TMP/elsewhere/e.c" +run_row "c-plan-lint-file" "0" "plan-lint" "$BIN" "$TMP/elsewhere" --plan-lint="$FX/f.c" +case "$RVERDICT" in + ok) ok "(C) plan-lint, FILE in the fixture and the root elsewhere: answered, and the clean filter did not run (0 firings)" ;; + rc) no "(C) plan-lint FILE elsewhere: exit $RRC, expected 0: $( head -c 200 "$TMP/r-c-plan-lint-file.err" )" ;; + marker) no "(C) plan-lint FILE elsewhere: stdout lacks 'plan-lint' — the verb did not answer" ;; + *) no "(C) plan-lint FILE elsewhere: the clean filter ran $RFIRED time(s): $( head -2 "$MARK" | tr '\n' ';' )" ;; +esac +grep -q 'git_harden=filter-driver' "$TMP/r-c-plan-lint-file.err" \ + && ok "(C) plan-lint FILE elsewhere: stderr discloses git_harden=filter-driver for FILE's repository" \ + || no "(C) plan-lint FILE elsewhere: no git_harden=filter-driver line: $( head -c 200 "$TMP/r-c-plan-lint-file.err" )" +if [ -n "$BASE" ]; then + run_row "base-plan-lint-file" "0" "plan-lint" "$BASE" "$TMP/elsewhere" --plan-lint="$FX/f.c" + [ "$RVERDICT" = fired ] \ + && ok "(B) red: the pre-fix binary's plan-lint on FILE elsewhere ran the clean filter $RFIRED time(s)" \ + || no "(B) the pre-fix binary's plan-lint did not run the filter (verdict $RVERDICT) — the red cannot be shown" +fi + +# ── (D) disclosure ────────────────────────────────────────────────────────────────────────────────────── +grep -q 'git_harden=filter-driver' "$TMP/r-c-situ.err" \ + && ok "(D) stderr discloses the neutralisation: $( grep -m1 -o 'git_harden=filter-driver' "$TMP/r-c-situ.err" )" \ + || no "(D) stderr carries no git_harden=filter-driver line: $( head -c 300 "$TMP/r-c-situ.err" )" +grep -q 'git_harden=' "$TMP/r-c-situ.out" \ + && no "(D) stdout carries a git_harden= line (the disclosure leaked into the answer)" \ + || ok "(D) stdout carries no git_harden= (disclosure stays on stderr)" +DOCROW="$( grep -oE ']*/>' "$TMP/r-c-doctor.out" )" +case "$DOCROW" in + *'filter_drivers="1" filter_scope="local" filter_pinned="1"'*) ok "(D) --doctor reports the pinning: $DOCROW" ;; + *) no "(D) --doctor's git-config-trust row lacks filter_drivers=\"1\" filter_scope=\"local\" filter_pinned=\"1\": '${DOCROW:-}'" ;; +esac + +# ── (E) MCP roots named only in a request ────────────────────────────────────────────────────────────── +# A second, plain repository: the other member of the multi-root row, and the startup root of the last row. +SECOND="$TMP/second" +( mkdir -p "$SECOND" && cd "$SECOND" && "$REALGIT" init -q && printf 'int g(void){return 2;}\n' > g.c && "$REALGIT" add -A \ + && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm i ) >/dev/null 2>&1 || no "(E) could not build the second repository" +mcpsession(){ # ARGUMENTS-JSON for one tools/call (id 2) after the handshake + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"gitchildoptscheck","version":"1"}}}' + printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}' + printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}\n' "$1" "$2" +} +MCPROWS="situational_awareness|{\"path\":\"$FX\"}| +quality_delta|{\"path\":\"$FX\"}| +stray_content|{\"path\":\"$FX\"}| +edit_check|{\"path\":\"$FX\",\"symbol\":\"f\"}| +quality_baseline|{\"path\":\"$FX\"}| +analyze|{\"paths\":[\"$FX\",\"$SECOND\"]}| +batch|{\"path\":\"$FX\",\"queries\":[{\"verb\":\"stray_content\"},{\"verb\":\"callers\",\"symbol\":\"f\"}]}| +quality_delta|{\"path\":\"$FX\"}|$SECOND" +erow=0 +while IFS='|' read -r everb eargs estart; do + erow=$(( erow + 1 )) + mcpsession "$everb" "$eargs" > "$TMP/mcp-$erow.in" + if [ -n "$estart" ]; then + run_row "e-$erow" "0" '"id":2,"result"' sh -c '"$1" "$3" --mcp < "$2"' _ "$BIN" "$TMP/mcp-$erow.in" "$estart" + elabel="$everb (server started on another root)" + else + run_row "e-$erow" "0" '"id":2,"result"' sh -c '"$1" --mcp < "$2"' _ "$BIN" "$TMP/mcp-$erow.in" + elabel="$everb" + case "$eargs" in *'"paths"'*) elabel="$everb (two-root paths workspace)" ;; esac + fi + case "$RVERDICT" in + ok) ok "(E) MCP request-named root, $elabel: answered (id 2 result) and the clean filter did not run" ;; + *) no "(E) MCP request-named root, $elabel: verdict $RVERDICT, exit $RRC, firings $RFIRED: $( head -c 200 "$TMP/r-e-$erow.err" )" ;; + esac +done <<< "$MCPROWS" +DOCPLAIN="$( "$BIN" "$SECOND" --doctor 2>/dev/null | grep -oE ']*/>' )" +case "$DOCPLAIN" in + *'filter_drivers="0"'*filter_scope*) no "(D) --doctor on a repository with no filter driver still names a scope: $DOCPLAIN" ;; + *'filter_drivers="0"'*) ok "(D) --doctor on a repository with no filter driver: $DOCPLAIN" ;; + *) no "(D) --doctor on a repository with no filter driver: '${DOCPLAIN:-}'" ;; +esac + +# ── (S) the governing config file is a symlink ────────────────────────────────────────────────────────── +mkfx_symcfg(){ + mkfx + mkdir -p "$TMP/cfgstore" && mv "$FX/.git/config" "$TMP/cfgstore/config" && ln -s "$TMP/cfgstore/config" "$FX/.git/config" \ + || { echo "symlinked-config fixture build failed"; exit 2; } +} +mkfx_symcfg +"$REALGIT" --no-optional-locks -C "$FX" status --porcelain >/dev/null 2>&1 +[ "$( fired )" -ge 1 ] \ + && ok "(S) presence: with .git/config a symlink, plain git status still runs the clean filter (git follows the link)" \ + || no "(S) presence: git did not follow the symlinked config — the arm cannot conclude" +srow(){ # TAG CMD…: like run_row, on the symlinked-config fixture + stag="$1"; shift + mkfx_symcfg + "$@" > "$TMP/r-$stag.out" 2> "$TMP/r-$stag.err" + SRC=$?; SFIRED="$( fired )" +} +srow s-situ "$BIN" "$FX" --situ +{ [ "$SRC" -eq 0 ] && grep -qF situational-awareness "$TMP/r-s-situ.out" && [ "$SFIRED" -eq 0 ]; } \ + && ok "(S) --situ with a symlinked .git/config: answered and the clean filter did not run" \ + || no "(S) --situ with a symlinked .git/config: exit $SRC, firings $SFIRED" +mcpsession quality_delta "{\"path\":\"$FX\"}" > "$TMP/mcp-s.in" +srow s-mcp sh -c '"$1" --mcp < "$2"' _ "$BIN" "$TMP/mcp-s.in" +{ [ "$SRC" -eq 0 ] && grep -qF '"id":2,"result"' "$TMP/r-s-mcp.out" && [ "$SFIRED" -eq 0 ]; } \ + && ok "(S) MCP quality_delta with a symlinked .git/config: answered and the clean filter did not run" \ + || no "(S) MCP quality_delta with a symlinked .git/config: exit $SRC, firings $SFIRED" + +# ── (F) the PATH shim: argv + GIT_* environment of every child ────────────────────────────────────────── +mkdir -p "$TMP/shim" +cat > "$TMP/shim/git" <> "\${GITCHILD_LOG:?}" +exec "$REALGIT" "\$@" +EOF +chmod +x "$TMP/shim/git" + +# A plain repository with history, a rename and a working-tree edit, so log/diff/blame paths run too. +RICH="$TMP/rich" +( mkdir -p "$RICH" && cd "$RICH" && "$REALGIT" init -q \ + && printf 'int a(void){return 1;}\nint b(void){return a();}\n' > a.c && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm one \ + && printf 'int c(void){return 3;}\n' > c.c && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm two \ + && "$REALGIT" mv c.c d.c && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm three \ + && printf '### T1\nIn progress.\n\xe2\x8f\xb3 pending\n' > wave.md && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm four \ + && printf 'int e(void){return b();}\n' >> a.c ) >/dev/null 2>&1 || no "(F) could not build the plain history repository" +PLAINVERBS="$VERBS +plan-lint|--plan-lint=$RICH/wave.md|0|plan-lint" + +# check_log FILE PINS: prints one line per violation, "F1|F2|F3|F4 ". PINS=1 enforces F2 on every +# child after the filter-driver probe; PINS=2 on every child but a config read (the control for a binary with no probe). +subcmd_of(){ # argv fields (\x1f-separated, leading empty field) → the subcommand: skip global options and their values + printf '%s' "$1" | awk -F '\037' '{ for( i = 2; i <= NF; ++i ) { if( $i == "-c" || $i == "-C" ) { ++i; continue } if( $i ~ /^-/ ) { continue } print $i; exit } }' +} +check_log(){ + clog="$1"; cpins="$2"; seenprobe=0 + while IFS= read -r line; do + case "$line" in + ENV*) + for v in GIT_EXTERNAL_DIFF GIT_ATTR_SOURCE GIT_CONFIG_PARAMETERS; do + case "$line" in *"$v="*) printf 'F4 env %s\n' "$v" ;; esac + done + continue ;; + esac + argv="${line#ARGV}" + sub="$( subcmd_of "$argv" )" + flat="$( printf '%s' "$argv" | tr '\037' ' ' )" + case "$flat" in *" config --get core.fsmonitor") continue ;; esac + case "$flat" in + " --no-optional-locks -c core.fsmonitor=false -c log.showSignature=false"*) ;; + *) printf 'F1 %s %s\n' "$sub" "$flat" ;; + esac + if [ "$sub" = config ]; then + case "$flat" in *"--show-scope"*|*"filter\\."*) seenprobe=1 ;; esac + continue + fi + if { [ "$cpins" = 1 ] && [ "$seenprobe" = 1 ]; } || [ "$cpins" = 2 ]; then + for k in clean= smudge= process= required=false; do + case "$flat" in *" -c filter.ev.$k"*) ;; *) printf 'F2 %s %s\n' "$sub" "$flat"; break ;; esac + done + fi + case "$sub" in + diff|log|show|blame) + case "$flat" in *" --no-ext-diff"*) ;; *) printf 'F3 %s %s\n' "$sub" "$flat" ;; esac + case "$flat" in *" --no-textconv"*) ;; *) printf 'F3 %s %s\n' "$sub" "$flat" ;; esac ;; + esac + case "$sub" in + diff|status) + case "$flat" in *" --ignore-submodules=dirty"*) ;; *) printf 'F3 %s %s\n' "$sub" "$flat" ;; esac ;; + esac + done < "$clog" + return 0 +} + +# shim_sweep BINARY LABEL REPO PINS VERBLIST: one log per verb; prints the table rows; accumulates violations in $TMP/viol-LABEL. +shim_sweep(){ + sbin="$1"; slabel="$2"; srepo="$3"; spins="$4"; slist="$5" + : > "$TMP/viol-$slabel" + while IFS='|' read -r vtag vflag vrcs vmark; do + [ "$srepo" = "$FX" ] && mkfx + GITCHILD_LOG="$TMP/shim-$slabel-$vtag.log"; : > "$GITCHILD_LOG"; export GITCHILD_LOG + # shellcheck disable=SC2086 + PATH="$TMP/shim:$PATH" "$sbin" "$srepo" $vflag >/dev/null 2>&1 + unset GITCHILD_LOG + n="$( grep -c '^ARGV' "$TMP/shim-$slabel-$vtag.log" )" + subs="$( grep '^ARGV' "$TMP/shim-$slabel-$vtag.log" | while IFS= read -r l; do subcmd_of "${l#ARGV}"; done | sort | uniq -c | awk '{ printf "%s%s×%s", sep, $2, $1; sep=" " }' )" + check_log "$TMP/shim-$slabel-$vtag.log" "$spins" > "$TMP/viol-$slabel-$vtag" + cat "$TMP/viol-$slabel-$vtag" >> "$TMP/viol-$slabel" + f1="$( grep -c '^F1' "$TMP/viol-$slabel-$vtag" )"; f2="$( grep -c '^F2' "$TMP/viol-$slabel-$vtag" )" + f3="$( grep -c '^F3' "$TMP/viol-$slabel-$vtag" )"; f4="$( grep -c '^F4' "$TMP/viol-$slabel-$vtag" )" + printf ' | %-8s | %-14s | %3s | F1=%s F2=%s F3=%s F4=%s | %s\n' "$slabel" "$vtag" "$n" "$f1" "$f2" "$f3" "$f4" "$subs" + echo "$n" >> "$TMP/count-$slabel" + done <<< "$slist" + return 0 +} + +echo " ---- (F) shim table: repo | verb | children | violations | subcommands" +rm -f "$TMP"/count-* +shim_sweep "$BIN" fixture "$FX" 1 "$VERBS" +shim_sweep "$BIN" plain "$RICH" 0 "$PLAINVERBS" +for lbl in fixture plain; do + total="$( awk '{ s += $1 } END { print s + 0 }' "$TMP/count-$lbl" )" + nv="$( wc -l < "$TMP/viol-$lbl" | tr -d ' ' )" + [ "$total" -ge 10 ] \ + && ok "(F) presence ($lbl): the shim logged $total git children across the verb sweep" \ + || no "(F) presence ($lbl): only $total git children logged — the arm cannot conclude" + [ "$nv" -eq 0 ] \ + && ok "(F) every git child ($lbl) carried F1-F3 and saw no injected F4 variable" \ + || no "(F) $nv violation(s) ($lbl): $( head -3 "$TMP/viol-$lbl" | tr '\n' ';' )" +done +for s in diff log blame status; do + grep -h '^ARGV' "$TMP"/shim-plain-*.log "$TMP"/shim-fixture-*.log 2>/dev/null | while IFS= read -r l; do subcmd_of "${l#ARGV}"; done | grep -qx "$s" \ + && ok "(F) coverage: the sweep exercised at least one '$s' child" \ + || no "(F) coverage: no '$s' child was exercised — F3 is unproven for it" +done + +# Controls: the checker flags each missing option. +CTRL="$TMP/control.log" +printf 'ARGV\037--no-optional-locks\037-c\037core.fsmonitor=false\037-C\037/x\037status\037--porcelain\n' > "$CTRL" # F1 + F3 (status) +printf 'ARGV\037--no-optional-locks\037-c\037core.fsmonitor=false\037-c\037log.showSignature=false\037config\037--show-scope\n' >> "$CTRL" # the probe +printf 'ARGV\037--no-optional-locks\037-c\037core.fsmonitor=false\037-c\037log.showSignature=false\037-C\037/x\037log\037-1\n' >> "$CTRL" # F2 + F3 (log) +printf 'ENV\037GIT_EXTERNAL_DIFF=/x\037\n' >> "$CTRL" # F4 +check_log "$CTRL" 1 > "$TMP/control.viol" +for want in F1 F2 F3 F4; do + grep -q "^$want" "$TMP/control.viol" \ + && ok "(F) control: the checker flags a synthetic child missing its $want option" \ + || no "(F) control: the checker did NOT flag $want — the arm cannot fail there" +done +if [ -n "$BASE" ]; then + rm -f "$TMP"/count-base* + shim_sweep "$BASE" base "$FX" 2 "$VERBS" > "$TMP/base-table" 2>&1 + sed 's/^/ /' "$TMP/base-table" + grep -q '^F2' "$TMP/viol-base" && grep -q '^F3' "$TMP/viol-base" \ + && ok "(F) control: the pre-fix binary's children fail F2 ($( grep -c '^F2' "$TMP/viol-base" )) and F3 ($( grep -c '^F3' "$TMP/viol-base" )) under the same checker" \ + || no "(F) control: the pre-fix binary passed the checker — it cannot tell fixed from unfixed" +fi + +# ── (G) structural ─────────────────────────────────────────────────────────────────────────────────────── +# A line spells an executed git invocation when it hands a "log|diff|show|blame …" literal to one of the git +# entry points (gitCmd and the wrappers that prepend it). Display-only strings (situ.h's printed hint, +# editplan.h's next= hint) do not go through them. +structural(){ + grep -rnE '(gitCmd|gitOneLine|gitCapture|gitListChecked|pinned \+|shSingleQuote\( (root|repoRoot) \) \+).*"[[:space:]]*(log|diff|show|blame)( |")|^[[:space:]]*\+ "[[:space:]]*(log|diff|blame)[[:space:]]' "$1/src" 2>/dev/null \ + | grep -v -- '--no-textconv' | sed "s|^$1/||" + grep -rnE '"status --porcelain' "$1/src" 2>/dev/null | grep -v -- '--ignore-submodules=dirty' | sed "s|^$1/||" +} +GOFF="$( structural "$ROOT" )" +GSEEN="$( grep -rnE -- '--no-textconv' "$ROOT/src" | wc -l | tr -d ' ' )" +[ "$GSEEN" -ge 20 ] \ + && ok "(G) presence: $GSEEN source lines carry --no-textconv" \ + || no "(G) presence: only $GSEEN source lines carry --no-textconv — the scan cannot be reading src/" +[ -z "$GOFF" ] \ + && ok "(G) every executed git log/diff/show/blame spelling carries --no-textconv; every status --porcelain carries --ignore-submodules=dirty" \ + || no "(G) unhardened spelling(s): $( printf '%s' "$GOFF" | head -3 | tr '\n' ';' )" +mkdir -p "$TMP/gmut" && cp -R "$ROOT/src" "$TMP/gmut/src" +printf '\ninline std::string gitGateMutationD( const std::string& root ) { return gitOneLine( root, "log -1 --format=%%H" ); }\n' >> "$TMP/gmut/src/quality.h" +printf '\ninline std::string gitGateMutationE( const std::string& root ) { return gitOneLine( root, "status --porcelain" ); }\n' >> "$TMP/gmut/src/quality.h" +GMUT="$( structural "$TMP/gmut" )" +for gm in gitGateMutationD gitGateMutationE; do + printf '%s\n' "$GMUT" | grep -q "$gm" \ + && ok "(G) mutation control: the same scan finds $gm" \ + || no "(G) mutation control: the scan did not find $gm — the arm cannot fail there" +done + +[ "$fail" -eq 0 ] && echo "gitchildoptscheck: PASS" || echo "gitchildoptscheck: FAIL" +exit "$fail" diff --git a/test/githardencheck.sh b/test/githardencheck.sh index 8cb360843..a354eee63 100755 --- a/test/githardencheck.sh +++ b/test/githardencheck.sh @@ -53,6 +53,16 @@ # regular-file-only open, so the run finishes, and nothing is disclosed from a file it could not read. A # blocking `cat` of the same FIFO under the same timeout harness is the contrast that shows the harness # detects a stall. +# (O) the startup scan finds the governing config from the filesystem alone, a superset of git's discovery: an +# invalid .git dir skipped for the hooked parent, a commondir planted in an ordinary .git, a bare root, a +# symlinked root (git walks the physical path), a symlinked .git/config (git reads through it), a NUL byte +# inside a .git gitdir file or a commondir file (git cuts the path at the NUL, so the scan defers to git), and a +# .git pointer file behind a symlink (the scan defers to git; the shim sees that child). Each shape first proves +# plain git reads the hook value there. +# (Q) no stale answer: a repo whose config gains the hook (directly, then only via include.path) between two +# calls is disclosed on the next call. +# (P) the cost, counted through a PATH shim: a plain repo's run starts no startup-scan git child; with GIT_DIR +# set the same shim sees the rev-parse child (control). # (G) determinism: two `--situ` stdouts are byte-identical. # # Usage: RIPWIRE_BIN=build/ripwire bash test/githardencheck.sh @@ -423,6 +433,135 @@ else no "(N) the FIFO at .git/config.worktree was replaced or removed" fi +# ── (O) the startup scan finds the governing config without asking git (githarden.h discoveryConfigSuperset) ── +# The scan unions every config file git's discovery COULD pick — every ancestor's .git dir or gitdir: file, every +# ancestor that is itself a gitdir, each one's commondir — and runs `git config --get` only when one of them could +# carry the key. Each shape below first proves plain git governs the root with the hook value (presence), then +# requires the disclosure and zero firings. (O1) and (O2) also held when git resolved the dirs; (O3)-(O5) were +# missed before (the pre-check walked the LOGICAL path looking only for a `.git` entry, so a bare root and a +# symlinked root never reached git; a linked config was read no-follow and skipped) — red on the base binary. +# discloses TAG ROOT: one default-map run; rc 0, git_harden=fsmonitor-hook on stderr, the hook never ran. +discloses(){ + otag="$1"; oroot="$2" + rm -f "$TMP/MARK" + "$BIN" "$oroot" > "$TMP/o-$otag.out" 2> "$TMP/o-$otag.err" + orc=$? + if [ "$orc" -eq 0 ] && grep -q 'git_harden=fsmonitor-hook' "$TMP/o-$otag.err" && [ "$( fired )" -eq 0 ]; then + ok "(O) $otag: disclosed (git_harden=fsmonitor-hook), hook unfired, rc 0" + else + no "(O) $otag: rc=$orc disclosure=$( grep -c git_harden= "$TMP/o-$otag.err" ) fired=$( fired ): $( head -c 200 "$TMP/o-$otag.err" )" + fi + return 0 +} +# governs TAG ROOT: plain git reads the hook value at ROOT — the premise each (O) shape depends on. +governs(){ + gtag="$1"; groot="$2" + [ "$( "$REALGIT" -C "$groot" config --get core.fsmonitor 2>/dev/null )" = "$TMP/hook.sh" ] \ + && ok "(O) $gtag presence: plain git at the root reads core.fsmonitor = the hook" \ + || no "(O) $gtag presence: plain git at the root does not read the hook value — the shape cannot conclude" + return 0 +} +newrepo(){ ( mkdir -p "$1" && cd "$1" && "$REALGIT" init -q && printf 'int f;\n' > f.c && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm init ) >/dev/null 2>&1; } + +# (O1) an invalid .git directory in the root: git skips it and the walk goes on to the hooked parent repo. +FO1="$TMP/o1"; cp -R "$FX" "$FO1"; mkdir -p "$FO1/sub/.git" +governs "O1-invalid-dotgit" "$FO1/sub" +discloses "O1-invalid-dotgit" "$FO1/sub" +# (O2) a commondir planted in an ordinary .git: git takes the shared config from the dir it names. +newrepo "$TMP/o2"; newrepo "$TMP/o2common"; "$REALGIT" -C "$TMP/o2common" config core.fsmonitor "$TMP/hook.sh" +printf '../../o2common/.git\n' > "$TMP/o2/.git/commondir" +grep -qiE 'fsmonitor|include' "$TMP/o2/.git/config" \ + && no "(O) O2-commondir fixture: o2/.git/config itself spells the key — the arm would not test the commondir" \ + || ok "(O) O2-commondir fixture: o2/.git/config spells neither fsmonitor nor include" +governs "O2-commondir" "$TMP/o2" +discloses "O2-commondir" "$TMP/o2" +# (O3) a bare repository as the root: the root is the gitdir itself. +( "$REALGIT" init -q --bare "$TMP/o3.git" && "$REALGIT" -C "$TMP/o3.git" config core.fsmonitor "$TMP/hook.sh" ) >/dev/null 2>&1 +governs "O3-bare" "$TMP/o3.git" +discloses "O3-bare" "$TMP/o3.git" +# (O4) a symlink to a subdirectory of the hooked checkout: git walks the PHYSICAL path. +ln -s "$FX/sub" "$TMP/o4link" +governs "O4-symlinked-root" "$TMP/o4link" +discloses "O4-symlinked-root" "$TMP/o4link" +# (O5) .git/config is a symlink to the hooked file: git reads through it; the no-follow scan must not call it clean. +newrepo "$TMP/o5"; cp "$TMP/o5/.git/config" "$TMP/o5.cfg" +printf '[core]\n\tfsmonitor = %s\n' "$TMP/hook.sh" >> "$TMP/o5.cfg" +rm -f "$TMP/o5/.git/config"; ln -s "$TMP/o5.cfg" "$TMP/o5/.git/config" +if [ -L "$TMP/o5/.git/config" ]; then ok "(O) O5-symlinked-config fixture: .git/config is a symlink"; else no "(O) O5-symlinked-config fixture: .git/config is not a symlink"; fi +governs "O5-symlinked-config" "$TMP/o5" +discloses "O5-symlinked-config" "$TMP/o5" +# (O6) a .git gitdir: file with a NUL inside the path: git reads it as a C string, cut at the NUL, so it governs with +# the hooked gitdir before the NUL; a scan that keeps the NUL in the path opens nothing and must not call it clean. +newrepo "$TMP/o6src"; "$REALGIT" -C "$TMP/o6src" config core.fsmonitor "$TMP/hook.sh" +mkdir -p "$TMP/o6" && printf 'int f;\n' > "$TMP/o6/f.c" && printf 'gitdir: %s/o6src/.git\000junk\n' "$TMP" > "$TMP/o6/.git" +if od -An -c "$TMP/o6/.git" | grep -q '\\0'; then ok "(O) O6-nul-gitdir fixture: o6/.git holds a NUL byte"; else no "(O) O6-nul-gitdir fixture: o6/.git holds no NUL byte"; fi +governs "O6-nul-gitdir" "$TMP/o6" +discloses "O6-nul-gitdir" "$TMP/o6" +# (O7) a commondir with a NUL inside the path: the same cut, for the shared config. +newrepo "$TMP/o7"; newrepo "$TMP/o7common"; "$REALGIT" -C "$TMP/o7common" config core.fsmonitor "$TMP/hook.sh" +printf '%s/o7common/.git\000junk\n' "$TMP" > "$TMP/o7/.git/commondir" +if od -An -c "$TMP/o7/.git/commondir" | grep -q '\\0'; then ok "(O) O7-nul-commondir fixture: commondir holds a NUL byte"; else no "(O) O7-nul-commondir fixture: commondir holds no NUL byte"; fi +governs "O7-nul-commondir" "$TMP/o7" +discloses "O7-nul-commondir" "$TMP/o7" +# (O8) the root's .git is a symlink to a gitdir: file: git reads through it; the scan defers to git, so the disclosure +# comes from the git-resolved fallback — the shim must see its rev-parse child. +newrepo "$TMP/o8src"; "$REALGIT" -C "$TMP/o8src" config core.fsmonitor "$TMP/hook.sh" +printf 'gitdir: %s/o8src/.git\n' "$TMP" > "$TMP/o8.ptr" +mkdir -p "$TMP/o8" && printf 'int f;\n' > "$TMP/o8/f.c" && ln -s "$TMP/o8.ptr" "$TMP/o8/.git" +if [ -L "$TMP/o8/.git" ]; then ok "(O) O8-symlinked-pointer fixture: o8/.git is a symlink"; else no "(O) O8-symlinked-pointer fixture: o8/.git is not a symlink"; fi +governs "O8-symlinked-pointer" "$TMP/o8" +discloses "O8-symlinked-pointer" "$TMP/o8" +mkdir -p "$TMP/shimo8" +printf '#!/bin/bash\nprintf "%%s\\n" "$*" >> "%s/shimo8.log"\nexec "%s" "$@"\n' "$TMP" "$REALGIT" > "$TMP/shimo8/git" +chmod +x "$TMP/shimo8/git" +rm -f "$TMP/shimo8.log"; PATH="$TMP/shimo8:$PATH" "$BIN" "$TMP/o8" >/dev/null 2>&1; o8rc=$? +O8RES="$( grep -c 'rev-parse --absolute-git-dir' "$TMP/shimo8.log" 2>/dev/null )"; O8RES="${O8RES:-0}" +[ "$o8rc" -eq 0 ] && [ "$O8RES" -ge 1 ] \ + && ok "(O) O8-symlinked-pointer: the scan deferred to git (rev-parse --absolute-git-dir x$O8RES), rc 0" \ + || no "(O) O8-symlinked-pointer: rc=$o8rc, rev-parse --absolute-git-dir x$O8RES — the git fallback was not taken" + +# ── (Q) no stale answer: a config that gains the key between two calls is hardened on the second ────────────── +# Nothing about the scan is kept across processes; this pins it. The first (warm-cache) call sees a plain repo; then +# the config gains the hook, directly and (on a third call) only through an include. +newrepo "$TMP/q" +"$BIN" "$TMP/q" >/dev/null 2> "$TMP/q1.err"; rc1=$? +[ "$rc1" -eq 0 ] && ! grep -q 'git_harden=' "$TMP/q1.err" \ + && ok "(Q) call 1 on the plain repo: rc 0, no disclosure (nothing to neutralise)" \ + || no "(Q) call 1 on the plain repo: rc=$rc1, disclosure=$( grep -c git_harden= "$TMP/q1.err" )" +"$REALGIT" -C "$TMP/q" config core.fsmonitor "$TMP/hook.sh" +governs "Q-gains-key" "$TMP/q" +discloses "Q-gains-key (call 2, same repo, warm cache)" "$TMP/q" +"$REALGIT" -C "$TMP/q" config --unset core.fsmonitor; "$REALGIT" -C "$TMP/q" config include.path "$TMP/hidden.inc" +governs "Q-gains-include" "$TMP/q" +discloses "Q-gains-include (call 3, the key only behind include.path)" "$TMP/q" + +# ── (P) the cost: no git child for a root whose config cannot carry the key, counted through a PATH shim ────── +# Timing-free: the startup scan used to run `git rev-parse --absolute-git-dir --git-common-dir` once per root on +# EVERY call. Control: with GIT_DIR set the filesystem cannot speak for git, so the same shim must see that child. +newrepo "$TMP/p" +mkdir -p "$TMP/shimp" +cat > "$TMP/shimp/git" <> "$TMP/shimp.log" +exec "$REALGIT" "\$@" +EOF +chmod +x "$TMP/shimp/git" +rm -f "$TMP/shimp.log"; PATH="$TMP/shimp:$PATH" "$BIN" "$TMP/p" >/dev/null 2>&1; prc=$? +PTOTAL="$( wc -l < "$TMP/shimp.log" 2>/dev/null | tr -d ' ' )"; PTOTAL="${PTOTAL:-0}" +PRES="$( grep -c 'rev-parse --absolute-git-dir' "$TMP/shimp.log" 2>/dev/null )"; PRES="${PRES:-0}" +PCFG="$( grep -c 'config --get core.fsmonitor' "$TMP/shimp.log" 2>/dev/null )"; PCFG="${PCFG:-0}" +[ "$prc" -eq 0 ] && [ "$PTOTAL" -ge 1 ] \ + && ok "(P) presence: rc 0 and the shim logged $PTOTAL git child(ren) for the default map — it sees ripwire's git" \ + || no "(P) presence: rc=$prc, $PTOTAL git children logged — the arm cannot conclude" +[ "$PRES" -eq 0 ] && [ "$PCFG" -eq 0 ] \ + && ok "(P) the startup trust scan started no git child (rev-parse --absolute-git-dir: 0, config --get core.fsmonitor: 0)" \ + || no "(P) the startup trust scan still spawns git on a plain repo: rev-parse --absolute-git-dir x$PRES, config --get x$PCFG" +rm -f "$TMP/shimp.log"; PATH="$TMP/shimp:$PATH" GIT_DIR="$TMP/p/.git" GIT_WORK_TREE="$TMP/p" "$BIN" "$TMP/p" >/dev/null 2>&1 +PENV="$( grep -c 'rev-parse --absolute-git-dir' "$TMP/shimp.log" 2>/dev/null )"; PENV="${PENV:-0}" +[ "$PENV" -ge 1 ] \ + && ok "(P) control: with GIT_DIR set git is asked (rev-parse --absolute-git-dir x$PENV) — the count above can see that child" \ + || no "(P) control: with GIT_DIR set no rev-parse --absolute-git-dir was logged — the shim cannot see the startup child" + # ── (G) determinism ───────────────────────────────────────────────────────────────────────────────────── "$BIN" "$FX" --situ > "$TMP/situ2.out" 2>/dev/null cmp -s "$TMP/situ.out" "$TMP/situ2.out" \ diff --git a/test/impactimportcheck.sh b/test/impactimportcheck.sh index 239e22e92..c76585365 100755 --- a/test/impactimportcheck.sh +++ b/test/impactimportcheck.sh @@ -164,7 +164,8 @@ SHOWN="$( attr shown "$OUT_W" )" && ok "--impact=Widget: shown=1 and exactly 1 row — import rows are outside the paged listing" \ || no "--impact=Widget: rows=$S_ROWS vs shown=$SHOWN (expected 1 and 1)" # 0.6.5: the listing's first row states its hop depth (d="1": build calls Widget directly) — test/impactdepthcheck.sh. -printf '%s' "$OUT_W" | grep -qE '' \ +# attrib-multidef: Widget names several definitions, so the row also says which one it depends on (of_def=, defattrib.h) +printf '%s' "$OUT_W" | grep -qE '' \ && ok "--impact=Widget: user.js appears as the SYMBOL build in the call tier" \ || no "--impact=Widget: the call-reach row for build (lib/user.js:7) is gone" @@ -437,10 +438,11 @@ for LG in py ts go; do no "multidef $LG: the single-definition corpus has no via=name symbol row — its near-miss arm is inert" fi O_RF="$( rfrow "$M_OUT" 'o_00\.[a-z]+' )"; X_RF="$( rfrow "$M_OUT" 'mixed\.[a-z]+' )" - printf '%s' "$O_RF" | grep -qE 'syms="1" d="1" via="name"/>' \ + # attrib-multidef: the row's of_def= (which definitions its symbols depend on) rides after the fix27 hedge + printf '%s' "$O_RF" | grep -qE 'syms="1" d="1" via="name"( of_def="[^"]*")?/>' \ && ok "multidef $LG: an all-by-name file keeps the hedge: $O_RF" \ || no "multidef $LG: the o_00 rf row lost the symbol rows' via=\"name\": '$O_RF'" - { printf '%s' "$X_RF" | grep -qE 'syms="2" d="1" by_name="1"/>' && ! printf '%s' "$X_RF" | grep -q 'via='; } \ + { printf '%s' "$X_RF" | grep -qE 'syms="2" d="1" by_name="1"( of_def="[^"]*")?/>' && ! printf '%s' "$X_RF" | grep -q 'via='; } \ && ok "multidef $LG: a mixed file counts its hedged symbols: $X_RF" \ || no "multidef $LG: the mixed rf row: '$X_RF' (want syms=2 by_name=1, no via=)" if [ $LG != ts ]; then @@ -452,7 +454,7 @@ for LG in py ts go; do printf '%s' "$M_OUT" | grep -qF 'rf via="name": of= matched several definitions' \ && ok "multidef $LG: the rf hedge sentence rides with the hedged rows" \ || no "multidef $LG: legend lacks the rf via=/by_name= sentence" - perl -e 'alarm 30; exec @ARGV' "$BIN" "$MD" --impact=$SY --no-cache --json 2>/dev/null | grep -qE '"syms":1,"d":1,"via":"name"\}' \ + perl -e 'alarm 30; exec @ARGV' "$BIN" "$MD" --impact=$SY --no-cache --json 2>/dev/null | grep -qE '"syms":1,"d":1,"via":"name"(,"of_def":\[[^]]*\])?\}' \ && ok "multidef $LG: --json carries the same hedge" \ || no "multidef $LG: --json rollup rows lack \"via\":\"name\"" for NM in single qualified; do @@ -472,7 +474,7 @@ for LG in py ts go; do [ $LG = py ] && { MCP_M="$( 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":"arrange"}}}\n' "$MD" \ | perl -e 'alarm 60; exec @ARGV' "$BIN" --mcp 2>/dev/null | tail -1 )" - printf '%s' "$MCP_M" | grep -qF 'syms=\"1\" d=\"1\" via=\"name\"/>' && printf '%s' "$MCP_M" | grep -qF 'by_name=\"1\"' \ + printf '%s' "$MCP_M" | grep -qE 'syms=\\"1\\" d=\\"1\\" via=\\"name\\"( of_def=\\"[^\\]*\\")?/>' && printf '%s' "$MCP_M" | grep -qF 'by_name=\"1\"' \ && ok "multidef py: the MCP impact twin keeps the same hedge" \ || no "multidef py: MCP impact rollup rows lack via=/by_name=" } diff --git a/test/impactpartitioncheck.sh b/test/impactpartitioncheck.sh index 5e2eb59f4..9f20d145d 100755 --- a/test/impactpartitioncheck.sh +++ b/test/impactpartitioncheck.sh @@ -155,7 +155,9 @@ for name in seed_names: # row COUNT this invariance reads is unchanged by it (test/impactdepthcheck.sh gates the depths themselves). # 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) + # attrib-multidef: a row of a multi-definition selector carries of_def= (which definition it is a dependant of) last — it + # attributes the row, never adds or removes one, so the count this invariance reads takes it like via= above. + rows = re.findall(r'', doc) reaches = int(attrs.get("reaches", "-1")) if len(rows) != reaches: row_count_ok = False diff --git a/test/jsonparitycheck.sh b/test/jsonparitycheck.sh index 3b6abcde3..aa1a23ff4 100755 --- a/test/jsonparitycheck.sh +++ b/test/jsonparitycheck.sh @@ -362,7 +362,8 @@ XML_ROUTES="$( "$BIN" "$ROOT/test/routeedgefix" --for="$RQ" --no-cache 2>/dev/nu # that sweep is what still holds the line. Three clauses, per rung: # • budget_tokens is on the JSON root whenever it is on the XML root, and names the SAME N; # • est_tokens is served in both (the price a budget shapes against is never withheld); -# • est_tokens > N implies over_ceiling in that dialect's own spelling — ONE unit, both dialects. +# • est_tokens > N implies over_ceiling in that dialect's own spelling — ONE unit, both dialects. (forbudget-soft: N is a +# soft target now; the clause is "neither dialect labels it, both continue the cut" — still one rule, both dialects.) # The XML side is asserted here too rather than assumed: this is a PARITY arm, and a parity that only ever # reads one side cannot tell a fixed dialect from a regressed reference. echo @@ -389,12 +390,18 @@ for BTB in 880 900 1500; do else no "R1: budget=$BTB — est_tokens missing (xml='$XE' json='$JE'); a budget that shapes a bundle must price it" fi - { [ -n "$XE" ] && { [ "$XE" -le "$BTB" ] || [ "$XL" -eq 1 ]; }; } 2>/dev/null \ - && ok "R1: budget=$BTB xml — est_tokens=$XE is inside the ceiling or labelled (over_ceiling=$XL)" \ - || no "R1: budget=$BTB xml — est_tokens=$XE exceeds the stated ceiling with NO over_ceiling" - { [ -n "$JE" ] && { [ "$JE" -le "$BTB" ] || [ "$JL" -eq 1 ]; }; } 2>/dev/null \ - && ok "R1: budget=$BTB json — est_tokens=$JE is inside the ceiling or labelled (over_ceiling=$JL)" \ - || no "R1: budget=$BTB json — est_tokens=$JE exceeds the stated ceiling with NO over_ceiling — the XML twin's unit did not reach this dialect" + # forbudget-soft (owner decisions 2026-10-14; rule 4): budget_tokens= on --for is a SOFT target — est_tokens may exceed it + # by contract and NEITHER dialect labels it over_ceiling (one rule, both dialects: the parity this arm exists for). The + # cut the target makes is continued in both: / "sigs_next". + XN=0; JN=0 + case "$BX" in *'/dev/null \ + && ok "R1: budget=$BTB xml — soft target: no over_ceiling (est_tokens=$XE), the cut continued ()" \ + || no "R1: budget=$BTB xml — over_ceiling=$XL on a soft target, or the cut carries no next= (est_tokens=$XE next=$XN)" + { [ -n "$JE" ] && [ "$JL" -eq 0 ] && [ "$JN" -eq 1 ]; } 2>/dev/null \ + && ok "R1: budget=$BTB json — soft target: no over_ceiling (est_tokens=$JE), the cut continued (\"sigs_next\")" \ + || no "R1: budget=$BTB json — over_ceiling=$JL on a soft target, or no \"sigs_next\" (est_tokens=$JE) — the twins disagree" done echo diff --git a/test/langidentity/ratchet.tsv b/test/langidentity/ratchet.tsv index 2deac642d..6e7c895d8 100644 --- a/test/langidentity/ratchet.tsv +++ b/test/langidentity/ratchet.tsv @@ -3,7 +3,11 @@ # per-language files below; counted from `ripwire src --regex='Lang::[A-Z][A-Za-z]+'`, one count per distinct line. # It may fall (lower the number in the same commit that removes the predicate) and may never rise. # perlang: a file that exists for ONE language, whose predicates are the point of the file and do not count. -ceiling 352 +# ONE-TIME NAMED EXCEPTION (owner waiver 2026-10-16): the 10 shared-file Lang:: predicates came from lanes signed before +# #399's ratchet (grep-carrier +1, ruby-pair +4, qd-memory-degrade +1, #380 +1, attrib-multidef/declined-sites +3); the +# ceiling moves 352 -> 362 for train 28 only. Train 29 brings it back to <= 352. This waiver is not a precedent: the rule +# above (may fall, never rise) holds for every other change. +ceiling 362 perlang ingest_elixir.h perlang elixir_resolve.h perlang ingest_clojure.h diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index 53666b4c2..f2c6c49e5 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -735,7 +735,17 @@ PY if [ -s "$TMP/noteerr" ]; then no "(E) the dropped-note sweep itself failed: $( head -c 300 "$TMP/noteerr" )"; fi cat "$TMP/noteout" -if [ ! -s "$TMP/notegaps" ] && ! grep -q 'names_checked=[1-9]' "$TMP/noteout" 2>/dev/null; then +# forbudget-soft (owner decisions 2026-10-14; rule 4): an explicit --token-budget on --for is a SOFT target, so no ceiling +# ladder runs and rung zero's note can no longer ride: at the tight probe budget the legend must be WHOLE in every dialect +# (HL) — the note ABSENT, which is exactly what the sweep reports as "ABSENT". Every first-screen attribute of that answer is +# still defined by its own legend (arm (A) runs the same roster). A note that rides again is the regression this now catches. +ABSENT_ONLY=0 +if [ -s "$TMP/notegaps" ] && ! grep -qv 'the dropped-legend note is ABSENT at --token-budget=' "$TMP/notegaps"; then ABSENT_ONLY=1; fi +if [ "$ABSENT_ONLY" = 1 ] && [ "$( wc -l < "$TMP/notegaps" | tr -d ' ' )" -ge 3 ]; then + ok "(E) no dropped-legend note in any dialect at the tight probe budget — a soft target never sheds a legend clause (HL; full, compact, default)" +elif [ ! -s "$TMP/notegaps" ] && grep -q 'names_checked=[1-9]' "$TMP/noteout" 2>/dev/null; then + no "(E) a dropped-legend note rides a soft --for target again — the header was shed (HL)" +elif [ ! -s "$TMP/notegaps" ] && ! grep -q 'names_checked=[1-9]' "$TMP/noteout" 2>/dev/null; then no "(E) the dropped-note sweep checked ZERO attribute names — a sweep with an empty population passes for the wrong reason" elif [ -s "$TMP/notegaps" ]; then no "(E) $( wc -l < "$TMP/notegaps" | tr -d ' ' ) dropped-legend note problem(s) — the note names a definition its own dialect does not have, or the probe has drifted off the rung:" diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index 228a2462f..397defd84 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -160,7 +160,9 @@ then if has ''; then ok "--lego=DSL lists TaskLib (include Rake::DSL) at rb/tasklib.rb:2"; else no "--lego=DSL: no TaskLib row: $( iface )"; fi if has ''; then ok "implementors=\"4\": the three classes and the main object the top-level self.extend mixes into"; else no "DSL count: $( iface )"; fi if iface | grep -q 'implementors_floor'; then no "the bound include still reads as an unbound floor: $( iface )"; else ok "no implementors_floor once the include binds"; fi fi if lego Helpers diff --git a/test/lib/softbudget.py b/test/lib/softbudget.py new file mode 100644 index 000000000..b8fb6efe8 --- /dev/null +++ b/test/lib/softbudget.py @@ -0,0 +1,98 @@ +# softbudget.py — the soft --token-budget readers' shared helpers (lane forbudget-soft; CHECKLIST 32: one copy, not five). +# +# Five gates read a soft-target answer's ST invariant (fornotesbudgetcheck, forsoftbudgetcheck, compactlegendcheck, +# estchargecheck, forsigspancheck). Each keeps its own row reader (their dialects and row charges differ); what they share +# lives here. The gates export RIPWIRE_TESTLIB="$ROOT/test/lib" and import this file (sys.path insert), as crashsweepcheck +# does shardmatch.py. No state; pure functions of the answer text. +import json, re + + +def tail_size( t ): + """Bytes of the answer's tail: XML, the whole … element; JSON, the compact "tail" object. + + FIX1 R1: the refill reserves the tail at its HEAD-scoped size (the tail of the no-budget answer) when that is what binds, + so maximality counts the tail at no less than the reference tail (an upper bound on the reserve: the check stays a bound). + """ + if t.lstrip().startswith( "{" ): + try: return len( json.dumps( json.loads( t ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + m = re.search( r"", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 + + +# ── FIX2 C1 (review rv-forbudget-soft-fix1; rule 4, CHECKLIST 1): the UPPER-bound twin of the relaxed maximality ────────────── +# FIX1 floored maximality's tail at the reference (no-budget) answer's tail: that only loosened the LOWER bound. Its twin bounds +# the rows from above: a cut served past the rank floor stops where T, less the other sections and the tail's reserve, runs out. +# A binary that ignores the tail's reserve (reserve = 0) serves rows into the bytes the tail was promised: RED here, while the +# lower bound and fits (rows <= T) both stay green on it. What bounds the rows (src/verbs_for.h, the soft refill and the tail's +# share; src/serialize.h forSigsBudgets / forSoftRowDropFill): +# XML rows = max(the cut, the refill). The refill charges every served section and the tail's reserve, min(its share, the +# head-scoped tail): rows' charge <= T - sections - reserve + 13 (the "" frame). At the cut, the tail's +# share is T less the cut's PRINTED rows and the sections, and the tail fills that share (its last row may cross it). So +# in both cases the PRINTED rows (the bytes inside , without the zero cx=/ccx=/in= the ladder also charges) are +# <= T - sections - min(served tail, ref tail) + the served tail's last row + 13. "sections" is the reader's U less the +# tail: U here carries FIX1's floor (= sections + max(served, ref tail)), so sections = U - max(served tail, ref tail). +# JSON rows = max(the cut, the refill). The refill reserves only the tail; the cut is T less the stanzas the sigs budget +# charges (header, surface counts, notes: the keys below that are neither the rows, the tail, nor the cut's own +# disclosure). So rows <= T - min(those stanzas, min(served tail, ref tail)) + the tail's last row + 13. +JSON_UNCHARGED_KEYS = frozenset( ( "sigs", "tail", "sigs_shown", "sigs_total", "sigs_next", "sigs_next_offset", "sigs_past_target", + "capped", "dropped_positive", "budget_tokens", "budget_bytes", "est_tokens", "over_ceiling" ) ) +SIGS_FRAME_BYTES = 13 # "" + "": forSoftRowDropFill adds it to the rows' budget (serialize.h kSigsWrapperBytes) + + +def _jlen( v ): + return len( json.dumps( v, separators=( ",", ":" ), ensure_ascii=False ).encode() ) + + +def rows_printed( t ): + """The rows as printed: XML, the bytes inside ; JSON, each "sigs" entry's compact bytes plus its comma. None when absent.""" + if t.lstrip().startswith( "{" ): + try: d = json.loads( t ) + except ValueError: return None + return sum( _jlen( e ) + 1 for e in d[ "sigs" ] ) if isinstance( d.get( "sigs" ), list ) else None + m = re.search( r"]*>(.*?)", t, re.S ) + return len( m.group( 1 ).encode() ) if m else None + + +def tail_last_row( t ): + """Bytes of the served tail's LAST row, the one that may cross the tail's share. XML: the last ; JSON: the last + "files" entry plus its "sym" entry (each with its comma).""" + if t.lstrip().startswith( "{" ): + try: tl = json.loads( t ).get( "tail", {} ) + except ValueError: return 0 + f, s = tl.get( "files" ) or [], tl.get( "sym" ) or [] + return ( _jlen( f[ -1 ] ) + 1 if f else 0 ) + ( _jlen( s[ -1 ] ) + 1 if s else 0 ) + m = re.search( r"", t, re.S ) + rows = re.findall( r"]*/>", m.group( 0 ) ) if m else [] + return len( rows[ -1 ].encode() ) if rows else 0 + + +def json_charged_stanzas( t ): + """Bytes of a JSON answer's stanzas the sigs budget charges: every top-level key (with its quotes, colon and comma) but the + rows, the tail and the cut's own disclosure keys (JSON_UNCHARGED_KEYS).""" + try: d = json.loads( t ) + except ValueError: return 0 + return sum( len( k.encode() ) + 4 + _jlen( v ) for k, v in d.items() if k not in JSON_UNCHARGED_KEYS ) + + +def upper_bound( answer, ref, T, k, total, u_floored ): + """The upper-bound twin. Returns (ok, detail). Applies only to a cut (k < total) served past the rank floor (k > 4). + `u_floored` is the reader's U including FIX1's tail floor (XML; ignored for JSON). A missing is not ok. + Resolution (forbudget-soft fix-1 review, F1): the XML twin carries about 1.8-2.8 rows of slack (measured on the fornotes + corpus), because at a cut the tail fills less than its share and that unused share is real headroom the answer does not + show, so this reader cannot recover it. A 1-row over-serve is therefore NOT caught in XML; JSON, which states its tail, + catches it at most rungs. A tighter XML twin needs the cut disclosed in the answer.""" + if k >= total or k <= 4: + return True, "upper=n/a" + rows = rows_printed( answer ) + if rows is None: + return False, "upper: no rows to measure (premise)" + st, rt = tail_size( answer ), tail_size( ref ) + if answer.lstrip().startswith( "{" ): + room = T - min( json_charged_stanzas( answer ), min( st, rt ) ) + else: + room = T - ( u_floored - max( st, rt ) ) - min( st, rt ) + slack = tail_last_row( answer ) + SIGS_FRAME_BYTES + ok = rows <= room + slack + return ok, "upper: rows printed %d B %s room %d B + %d B slack (served tail %d, ref tail %d)" % ( + rows, "<=" if ok else ">", room, slack, st, rt ) diff --git a/test/mcpforparitycheck.sh b/test/mcpforparitycheck.sh index f5e1d6847..03778622a 100755 --- a/test/mcpforparitycheck.sh +++ b/test/mcpforparitycheck.sh @@ -173,6 +173,9 @@ cmp -s "$TMP/ck5.xml" "$TMP/ck400.xml" \ || no "(4) CLI --for moved with --top-k — the reference this parity is measured against is not what it claims" # ── (5) budget_tokens on this twin is never a SILENT overshoot — the CLI rule, stated for the dialect ───── +# forbudget-soft (2026-10-14, rule 4): budget_tokens is now a SOFT target over the rows on both the CLI and this twin — +# est_tokens may exceed it and over_ceiling= never fires on it. The "est_tokens <= budget OR over_ceiling" clause described +# below is replaced by NOLBL + ST + NX (see the arm); the echo and served-or-declared price clauses are unchanged. # THE FAMILY RULE (METHODOLOGY §9 #2 + #4, and the CLI half of it: fornotesbudgetcheck arm 6). A caller who # states a ceiling gets one of exactly two honest answers, never a third: # • the price is SERVED — est_tokens= on the root — and then est_tokens <= budget_tokens, or the root @@ -191,6 +194,42 @@ cmp -s "$TMP/ck5.xml" "$TMP/ck400.xml" \ # removing the budget_tokens= splice (mcpverbs.h, the H9 insert) fails the echo clause on every rung. mcp_for_budget(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s","budget_tokens":%s}}}\n' \ "$CORPUS" "$1" "$2" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +# forbudget-soft (rule 4): the soft target's bytes per token (read from source: a missing constant FAILS every rung) and the +# uncut reference (a wide target serves the MCP rows uncut) the ST maximality reads the next row's bytes from. +TBPT="$( python3 -c "import re,sys; s=open(sys.argv[1]).read(); g=lambda k:float(re.search(r'inline constexpr double '+k+r' *= *([0-9.]+);',s).group(1)); print(g('kMinBytesPerToken')*g('kBudgetHeadroom'))" src/serialize.h 2>/dev/null )" +mcp_for_budget "$INERT_Q" 100000 >"$TMP/mcp_ref.xml" +cat > "$TMP/mcpst.py" <<'PY' +# mcpst.py REF N BYTES_PER_TOKEN < answer: "OK|BAD ST ..." — the rows' charge (bytes inside plus the zero cx=/ccx=/in= +# the lean spelling elides: the ladder's entryCost) fits T or is the 1..4 floor; a cut is maximal and carries next_budget_tokens > N +import re, sys +def rows( s ): + m = re.search( r']*)>(.*?)', s, re.S ) + if not m: return None + out = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', m.group( 2 ), re.S ): + a = d.group( 1 ) or d.group( 2 ); r = re.search( r'\br="(\d+)"', a ); pr = len( d.group( 0 ).encode() ) + c = pr + sum( n for k, n in ( ( "cx", 7 ), ( "ccx", 8 ), ( "in", 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) + elided += c - pr; out.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + out.sort() + return m.group( 1 ), [ c for _, c in out ], len( m.group( 2 ).encode() ) + elided +try: n, bpt = int( sys.argv[ 2 ] ), float( sys.argv[ 3 ] ) +except ValueError: print( "BAD premise: no numeric budget / bytes-per-token constant" ); sys.exit( 0 ) +ans = sys.stdin.read() +a, ref = rows( ans ), rows( open( sys.argv[ 1 ] ).read() ) +# round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the sections served before the tail +# ( rows, , , ) are reserved first and the rows past the cut fill what they leave of T — the +# maximality bound is T less those sections' bytes (U, read off the answer); the tail rides on top on this twin +after = ans.split( '', 1 )[ 1 ] if '' in ans else '' +U = len( after.split( ']*/>', ans.split( ' rows (answer or uncut reference)" ); sys.exit( 0 ) +T = int( n * bpt ); attrs, ar, charged = a; k = len( ar ); bad = [] +if not ( charged <= T or k <= 4 ): bad.append( "rows charged %d B > T=%d with %d rows (floor 4)" % ( charged, T, k ) ) +if k < len( ref[ 1 ] ): + if charged + ref[ 1 ][ k ] <= T - U - 160: bad.append( "cut at %d with the next row (%d B) fitting T less the sections (U=%d)" % ( k, ref[ 1 ][ k ], U ) ) + nb = re.search( r'\bnext_budget_tokens="([0-9]+)"', attrs ) + if not ( 'capped="1"' in attrs and nb and int( nb.group( 1 ) ) > n ): bad.append( "NX: a cut without capped + next_budget_tokens > %d" % n ) +print( "%s ST rows=%d/%d charged=%d T=%d U=%d%s" % ( "BAD" if bad else "OK", k, len( ref[ 1 ] ), charged, T, U, ( ": " + "; ".join( bad ) ) if bad else "" ) ) +PY for tb in 900 1200 1600 2000 6000; do b="$( mcp_for_budget "$INERT_Q" "$tb" )" root="$( printf '%s' "$b" | sed -n 's/^\(<[^>]*>\).*/\1/p' | head -1 )" @@ -204,11 +243,17 @@ for tb in 900 1200 1600 2000 6000; do esac est="$( printf '%s' "$root" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" if [ -n "$est" ]; then - case "$root" in *'over_ceiling="1"'*) lab=1 ;; *) lab=0 ;; esac - if [ "$est" -le "$tb" ] || [ "$lab" -eq 1 ]; then - ok "(5) budget_tokens=$tb: price SERVED and honest (est_tokens=$est, over_ceiling=$lab)" + # forbudget-soft (rule 4): MCP `for` budget_tokens is a SOFT target over the rows, so "est_tokens <= budget OR + # over_ceiling" is replaced by NOLBL (never over_ceiling= on budget_tokens; est_tokens may exceed it) + ST (the rows' + # charged bytes fit T = N x kMinBytesPerToken x kBudgetHeadroom, or are the rank 1..4 floor; a cut is maximal against + # the uncut wide-target answer) + NX (a cut carries next_budget_tokens= above N). RED on base (d4325a8c): over_ceiling="1" + # at 900/2000 and a hard cut far short of T (900: 4 rows). + st="$( printf '%s' "$b" | python3 "$TMP/mcpst.py" "$TMP/mcp_ref.xml" "$tb" "$TBPT" )" + case "$root" in *'over_ceiling'*) lab=1 ;; *) lab=0 ;; esac + if [ "$lab" -eq 0 ] && [ "${st%% *}" = OK ]; then + ok "(5) budget_tokens=$tb: price SERVED (est_tokens=$est), no over_ceiling on the soft target, $st" else - no "(5) budget_tokens=$tb: est_tokens=$est exceeds the stated ceiling by $(( est - tb )) with NO over_ceiling — a silent overshoot on the MCP twin" + no "(5) budget_tokens=$tb: est_tokens=$est over_ceiling=$lab (NOLBL: must be 0) — $st" fi else case "$root" in diff --git a/test/memguardcheck.sh b/test/memguardcheck.sh index f6a5d3bab..4db353370 100755 --- a/test/memguardcheck.sh +++ b/test/memguardcheck.sh @@ -677,17 +677,38 @@ if [ "$rc" = "$refRc" ] && [ -s "$TMP/b22ref.out" ] && cmp -s "$TMP/b22.out" "$T else no "(B22) hard:7 rc=$rc (want $refRc) or its answer differs from the unseamed run" fi -# (B23) #387: a commit ingest the memory guard cut is never scored. The range form refuses (exit 5, one line naming the -# stop and the verb, no stdout) instead of answering from a partial tree with the backstop line after it (0.6.7). +# (B23) #387: a commit ingest the memory guard cut is never scored. Until qd-memory-degrade the range form refused here +# (exit 5, one line, no stdout). BY-DESIGN BREAK (qd-memory-degrade, owner decision 4, disclosed in CHANGELOG): the +# cut tree is released unscored and BOTH commits' changed files answer the kinds that need only them — one cut +# ingest + two changed-file ingests, a degraded header (ingest_scope="changed-files", memory_stop= naming the stop), +# exit 2 if a row gates else 6, and no row names a file outside the diff (a/extra.c). test/qddegradecheck.sh pins +# the degraded answer's contents; this arm pins that nothing of the cut full tree reaches it. Cache-stats lines: 2 — +# the cut base ingest and the target's changed files; the base's changed-file set is EMPTY (a/extra.c is an add), +# and an ingest that parses nothing prints no cache-stats line. for trip in parse:3 crawl:20; do qr_run "$trip"; rc=$? - if [ "$rc" = 5 ] && [ ! -s "$TMP/b22.out" ] && grep -q '^ripwire: the memory guard stopped the .*; --quality-delta=A..B cannot answer from a partial index' "$TMP/b22.err" \ - && [ "$( grep -c '^ripwire: [^c]' "$TMP/b22.err" )" = 1 ] && [ "$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" )" = 1 ]; then - ok "(B23) $trip cuts the base commit's ingest: the range form refuses before the target is ingested (exit 5, one line)" + gt="$( sed -n 's/.*]* gating="\([0-9]*\)".*/\1/p' "$TMP/b22.out" | head -n 1 )" + want=6; [ -n "$gt" ] && [ "$gt" -gt 0 ] && want=2 + outside="$( grep -o ' p="[^"]*"' "$TMP/b22.out" | grep -vc ' p="a/extra\.c:' )" + if [ -n "$gt" ] && [ "$rc" = "$want" ] && grep -q ']* ingest_scope="changed-files"' "$TMP/b22.out" \ + && grep -q "]* memory_stop=\"${trip%%:*}\"" "$TMP/b22.out" && [ "$outside" = 0 ] \ + && [ "$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" )" = 2 ] && [ "$( grep -c '^ripwire: memory limit ' "$TMP/b22.err" )" = 1 ] \ + && grep -q "^ripwire: memory limit .*stopped the ${trip%%:*} of the base commit" "$TMP/b22.err"; then + ok "(B23) $trip cuts the base commit's ingest: the cut tree is never scored — the changed files answer (exit $rc, degraded header naming the base's stop)" else - no "(B23) $trip rc=$rc stdout=$( wc -c <"$TMP/b22.out" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" + no "(B23) $trip rc=$rc (want $want) gating=$gt rows-outside-the-diff=$outside ingests=$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" ) stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" fi done +# (B23 twin, rule 4) the refusal still holds when even the changed files are cut: crawl:1 trips every ingest at its first +# crawl entry, so the target's changed-file ingest (a/, a/extra.c) is cut too — exit 5, no stdout, ONE line naming +# the stop, that the verb cannot answer from a partial index, and that the changed files were tried. +qr_run "crawl:1"; rc=$? +if [ "$rc" = 5 ] && [ ! -s "$TMP/b22.out" ] && grep -q '^ripwire: the memory guard stopped the .*; --quality-delta=A..B cannot answer from a partial index' "$TMP/b22.err" \ + && grep -q 'changed files' "$TMP/b22.err" && [ "$( grep -c '^ripwire: [^c]' "$TMP/b22.err" )" = 1 ]; then + ok "(B23) crawl:1 cuts the changed files too: the range form refuses (exit 5, one line naming the stop and the changed-files attempt)" +else + no "(B23) crawl:1 rc=$rc stdout=$( wc -c <"$TMP/b22.out" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" +fi # (B11) a verb's non-zero verdict from a partial ingest still gets the line; --html and --index-out never pass silently run_trip crawl:30 "$FX" --no-cache --html >/dev/null 2>"$TMP/b11.err"; rc=$? diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index 6ca1ae7db..83a8f958b 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -83,6 +83,12 @@ # (L) legend: the qword clause is present iff a qword row is, and says "by name" (not a resolution claim). # (I) RIPWIRE_BASE_BIN set: head + RIPWIRE_NO_OWNER_HOP=1 vs base on the --for / --json / MCP / budget answers, # head (no knob) vs base on the no-owner questions, plus 12 out-of-scope argv — every binary's rc checked. +# BY-DESIGN BREAK (rule 4, lane for-body-slot FIX1 R4): the head side also runs with RIPWIRE_NO_BODY_SLOT=1. The +# body slot (forowner.h) is independent of RIPWIRE_NO_OWNER_HOP by design (forbodyslotcheck (EV1)), so on any +# base without the slot 4 of these 27 argv differed for the slot alone (H c "log walk the process tables", +# H py "Reactive work", H ts "relay forward a request", M ts) — not for owner-hop. The arm's premise is kept: the +# owner-hop knob is still the one under test, and a head whose RIPWIRE_NO_OWNER_HOP=1 stops restoring the base +# still differs here (checklist 28; forbodyslotcheck I0 pins the slot-off == base half separately). # # Exit: 0 all PASS (named SKIPs allowed), 1 any FAIL, 2 missing prerequisite. @@ -379,7 +385,12 @@ missing = [ident(r) for r in base if ident(r) not in hd or not same(calls(r), ca gained = [r['n'] for r in base if ident(r) in hd and len(calls(hd[ident(r)])) > len(calls(r))] moved = [r['n'] for r in off['rows'] if ident(r) in owners] order_ok = [ident(r) for r in head if ident(r) in {ident(b) for b in base}] == [ident(r) for r in base if ident(r) in hd] -premise = bool(gained) and bool(moved) if mode == 'prefix' else True +# forbudget-soft (decision D-3; rule 4): the DEFAULT is no longer cut at its 1,000 B allowance, so the rows of the A/B handle +# are already complete and a moved owner can free nothing — "gained" is then impossible by construction. The premise keeps +# "an owner moved" and accepts either a gain or an A/B listing with no BUDGET-cut left to widen (a listing cut at the +# 16-name row cap cannot gain either: the cap is not the budget). +base_complete = all((r.get('ca') or {}).get('capped') != '1' or int((r.get('ca') or {}).get('shown', '0') or 0) >= 16 for r in base) # only the 16-name row cap left +premise = bool(moved) and (bool(gained) or base_complete) if mode == 'prefix' else True print('OK' if not missing and order_ok and off['rows'] and premise else 'BAD missing=%s order_ok=%s base_rows=%d gained=%s moved=%s' % (missing[:3], order_ok, len(off['rows']), gained, moved)) PY @@ -408,7 +419,20 @@ for spec in "compact|$QTS|" "compact|$QV|" "full|$QTS|--legend=full" "compact|$Q if [ "$non" -ge 1 ] && [ "$noff" = 0 ] && grep -q 'qword="gateway"' "$on"; then : else no "(S/$dia '$q'${extra:+ $extra}) premise: via clause head=$non A/B=$noff (want >=1 / 0) with the gateway owner row"; continue; fi fi - if [ "$( SECTS "$on" )" = "$( SECTS "$off" )" ] && [ "$( SECTS "$off" )" != "[]" ]; then + # forbudget-soft round C (owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): under an explicit --token-budget the + # owner rows are hop rows — a trailing section reserved before the rows past the target cut — so they may cost rows PAST + # the cut, never a row at it: the rows are then a rank prefix of the A/B handle's (the clause itself, a header disclosure, + # is still never charged). The default-regime specs keep the byte-identity. + if [ -n "$extra" ] && [ "$extra" != "--legend=full" ]; then + if python3 - "$on" "$off" <<'PY' +import re, sys +rows = lambda f: re.findall( r']*?\bp="([^"]*)"[^>]*?\bn="([^"]*)"|]*?\bn="([^"]*)"[^>]*?\bp="([^"]*)"', open( f ).read().split( '' )[ 0 ] ) +a, b = rows( sys.argv[ 1 ] ), rows( sys.argv[ 2 ] ) +sys.exit( 0 if a and b and b[ :len( a ) ] == a and 'over_ceiling="1"' not in open( sys.argv[ 1 ] ).read() else 1 ) +PY + then ok "(S/$dia '$q' $extra) soft target: the owner rows (a section, reserved first) leave a rank prefix of the A/B handle's, no over_ceiling" + else no "(S/$dia '$q' $extra) soft target: is not a rank prefix of the A/B handle's: $( grep -o ']*>' "$on" | cut -c1-80 ) vs $( grep -o ']*>' "$off" | cut -c1-80 )"; fi + elif [ "$( SECTS "$on" )" = "$( SECTS "$off" )" ] && [ "$( SECTS "$off" )" != "[]" ]; then ok "(S/$dia '$q'${extra:+ $extra}) the owner rows' via=\"name\" clause costs / nothing (byte-identical to the A/B handle)" else no "(S/$dia '$q'${extra:+ $extra}) / differ from the A/B handle: $( grep -o ']*>' "$on" ) vs $( grep -o ']*>' "$off" )"; fi done @@ -422,7 +446,9 @@ done # ── (B) explicit budget: owner funded first; an exhausted ceiling adds nothing; a cut is disclosed (C7) ────────────── f="$TMP/ts.b.xml"; run ts "$f" "--for=$QTS" --token-budget=4500 fo="$TMP/ts.b.off.xml"; runoff ts "$fo" "--for=$QTS" --token-budget=4500 -if ran_ok "$fo" "(B1) A/B" && ! grep -q 'reason="compact-route"' "$fo"; then no "(B1) premise: --token-budget=4500 no longer leaves the hops a budget on the A/B handle — re-tune"; fi +# for-body-slot (train 28; rule 4, by design): an explanatory question's owner body now rides beside the hops, and a served one +# turns the root's reason to "owner" — the premise "the hops kept a budget" reads either spelling (never "budget") +if ran_ok "$fo" "(B1) A/B" && ! grep -qE 'reason="(compact-route|owner)"' "$fo"; then no "(B1) premise: --token-budget=4500 no longer leaves the hops a budget on the A/B handle — re-tune"; fi if ran_ok "$f" "(B1)"; then HJ "$f" "$TMP/ts.b.json" "(B1)"; H root "$f" >"$TMP/ts.b.root" first="$( J "d['rows'][0]['n'] if d['rows'] else None" <"$TMP/ts.b.json" )" @@ -440,9 +466,11 @@ if ran_ok "$f" "(B1b)"; then if [ "$got" = row ] || [ "$got" = cut ] || grep -q 'reason="budget"' "$f"; then ok "(B1b) --token-budget=4000: the owner is first or disclosed ($got)" else no "(B1b) --token-budget=4000: gateway neither the first owner row nor named by qword_cut/next= ($got)"; fi fi -on="$TMP/b2.on.xml"; off="$TMP/b2.off.xml"; run ts "$on" "--for=$QTS" --token-budget=1200; runoff ts "$off" "--for=$QTS" --token-budget=1200 +# forbudget-soft round C (rule 4): sections are funded from the rows at the soft target's cut (reserve first), so 1200 tokens +# now leaves the hops a share; re-anchored at 700, the widest budget that still exhausts it (measured: 700 budget, 900 not) +on="$TMP/b2.on.xml"; off="$TMP/b2.off.xml"; run ts "$on" "--for=$QTS" --token-budget=700; runoff ts "$off" "--for=$QTS" --token-budget=700 if ran_ok "$on" "(B2)" && ran_ok "$off" "(B2) A/B"; then - grep -q 'reason="budget"' "$off" || no "(B2) premise: --token-budget=1200 no longer exhausts the ceiling (no reason=\"budget\") — re-tune" + grep -q 'reason="budget"' "$off" || no "(B2) premise: --token-budget=700 no longer exhausts the ceiling (no reason=\"budget\") — re-tune" if cmp -s "$on" "$off"; then ok "(B2) exhausted ceiling: byte-identical to the A/B handle (owner rows add nothing)"; else no "(B2) exhausted-ceiling answer changed"; fi fi # (B3) a budget sweep on the 105-callee owner: no budget may drop callee names silently @@ -505,10 +533,10 @@ if [ -n "${RIPWIRE_BASE_BIN:-}" ] && [ -x "${RIPWIRE_BASE_BIN}" ]; then mode="${parts[0]}"; r="${parts[1]}"; args=( ${parts[@]+"${parts[@]:2}"} ); shown="${args[*]-}" n=$((n+1)); hf="$TMP/i.$n.h"; bf="$TMP/i.$n.b" if [ "$mode" = M ]; then - mcp "RIPWIRE_NO_OWNER_HOP=1" "$BIN" "$hf"; mcp "RIPWIRE_X=0" "$RIPWIRE_BASE_BIN" "$bf" + mcp "RIPWIRE_NO_OWNER_HOP=1 RIPWIRE_NO_BODY_SLOT=1" "$BIN" "$hf"; mcp "RIPWIRE_X=0" "$RIPWIRE_BASE_BIN" "$bf" # R4: slot off on the head side else if [ "$mode" = K ]; then knob="RIPWIRE_NO_OWNER_HOP=1"; else knob="RIPWIRE_X=0"; fi - ( cd "$FIX/$r" && env "$knob" "$BIN" . --no-cache ${args[@]+"${args[@]}"} >"$hf" 2>"$hf.err" ); printf '%s' "$?" >"$hf.rc" + ( cd "$FIX/$r" && env "$knob" RIPWIRE_NO_BODY_SLOT=1 "$BIN" . --no-cache ${args[@]+"${args[@]}"} >"$hf" 2>"$hf.err" ); printf '%s' "$?" >"$hf.rc" # R4 ( cd "$FIX/$r" && "$RIPWIRE_BASE_BIN" . --no-cache ${args[@]+"${args[@]}"} >"$bf" 2>"$bf.err" ); printf '%s' "$?" >"$bf.rc" fi if ! ran_ok "$hf" "(I) head $mode $r $shown" || ! ran_ok "$bf" "(I) base $mode $r $shown"; then e=$((e+1)); continue; fi diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 573236c98..3ce61d08a 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 1dd0a41c39f89e4b35fb40750a4b2fd56376daf2161d24b03d32423d788b352d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 1b039d143be854f26a34412e4a2949e98e3b71caf41f76bd4fc4935c89047295 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 ecc9e1ea480e502fb228fb129f1309e37e30918d8baf3f36e9b9f01be0277d59 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 0cca7164a48d6543694bf07b4252209558d189771d50e913e46783bb98ae4bed e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 827b569b350640938954e34497f0430f757b4baac8f0daa2194306113cb46a36 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 e3535cdf695ad1d3e9ffbec44d529478059317b226fb60b2dfffaebc90b44218 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 3a8337704c1a55c31816e1bd43c5389186062503f35f1ecac548ca52ff37e84c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 de8c0030151f952e432e193a0979f3fb49c3703fadc09e0764647a29bff68ffb e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 75e01af89c0b091a55273242ac8cd84582d27f95c3d6c6452a04f2d3f1ee353c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/pyimportprecisecheck.sh b/test/pyimportprecisecheck.sh index 355a28c70..3f08d6c51 100755 --- a/test/pyimportprecisecheck.sh +++ b/test/pyimportprecisecheck.sh @@ -205,11 +205,12 @@ if python3 - "$TMP/pn.fresh" "$TMP/pn.bin" "$ROOT" <<'PY' import copy, pathlib, re, struct, sys, xml.etree.ElementTree as ET from collections import Counter -# readFileRecord/writeBind in ingest_cache.h, lean cache v31. No heuristic string searches: +# readFileRecord/writeBind in ingest_cache.h, lean cache v32 (train 28: 31 -> 32 moved the number only — two appended +# RefRole values and one appended bind kind; no record changes shape). No heuristic string searches: # walk the counted arrays and require complete file coverage, clean parse health and exact EOF. blob = pathlib.Path(sys.argv[2]).read_bytes() endian = '<' if sys.byteorder == 'little' else '>' -assert blob[:4] == b'CTPK' and struct.unpack_from(endian + 'I', blob, 4)[0] == 31, 'update the cache walker for a new layout' +assert blob[:4] == b'CTPK' and struct.unpack_from(endian + 'I', blob, 4)[0] == 32, 'update the cache walker for a new layout' table, count = struct.unpack_from(endian + 'QI', blob, len(blob) - 24) assert count == 5 == struct.unpack_from(endian + 'I', blob, 21)[0], 'all five fixtures must be cached' assert table + count * 32 == len(blob) - 24, 'cache table bounds' diff --git a/test/qchurnmemocheck.sh b/test/qchurnmemocheck.sh index 5d9df07f3..390a8c063 100755 --- a/test/qchurnmemocheck.sh +++ b/test/qchurnmemocheck.sh @@ -73,14 +73,35 @@ CHURN="$( printf '%s' "$OUT" | tr '<' '\n' | grep -c 'kind="short-horizon-churn" && ok "fixture produces $CHURN short-horizon-churn rows on one file (non-vacuous)" \ || { no "fixture produced $CHURN churn rows — the memo check would be vacuous"; printf '%s' "$OUT" | tr '<' '\n' | grep '^r kind=' | head -5; } -SPAWNS="$( grep -c 'diff --unified=0' "$LOG" )" -PATHS="$( grep 'diff --unified=0' "$LOG" | sed 's/.* -- //' | sort -u | grep -c . )" +# The spawn is matched by its SHAPE, not one exact spelling: the git subcommand `diff`, then only long options +# (the binary passes its own neutralising options such as --no-ext-diff / --no-textconv here), then +# --unified=0. A `-c diff.x=y` value, a `log -p --unified=0` or a `diff --stat` is not this spawn. +DIFFRE='(^| )diff( --[^ ]+)* --unified=0( |$)' +spawns_in(){ grep -cE "$DIFFRE" "$1"; } +paths_in(){ grep -E "$DIFFRE" "$1" | sed 's/.* -- //' | sort -u | grep -c .; } + +# matcher control: a planted log with the pre-memo shape (three identical spawns for ONE path) plus near-miss +# spawns that must not count. If the matcher cannot see the duplicates, the (b) arm below proves nothing. +CTL="$WORK/ctl.txt" +{ printf '%s\n' "-c core.quotepath=false -C /r diff --no-ext-diff --no-textconv --ignore-submodules=dirty --unified=0 --no-color HEAD -- src/f.cpp" \ + "-c core.quotepath=false -C /r diff --no-ext-diff --no-textconv --ignore-submodules=dirty --unified=0 --no-color HEAD -- src/f.cpp" \ + "-c core.quotepath=false -C /r diff --unified=0 --no-color HEAD -- src/f.cpp" \ + "-c diff.algorithm=myers -C /r log -p --unified=0 HEAD -- src/g.cpp" \ + "-C /r diff --stat HEAD -- src/h.cpp" \ + "-C /r diff-tree --unified=0 HEAD -- src/i.cpp"; } > "$CTL" +CS="$( spawns_in "$CTL" )"; CP="$( paths_in "$CTL" )" +[ "$CS" = 3 ] && [ "$CP" = 1 ] \ + && ok "(control) the spawn matcher counts 3 pre-memo duplicates of one path and none of 3 near-miss spawns" \ + || no "(control) the spawn matcher saw $CS spawn(s) / $CP path(s) in the planted log, expected 3 / 1" + +SPAWNS="$( spawns_in "$LOG" )" +PATHS="$( paths_in "$LOG" )" [ "$SPAWNS" -gt 0 ] && ok "the churn SELF/AMBIENT classifier ran (${SPAWNS} unified=0 diff spawn(s) logged)" \ || no "no 'git diff --unified=0' spawn logged — the shim or the fixture is wrong" [ "$SPAWNS" -eq "$PATHS" ] \ && ok "ONE spawn per PATH: $SPAWNS spawn(s) for $PATHS distinct path(s) — the run-scoped memo holds" \ || { no "$SPAWNS spawns for only $PATHS distinct path(s) — the per-path memo is gone or bypassed" - grep 'diff --unified=0' "$LOG" | sort | uniq -c | sort -rn | head -3; } + grep -E "$DIFFRE" "$LOG" | sort | uniq -c | sort -rn | head -3; } [ "$CHURN" -gt "$SPAWNS" ] \ && ok "the memo saved real work: $CHURN churn rows served by $SPAWNS spawn(s)" \ || no "churn rows ($CHURN) <= spawns ($SPAWNS) — nothing was actually deduplicated" diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh new file mode 100755 index 000000000..d50e2c2dc --- /dev/null +++ b/test/qddegradecheck.sh @@ -0,0 +1,639 @@ +#!/usr/bin/env bash +# qddegradecheck.sh — #387 follow-up: --quality-delta=A..B ANSWERS under memory pressure instead of refusing. +# +# THE DEFECT. A tripped memory guard in the range form gives exit 5 and nothing on stdout. On a large repository a +# 4-file change cannot get even the findings that need only those 4 files. +# +# THE CONTRACT these arms pin. Why a kind is LOCAL: every per-symbol baseline key is path-qualified (quality.h qualityKey) +# and so is the canonical id, so an UNCHANGED file yields the same keys and metrics in both trees and no row of such a kind: +# * The 12 kinds split by input scope. LOCAL kinds (api-surface, complexity, error-masking, nesting, params, +# placeholder, verbosity) need only the changed files' trees. defect-shape is LOCAL when the changed files define +# the same C-family macro names on both sides and either the base's whole macro set was read (P1) or no changed +# file is C++ (P0); otherwise it is skipped. WHOLE-TREE kinds (dead-code, duplication, new-clone-of-reused-helper) +# need every file. short-horizon-churn is unavailable in the range form. +# * When the base commit fit but the target phase is projected past the limit (P1), when the guard stopped a commit's +# full ingest (P0), or when it stopped the target's ingest or graph after the projection said it fits (P1-late), +# the range form ingests only the changed files of BOTH commits (never through a cache) and runs the kinds that can +# run there. Every kind it does not run is NAMED (kinds_skipped=, skipped_why="memory"), with the limit +# (memory_limit=), the projected need (memory_need="NM") or, after a stop, the stop (memory_stop=) and +# memory_need="unmeasured", the whole-answer facts not computed (also_skipped=), and a replay (kinds_next=) that +# runs exactly the skipped kinds. +# * Exit: 2 when a row of a kind that ran gates (the skipped kinds can only add rows); otherwise 6 = a partial answer +# with no gating row (never 0). Exit 5 with nothing on stdout only when even the changed files do not fit, or when +# the changed files cannot stand in for the trees (a symlink, an export attribute) — named on one line. +# * When memory suffices the degraded path is unreachable: output byte-identical to the build before it (L, T). +# * --quality-kinds=K[,K] (range form only) runs exactly those kinds (the replay's selector): kinds_skipped= +# skipped_why="selector"; stale acks are checked for the selected kinds only. +# +# SEAMS (RIPWIRE_TEST_MEMGUARD, memguard.h): crawl:N and parse:N count per INGEST (each ingest's own Watch), so (E) depends +# on the changed-file subsets staying under 12 crawl entries, directories included; project:N = the Nth projection +# reading says "does not fit" (one per range run); ingest:N = the Nth ingest of the process is cut at its first crawl +# entry; peak:N = the Nth post-answer peak reading reads as over the limit; hard:N = the Nth and later hard-line readings. +# +# Arms (RED on the build before this lane, except the guards (A), (G) A..A, (K) unpressured --json, (L) and (T)): +# (A) premise: the fixture's unpressured answer has a row of each of the 11 kinds the range form measures. +# (B) P1 forced (project:1): the degraded answer's header, exit code, stderr line, three ingests. +# (C) P1: each LOCAL kind's rows equal the same kind's rows of (A); no row of a skipped kind. +# (D) the kinds_next= replay yields exactly the skipped kinds' rows, not degraded again; union with (B) = (A). +# (E) P0 (crawl seam cuts the base's FULL ingest): answers from the changed files; defect-shape skipped (a changed +# C++ file and no whole macro set); memory_stop=, memory_need="unmeasured"; (E2) a Python-only change keeps it. +# (F) even the changed files do not fit (hard line after the base ingest, and every later reading): exit 5; the +# pre-check refusal says the changed files were NOT tried, never that they "did not fit" (review C3). +# (G) near misses under project:1: an empty diff, a rename-only diff, a non-code-only diff, and A..A. +# (H) --quality-kinds: unknown kind refused naming the 12; refused beside --quality-ack and on the bare form; each +# single kind = (A)'s rows of that kind; all 12 named = byte-identical to no flag. +# (I) --quality-ack is never pre-empted by the projection (review C2: project:1 acks in full, the same ledger as an +# unpressured ack); a real stop (crawl:12) refuses it (exit 5) and leaves the ledger untouched. +# (J) --scope naming an unchanged file under pressure is not refused as "names nothing indexed". +# (K) legend + JSON: --legend=full defines every new attribute and parses as XML; --json carries the twins. +# (L) guard: a projection seam index past the run's one projection reading changes nothing (byte-identical). +# --- round 2 (gates review R1-R9) --- +# (M) R1 defect-shape: a new site whose text also sits in an UNCHANGED file keeps its gating row under P1 and P0. +# (N) R2 the changed set: an unchanged symlink, an export-ignore flip and an export-subst tree refuse by name (exit 5); +# an unchanged export-ignore attribute and a CMakeCache.txt-pruned directory answer like the full run. +# A git grep that FAILS (exit 128, through a PATH shim) refuses (exit 1), never "no export attribute". +# (O) R3 acks: a legacy-scheme ack on a name two files define (one changed) suppresses the same rows as the full run; +# on a name one file defines, the degraded answer names ack-healing as skipped and shows the row. +# (P) R4 cache isolation: P1 with the cache on leaves the HEAD ingest-cache blob untouched; a cached run after it is +# byte-identical to (A). +# (Q) R5 the target-phase stop (ingest:2 cuts the target's full ingest): a degraded answer naming the target's stop. +# (R) R7b a peak over the limit (peak:1) is disclosed on a full answer, rc and rows unchanged; R7c a selection the +# changed files cannot answer refuses under a stop (exit 5), never an empty pass, and is never pre-empted by the +# projection (the replay runs in full). +# (S) R9 --quality-kinds=complexity reports no false finding-gone for a dead-code ack. +# (T) S2 fits near the line: --max-memory at 2x the measured full peak answers in full, byte-identical (guard). +# (V) R8 a degraded answer with rows and no gating row exits 6, not 2 and not 0. +# --- fix round 1 (final review C1) --- +# (W) --quality-kinds with a LOCAL kind under pressure: the kinds the caller left out are skipped_why="selector", +# never memory cuts; memory's share (memory_skipped=, skipped_why="selector,memory") is exactly what kinds_next= +# replays; no stderr line ends in an empty "skipped" list. +# +# Usage: bash test/qddegradecheck.sh [BIN] +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" +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 git >/dev/null 2>&1 || { echo " SKIP qddegradecheck (git not available)"; exit 0; } +command -v python3 >/dev/null 2>&1 || { echo " SKIP qddegradecheck (python3 not available)"; exit 0; } +WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/tmp" +echo "qddegradecheck: BIN=$BIN" + +LOCAL_KINDS="api-surface complexity defect-shape error-masking nesting params placeholder verbosity" +LOCAL_CSV="$( echo "$LOCAL_KINDS" | tr ' ' ',' )" +WHOLE_KINDS="dead-code,duplication,new-clone-of-reused-helper" +ALL12="api-surface,complexity,dead-code,defect-shape,duplication,error-masking,nesting,new-clone-of-reused-helper,params,placeholder,short-horizon-churn,verbosity" + +g(){ git -C "$1" -c user.name=t -c user.email=t@example.com "${@:2}"; } + +# ── the fixture: a base commit and a target commit with one change per kind, beside UNCHANGED files ───────────── +# The unchanged files make the full tree larger than the changed set, so a crawl seam can cut the full ingest while the +# changed-files ingest stays under it (E). +mkfx(){ + local R="$1"; rm -rf "$R"; mkdir -p "$R/src" "$R/py" + git -C "$R" init -q + for k in 1 2 3 4 5 6; do + printf 'int keep_%s( int v ) { return v + %s; }\n' "$k" "$k" >"$R/src/u$k.cpp" + printf 'def keep_%s(v):\n return v * %s\n' "$k" "$k" >"$R/py/u$k.py" + done + printf '#pragma once\ninline int api_scale( int a ) { return a * 2; }\n' >"$R/src/api.h" + { + printf '#include \n#include "api.h"\n' + printf 'int helper( int a, int b ) { int s = 0; for( int i = 0; i < a; ++i ) { s += i * b; if( s > 100 ) { s -= b; } } return s + a - b; }\n' + printf 'int use1( int x ) { return helper( x, 1 ); }\nint use2( int x ) { return helper( x, 2 ); }\nint use3( int x ) { return helper( x, 3 ); }\n' + printf 'int legacy_used( int x ) { return x + 7; }\n' + printf 'int wide( int a, int b, int c, int d, int e ) { return a + b + c + d + e; }\n' + printf 'void risky() {}\nvoid handle() { try { risky(); } catch( ... ) { std::puts( "logged" ); } }\n' + printf 'int grow( int x )\n{\n if( x > 1 ) { return 1; }\n return 0;\n}\n' + printf 'int chatty( int x )\n{\n int r = x;\n r += 1;\n r += 2;\n return r;\n}\n' + printf 'int drive( int x ) { return use1( x ) + use2( x ) + use3( x ) + legacy_used( x ) + wide( 1, 2, 3, 4, 5 ) + grow( x ) + api_scale( x ) + chatty( x ); }\n' + } >"$R/src/core.cpp" + printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {}".format(name)\n' >"$R/py/mod.py" + printf 'notes\n' >"$R/NOTES.txt" + g "$R" add -A; g "$R" commit -qm base +} +# target: complexity+nesting (grow), verbosity (chatty), params (wide 5->6), api-surface (api_scale 1->2, a header), +# error-masking (an emptied catch), dead-code (legacy_used loses its caller), duplication + new-clone-of-reused-helper +# (helper, fan-in 3, copied), placeholder (a TODO, a NotImplementedError stub), defect-shape (a .format arity miss) +mktarget(){ + local R="$1" + printf '#pragma once\ninline int api_scale( int a, int b ) { return a * b; }\n' >"$R/src/api.h" + { + printf '#include \n#include "api.h"\n' + printf 'int helper( int a, int b ) { int s = 0; for( int i = 0; i < a; ++i ) { s += i * b; if( s > 100 ) { s -= b; } } return s + a - b; }\n' + printf 'int use1( int x ) { return helper( x, 1 ); }\nint use2( int x ) { return helper( x, 2 ); }\nint use3( int x ) { return helper( x, 3 ); }\n' + printf 'int legacy_used( int x ) { return x + 7; }\n' + printf 'int wide( int a, int b, int c, int d, int e, int f ) { return a + b + c + d + e + f; }\n' + printf 'void risky() {}\nvoid handle() { try { risky(); } catch( ... ) {} }\n' + printf 'int grow( int x )\n{\n int r = 0;\n for( int i = 0; i < x; ++i ) {\n if( i %% 2 ) {\n while( r < i ) {\n' + printf ' if( r %% 3 ) {\n for( int j = 0; j < 3; ++j ) {\n' + printf ' if( j && r ) { r += j; } else if( r > 9 ) { r -= 1; } else { r += 2; }\n }\n' + printf ' } else if( r %% 5 ) { r += 3; } else { r += 4; }\n }\n' + printf ' } else if( i %% 7 ) { r -= 1; } else if( i %% 11 ) { r += 5; } else { r -= 2; }\n' + printf ' if( r > 1000 && i > 3 ) { break; }\n if( r < -1000 || i > 900 ) { continue; }\n }\n return r;\n}\n' + printf 'int chatty( int x )\n{\n int r = x;\n' + for i in $( seq 1 69 ); do printf ' r += %s;\n' "$i"; done + printf ' return r;\n}\n' + printf 'int drive( int x ) { return use1( x ) + use2( x ) + use3( x ) + wide( 1, 2, 3, 4, 5, 6 ) + grow( x ) + api_scale( x, 2 ) + chatty( x ); }\n' + } >"$R/src/core.cpp" + printf 'int helper_copy( int a, int b ) { int s = 0; for( int i = 0; i < a; ++i ) { s += i * b; if( s > 100 ) { s -= b; } } return s + a - b; }\n' >"$R/src/copy.cpp" + printf 'def other():\n # TODO: wire this up\n return 1\n\ndef greet(name):\n return "hello {} {}".format(name)\n\ndef later():\n raise NotImplementedError\n' >"$R/py/mod.py" + g "$R" add -A; g "$R" commit -qm target +} + +FX="$WORK/fx"; mkfx "$FX"; mktarget "$FX" +BASE="$( git -C "$FX" rev-parse HEAD~1 )"; TGT="$( git -C "$FX" rev-parse HEAD )" +# run [ENV=VAL ...] -- ARGS: stdout/stderr to $WORK/o, $WORK/e; rc in $RC +qd(){ ( cd "$FX" && env TMPDIR="$WORK/tmp" RIPWIRE_CACHE_STATS=1 "$@" >"$WORK/o" 2>"$WORK/e" ); RC=$?; } +# the same in another repo: qdin REPO [ENV=VAL ...] BIN ARGS +qdin(){ local R="$1"; shift; ( cd "$R" && env TMPDIR="$WORK/tmp" RIPWIRE_CACHE_STATS=1 "$@" >"$WORK/o" 2>"$WORK/e" ); RC=$?; } +# rows: every and element on its own line, sorted (the union rule: rows compare as SORTED sets) +rows(){ python3 - "$1" "${2:-}" <<'PY' +import re, sys +doc = open(sys.argv[1], encoding="utf-8", errors="replace").read() +want = set(k for k in sys.argv[2].split(",") if k) +out = [] +for m in re.finditer(r'<(r|sa) [^>]*/>', doc): + k = re.search(r' kind="([^"]*)"', m.group(0)) + if k and (not want or k.group(1) in want): + out.append(m.group(0)) +print("\n".join(sorted(out))) +PY +} +# the root attribute NAMED $2 — space-led, so gating= never reads scoped-out-gating= (the greedy match takes the last) +attr(){ sed -n 's/.*]* '"$2"'="\([^"]*\)".*/\1/p' "$1" | head -n 1; } +hasattr(){ grep -q "]* $2=\"" "$1"; } +ingests(){ grep -c '^ripwire: cache-stats ' "$WORK/e"; } +memlines(){ grep -c '^ripwire: memory limit ' "$WORK/e"; } +isnum(){ case "$1" in ''|*[!0-9]*) return 1;; *) return 0;; esac; } +# R8: the exact exit code of a degraded answer — 2 when a row gates, else 6 (a partial answer with no gating row). +# Read only off a document whose gating= is a number; anything else is no exit code at all. +degrc(){ local gt; gt="$( attr "$1" gating )"; isnum "$gt" || { echo "none"; return; }; [ "$gt" -gt 0 ] && echo 2 || echo 6; } +validxml(){ python3 -c 'import sys,xml.dom.minidom as m; m.parse(sys.argv[1])' "$1" 2>/dev/null; } +fnv(){ python3 -c ' +import sys +h = 0xcbf29ce484222325 +for b in sys.argv[1].encode(): + h ^= b; h = (h * 0x100000001b3) & 0xFFFFFFFFFFFFFFFF +print("%016x" % h)' "$1"; } + +# ── (A) the premise ─────────────────────────────────────────────────────────────────────────────────────────── +qd "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/A.xml"; cp "$WORK/e" "$WORK/A.err"; ARC=$RC +missing="" +for k in api-surface complexity dead-code defect-shape duplication error-masking nesting new-clone-of-reused-helper params placeholder verbosity; do + [ -n "$( rows "$WORK/A.xml" "$k" )" ] || missing="$missing $k" +done +if [ "$ARC" = 2 ] && [ -z "$missing" ]; then ok "(A) premise: the unpressured answer has a row of each of the 11 measured kinds (rc=2)" +else no "(A) premise: rc=$ARC, kinds with no row:$missing — every arm below compares against this answer, so it proves nothing"; fi +AGATE="$( attr "$WORK/A.xml" gating )" + +# ── (B) P1 forced: the base fit, the target phase is projected past the limit ─────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/B.xml"; cp "$WORK/e" "$WORK/B.err"; BRC=$RC; BING="$( ingests )" +bgate="$( attr "$WORK/B.xml" gating )"; need="$( attr "$WORK/B.xml" memory_need )"; needn="${need%M}" +wantrc="$( degrc "$WORK/B.xml" )" +bnext="$( attr "$WORK/B.xml" kinds_next )" +if [ -s "$WORK/B.xml" ] && validxml "$WORK/B.xml" \ + && [ "$( attr "$WORK/B.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/B.xml" skipped_why )" = memory ] \ + && [ "$( attr "$WORK/B.xml" ingest_scope )" = changed-files ] && hasattr "$WORK/B.xml" memory_limit \ + && isnum "$needn" && [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ] && ! hasattr "$WORK/B.xml" stale \ + && ! hasattr "$WORK/B.xml" memory_stop && ! hasattr "$WORK/B.xml" register-macro-excluded && isnum "$bgate" && [ "$BRC" = "$wantrc" ]; then + ok "(B) P1: a degraded answer (rc=$BRC, gating=$bgate) names kinds_skipped=$WHOLE_KINDS, the limit, need=${need}, also_skipped, no stale=/dead-code counts" +else + no "(B) P1: rc=$BRC (want $wantrc) gating=$bgate kinds_skipped=$( attr "$WORK/B.xml" kinds_skipped ) why=$( attr "$WORK/B.xml" skipped_why ) scope=$( attr "$WORK/B.xml" ingest_scope ) need=$need stdout=$( wc -c <"$WORK/B.xml" | tr -d ' ' )B" +fi +# R7c: the replay's --max-memory is strictly above the projected need (in whole MiB) and clears the parser's 64M floor. +# CodeRabbit #413 (by-design change of this pin, rule 4): it is also never below the limit this run used — a replay under +# a lower limit than the one that degraded cannot answer in full — and above twice the ingest need (the parse line; on +# this fixture far under the limit, so the limit term decides). The old reading (above need, at least 64M) still holds. +blim="$( attr "$WORK/B.xml" memory_limit )"; blimn="${blim%M}" +replaymb=""; isnum "$needn" && isnum "$blimn" && { replaymb=$(( needn > blimn ? needn + 1 : blimn + 1 )); [ "$replaymb" -lt 64 ] && replaymb=64; } +if [ -n "$replaymb" ] && [ "$replaymb" -gt "$needn" ] && [ "$bnext" = "--quality-delta=$BASE..$TGT --quality-kinds=$WHOLE_KINDS --max-memory=${replaymb}M" ]; then + ok "(B) kinds_next= replays exactly the skipped kinds at --max-memory=${replaymb}M (above need=${need} and the ${blim} limit, at least the 64M floor)" +else no "(B) kinds_next='$bnext' (want --max-memory=${replaymb:-?}M: above need=${need} and limit=${blim})"; fi +if [ "$( memlines )" = 1 ] && grep -q "^ripwire: memory limit .*dead-code, duplication, new-clone-of-reused-helper" "$WORK/B.err" && [ "$BING" = 3 ]; then + ok "(B) one stderr line names the skipped kinds; three ingests (the base in full, then both commits' changed files)" +else no "(B) memory lines=$( memlines ) ingests=$BING stderr: $( grep '^ripwire: [^c]' "$WORK/B.err" | head -c 300 )"; fi + +# ── (C) P1: LOCAL kinds equal the unpressured answer, kind by kind ───────────────────────────────────────────── +if [ -n "$( attr "$WORK/B.xml" kinds_skipped )" ]; then + for k in $LOCAL_KINDS; do + if [ "$( rows "$WORK/B.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ] && [ -n "$( rows "$WORK/A.xml" "$k" )" ]; then ok "(C) P1 $k: rows identical to the unpressured answer" + else no "(C) P1 $k: rows differ: $( diff <( rows "$WORK/A.xml" "$k" ) <( rows "$WORK/B.xml" "$k" ) | head -c 300 )"; fi + done + if [ -z "$( rows "$WORK/B.xml" "$WHOLE_KINDS" )" ]; then ok "(C) P1: no row of a skipped kind"; else no "(C) P1: a skipped kind has rows"; fi + if [ "$( attr "$WORK/B.xml" api-new-surface )" = "$( attr "$WORK/A.xml" api-new-surface )" ]; then ok "(C) P1: api-new-surface= equal"; else no "(C) api-new-surface differs"; fi +else + no "(C) P1: premise — (B) is not a degraded answer (no kinds_skipped=), so per-kind identity proves nothing" +fi + +# ── (D) the replay ───────────────────────────────────────────────────────────────────────────────────────────── +if [ -n "$bnext" ]; then + # shellcheck disable=SC2086 + qd "$BIN" . $bnext --no-cache; cp "$WORK/o" "$WORK/D.xml"; DRC=$RC + dgate="$( attr "$WORK/D.xml" gating )" + if [ "$( rows "$WORK/D.xml" )" = "$( rows "$WORK/A.xml" "$WHOLE_KINDS" )" ] && [ -n "$( rows "$WORK/D.xml" )" ]; then ok "(D) the replay's rows are exactly (A)'s skipped-kind rows" + else no "(D) replay rc=$DRC rows differ: $( diff <( rows "$WORK/A.xml" "$WHOLE_KINDS" ) <( rows "$WORK/D.xml" ) | head -c 300 )"; fi + if [ "$( { rows "$WORK/B.xml"; rows "$WORK/D.xml"; } | sort )" = "$( rows "$WORK/A.xml" )" ]; then ok "(D) union of the degraded answer and the replay = the unpressured answer (as sorted rows)" + else no "(D) union differs from (A)"; fi + if isnum "$bgate" && isnum "$dgate" && isnum "$AGATE" && [ $(( bgate + dgate )) = "$AGATE" ]; then ok "(D) gating= adds up: $bgate + $dgate = $AGATE" + else no "(D) gating: B=$bgate D=$dgate A=$AGATE"; fi + if [ "$( attr "$WORK/D.xml" skipped_why )" = selector ]; then ok "(D) the replay says its kinds were selected (skipped_why=selector)"; else no "(D) replay skipped_why=$( attr "$WORK/D.xml" skipped_why )"; fi + # R7c: the replay's --max-memory clears the parser floor and is above the projection that degraded — it is accepted + # (not exit 1) and answers in full (no ingest_scope=), with the exit code its own rows decide + dwant=0; isnum "$dgate" && [ "$dgate" -gt 0 ] && dwant=2 + if [ "$DRC" = "$dwant" ] && [ -s "$WORK/D.xml" ] && ! hasattr "$WORK/D.xml" ingest_scope && ! hasattr "$WORK/D.xml" memory_stop; then + ok "(D) the replay is accepted at its --max-memory and is not degraded again (rc=$DRC)" + else no "(D) the replay rc=$DRC (want $dwant) or it degraded again: scope=$( attr "$WORK/D.xml" ingest_scope ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +else + no "(D) premise: no kinds_next= to replay" +fi + +# ── (E) P0: the guard cut the base commit's FULL ingest (crawl seam: the full tree has more entries than the changed set) ─ +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/E.xml"; cp "$WORK/e" "$WORK/E.err"; ERC=$RC +if [ -s "$WORK/E.xml" ] && [ "$( attr "$WORK/E.xml" kinds_skipped )" = "dead-code,defect-shape,duplication,new-clone-of-reused-helper" ] \ + && [ "$( attr "$WORK/E.xml" ingest_scope )" = changed-files ] && [ "$( ingests )" = 3 ] && [ "$ERC" = "$( degrc "$WORK/E.xml" )" ]; then + ok "(E) P0: the cut full ingest is never scored; the changed files answer (rc=$ERC); defect-shape skipped (a changed C++ file)" +else + no "(E) P0: rc=$ERC (want $( degrc "$WORK/E.xml" )) kinds_skipped=$( attr "$WORK/E.xml" kinds_skipped ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/E.err" | head -c 300 )" +fi +if [ -n "$( attr "$WORK/E.xml" kinds_skipped )" ]; then + for k in api-surface complexity error-masking nesting params placeholder verbosity; do + if [ "$( rows "$WORK/E.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ]; then ok "(E) P0 $k: rows identical"; else no "(E) P0 $k: rows differ"; fi + done + # R7a: a soft stop does not show the need exceeds the limit — the stop is named, the need is not claimed + if [ "$( attr "$WORK/E.xml" memory_stop )" = crawl ] && [ "$( attr "$WORK/E.xml" memory_need )" = unmeasured ]; then + ok "(E) P0: memory_stop=\"crawl\" names the stop; memory_need=\"unmeasured\" (a crawl stop does not measure the need)" + else no "(E) P0 memory_stop=$( attr "$WORK/E.xml" memory_stop ) memory_need=$( attr "$WORK/E.xml" memory_need )"; fi +else + no "(E) P0: premise — not a degraded answer" +fi +# (E2) a Python-only change: no changed C++ file, so defect-shape runs on the changed files and equals the unpressured rows +FX2="$WORK/fx2"; mkfx "$FX2" +printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {} {}".format(name)\n' >"$FX2/py/mod.py"; g "$FX2" add -A; g "$FX2" commit -qm py +( cd "$FX2" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2a.xml" 2>/dev/null ) +( cd "$FX2" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2.xml" 2>/dev/null ); e2rc=$? +if [ "$( attr "$WORK/E2.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ -n "$( rows "$WORK/E2a.xml" defect-shape )" ] \ + && [ "$( rows "$WORK/E2.xml" defect-shape )" = "$( rows "$WORK/E2a.xml" defect-shape )" ] && [ "$e2rc" = "$( degrc "$WORK/E2.xml" )" ]; then + ok "(E2) P0, Python-only change: defect-shape runs on the changed files and its rows equal the unpressured ones (rc=$e2rc)" +else + no "(E2) rc=$e2rc (want $( degrc "$WORK/E2.xml" )) kinds_skipped=$( attr "$WORK/E2.xml" kinds_skipped ) unpressured defect rows=$( rows "$WORK/E2a.xml" defect-shape | grep -c . )" +fi + +# ── (F) even the changed files do not fit ────────────────────────────────────────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=hard:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; FRC=$RC +if [ "$FRC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] \ + && grep -q '^ripwire: memory limit reached during the ingest of the base commit (limit .*changed files' "$WORK/e" \ + && grep -q 'after releasing the full tree, so the changed files were not tried' "$WORK/e" && ! grep '^ripwire: ' "$WORK/e" | grep -q 'did not fit'; then + ok "(F) still over the limit after releasing the full tree: exit 5, nothing on stdout, one line naming the phase and that the changed files were not tried" +else + no "(F) rc=$FRC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )" +fi + +# ── (G) near misses under project:1 ──────────────────────────────────────────────────────────────────────────── +nm(){ # $1 label, $2 repo; compares LOCAL rows with the unpressured run and requires the degraded header and the exact rc + # S5: new-symbol= counts the rows of the kinds that RAN, so the degraded count equals the unpressured run's LOCAL rows + # carrying origin="new-symbol" (a rename makes every moved symbol new in both modes); api-new-surface= equal outright. + local L="$1" R="$2" + ( cd "$R" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmA.xml" 2>/dev/null ) + ( cd "$R" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmB.xml" 2>"$WORK/nmB.err" ); local rc=$? + local localnew; localnew="$( rows "$WORK/nmA.xml" "$LOCAL_CSV" | grep -c 'origin="new-symbol"' )" + if [ "$( attr "$WORK/nmB.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( rows "$WORK/nmB.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/nmA.xml" "$LOCAL_CSV" )" ] \ + && ! grep -q 'could not materialize' "$WORK/nmB.err" && [ "$rc" = "$( degrc "$WORK/nmB.xml" )" ] \ + && [ "$( attr "$WORK/nmB.xml" new-symbol )" = "$localnew" ] \ + && [ "$( attr "$WORK/nmB.xml" api-new-surface )" = "$( attr "$WORK/nmA.xml" api-new-surface )" ]; then + ok "(G) $L: degraded answer (rc=$rc), LOCAL rows, new-symbol=$localnew (the LOCAL new-symbol rows) and api-new-surface= equal the unpressured run" + else + no "(G) $L: rc=$rc (want $( degrc "$WORK/nmB.xml" )) kinds_skipped=$( attr "$WORK/nmB.xml" kinds_skipped ) new-symbol want $localnew got $( attr "$WORK/nmB.xml" new-symbol ) stderr: $( head -c 300 "$WORK/nmB.err" )" + fi +} +FE="$WORK/fe"; mkfx "$FE"; g "$FE" commit -q --allow-empty -m empty; nm "empty diff" "$FE" +FR="$WORK/fr"; mkfx "$FR"; g "$FR" mv py/u1.py py/u1_moved.py; g "$FR" commit -qm rename; nm "rename-only diff" "$FR" +FN="$WORK/fn"; mkfx "$FN"; printf 'more notes\n' >>"$FN/NOTES.txt"; g "$FN" add -A; g "$FN" commit -qm notes; nm "non-code-only diff" "$FN" +( cd "$FX" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD..HEAD --no-cache >"$WORK/AA.xml" 2>"$WORK/AA.err" ); r1=$? +( cd "$FX" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD..HEAD --no-cache >"$WORK/AAp.xml" 2>"$WORK/AAp.err" ); r2=$? +if [ "$r1" = "$r2" ] && cmp -s "$WORK/AA.xml" "$WORK/AAp.xml" && [ -s "$WORK/AA.xml" ]; then ok "(G) A..A: one tree, nothing to project — answered in full, byte-identical (rc=$r1)" +else no "(G) A..A: rc $r1 vs $r2 or the answers differ"; fi + +# ── (H) --quality-kinds ─────────────────────────────────────────────────────────────────────────────────────── +qd "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dupe --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q "dupe" "$WORK/e" && grep -q "new-clone-of-reused-helper" "$WORK/e" && grep -q "short-horizon-churn" "$WORK/e"; then + ok "(H) an unknown kind is refused (exit 1) naming it and the 12 kinds" +else no "(H) unknown kind: rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi +qd "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --quality-ack --no-cache +if [ "$RC" = 1 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && grep -q -- '--quality-kinds.*--quality-ack\|--quality-ack.*--quality-kinds' "$WORK/e" && ! grep -q 'unknown flag' "$WORK/e"; then + ok "(H) --quality-kinds beside --quality-ack is refused (exit 1) naming both flags, no ledger written" +else no "(H) with --quality-ack rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi +# S3: the selector is the range form's (the bare form's MCP twin has no such field yet) — refused by name on the bare form +qd "$BIN" . --quality-delta --quality-kinds=complexity --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q -- '--quality-kinds' "$WORK/e" && grep -q 'A\.\.B' "$WORK/e" && ! grep -q 'unknown flag' "$WORK/e"; then + ok "(H) --quality-kinds on the bare (working-tree) form is refused (exit 1) naming the range form" +else no "(H) bare form: rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi +for k in api-surface complexity dead-code defect-shape duplication error-masking nesting new-clone-of-reused-helper params placeholder verbosity; do + qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$k" --no-cache + if [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" "$k" )" ] && [ -n "$( rows "$WORK/o" )" ] && [ "$( attr "$WORK/o" skipped_why )" = selector ]; then ok "(H) --quality-kinds=$k: exactly (A)'s $k rows" + else no "(H) --quality-kinds=$k: rc=$RC rows=$( rows "$WORK/o" | grep -c . ) want $( rows "$WORK/A.xml" "$k" | grep -c . ) why=$( attr "$WORK/o" skipped_why )"; fi +done +qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$ALL12" --no-cache +if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml"; then ok "(H) all 12 named: byte-identical to no selector"; else no "(H) all 12 named: rc=$RC or output differs from (A)"; fi + +# ── (I) --quality-ack: the projection never pre-empts it (C2); a real stop refuses it ─────────────────────────── +# Each run acks in its own copy of the fixture, so the ledgers compare. ackl: the ledger's ack lines (no header). +ackl(){ grep '^ack ' "$1/.ripwire_quality_acks" 2>/dev/null; } +FIA="$WORK/fia"; FIP="$WORK/fip"; FIS="$WORK/fis"; cp -R "$FX" "$FIA"; cp -R "$FX" "$FIP"; cp -R "$FX" "$FIS" +qdin "$FIA" "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache; iarc=$RC +qdin "$FIP" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache +if [ -n "$( ackl "$FIA" )" ] && [ "$RC" = "$iarc" ] && [ "$( ackl "$FIP" )" = "$( ackl "$FIA" )" ] && [ "$( memlines )" = 0 ]; then + ok "(I) --quality-ack under project:1 is not pre-empted: it acks in full, the same $( ackl "$FIA" | grep -c . ) ack line(s) as an unpressured ack (rc=$RC)" +else no "(I) project:1 --quality-ack: rc=$RC (unpressured $iarc) acks=$( ackl "$FIP" | grep -c . ) want $( ackl "$FIA" | grep -c . ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qdin "$FIS" RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache +if [ "$RC" = 5 ] && [ ! -e "$FIS/.ripwire_quality_acks" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'nothing was acked' "$WORK/e" && [ ! -s "$WORK/o" ]; then + ok "(I) --quality-ack under a real stop (crawl:12) refuses (exit 5, one line), the ledger untouched" +else no "(I) crawl:12 --quality-ack: rc=$RC ledger=$( [ -e "$FIS/.ripwire_quality_acks" ] && echo written || echo absent ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + +# ── (J) --scope naming an unchanged file under pressure ─────────────────────────────────────────────────────── +qd "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache; jA=$RC; cp "$WORK/o" "$WORK/JA.xml" +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache +if [ "$RC" != 1 ] && ! grep -q 'names nothing\|matches no indexed path' "$WORK/e" && [ -n "$( attr "$WORK/o" kinds_skipped )" ] \ + && [ "$RC" = "$( degrc "$WORK/o" )" ] && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/JA.xml" "$LOCAL_CSV" )" ]; then + ok "(J) --scope on an unchanged file under pressure: answered (rc=$RC, unpressured rc=$jA), not refused as naming nothing" +else no "(J) rc=$RC (want $( degrc "$WORK/o" ), unpressured $jA) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +# the near miss: a scope naming NO path of the target commit is still refused under pressure (a typo is not a measurement) +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/nosuch.py --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q 'matches no indexed path' "$WORK/e"; then ok "(J) a scope naming no path of the target commit is still refused under pressure (exit 1)" +else no "(J) --scope=py/nosuch.py under pressure: rc=$RC stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + +# ── (K) legend and JSON ─────────────────────────────────────────────────────────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache +missing="" +for a in kinds_skipped= skipped_why= ingest_scope= memory_need= also_skipped= kinds_next=; do + sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- "$a" || missing="$missing $a" +done +if [ -z "$missing" ] && validxml "$WORK/o"; then ok "(K) --legend=full defines every new attribute and parses as XML" +else no "(K) legend lacks:$missing (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_stop=' && validxml "$WORK/o"; then ok "(K) --legend=full over a stop defines memory_stop= and parses as XML" +else no "(K) the P0 legend lacks memory_stop= (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache +if python3 - "$WORK/o" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +assert d["kinds_skipped"] == ["dead-code", "duplication", "new-clone-of-reused-helper"], d.get("kinds_skipped") +assert d["skipped_why"] == "memory" and d["ingest_scope"] == "changed-files" +assert d["also_skipped"] == ["config-warnings", "stale-acks"] and "stale" not in d and "register-macro-excluded" not in d +assert d["kinds_next"].startswith("--quality-delta=") and "memory_need" in d and "memory_limit" in d +PY +then ok "(K) --json carries the twins (kinds_skipped list, skipped_why, ingest_scope, also_skipped, kinds_next, memory_*)" +else no "(K) --json twins missing or wrong: $( head -c 300 "$WORK/o" )"; fi +qd "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache +if [ "$RC" = "$ARC" ] && [ -s "$WORK/o" ] && ! grep -q '"kinds_skipped"\|"ingest_scope"\|"kinds_next"\|"also_skipped"\|"memory_' "$WORK/o"; then + ok "(K) an unpressured --json carries none of the degraded keys" +else no "(K) unpressured --json: rc=$RC (want $ARC), empty, or it carries a degraded-only key"; fi + +# ── (L) guard: the projection seam past the run's one projection reading changes nothing ─────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:2 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml" && [ "$( grep -v '^ripwire: cache-stats ' "$WORK/e" )" = "$( grep -v '^ripwire: cache-stats ' "$WORK/A.err" )" ]; then + ok "(L) project:2 (past the one projection reading) is byte-identical to no seam (rc=$RC)" +else no "(L) project:2 rc=$RC (want $ARC) or the answer differs"; fi + +# ══ round 2 — the gates review's R1-R9 ═══════════════════════════════════════════════════════════════════════════ + +# ── (M) R1: a new defect site whose text also sits in an UNCHANGED file ───────────────────────────────────────── +# defect-shape counts a site as new by its REPO-WIDE excess. Pairing a full base with a changed-files target would net the +# new copy against the unchanged one and lose a gating row; both sides' changed files cancel the unchanged copy exactly. +FM="$WORK/fm"; mkfx "$FM" +printf 'def old():\n return "x {} {}".format(1)\n' >"$FM/py/old.py"; g "$FM" add -A; g "$FM" commit -qm old +printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {}".format(name)\n\ndef dup():\n return "x {} {}".format(1)\n' >"$FM/py/mod.py" +g "$FM" add -A; g "$FM" commit -qm dup +qdin "$FM" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/MA.xml"; mrc=$RC +if [ "$mrc" = 2 ] && rows "$WORK/MA.xml" defect-shape | grep -q 'gating="1"'; then + for seam in project:1 crawl:12; do + qdin "$FM" RIPWIRE_TEST_MEMGUARD=$seam "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache + if [ -n "$( attr "$WORK/o" kinds_skipped )" ] && [ "$RC" = 2 ] && [ "$( rows "$WORK/o" defect-shape )" = "$( rows "$WORK/MA.xml" defect-shape )" ]; then + ok "(M) $seam: the new site's gating defect-shape row survives beside an unchanged copy of its text (rc=2)" + else no "(M) $seam: rc=$RC (want 2) kinds_skipped=$( attr "$WORK/o" kinds_skipped ) defect rows: $( rows "$WORK/o" defect-shape | head -c 200 )"; fi + done +else + no "(M) premise: the unpressured answer has no gating defect-shape row (rc=$mrc)" +fi + +# ── (N) R2: paths git diff does not list but an archive or a crawl treats differently ────────────────────────── +refused_by(){ # $1 label, $2 repo, $3 the word the one line must name + qdin "$2" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache + if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q "^ripwire: .*$3" "$WORK/e" && grep -q 'changed files' "$WORK/e"; then + ok "(N) $1: the changed files cannot stand in for the trees — refused by name (exit 5, one line naming $3)" + else no "(N) $1: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +} +same_as_full(){ # $1 label, $2 repo: the degraded LOCAL rows equal the unpressured ones + ( cd "$2" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nfA.xml" 2>/dev/null ) + qdin "$2" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache + if [ -n "$( attr "$WORK/o" kinds_skipped )" ] && [ "$RC" = "$( degrc "$WORK/o" )" ] && [ -n "$( rows "$WORK/nfA.xml" "$LOCAL_CSV" )" ] \ + && [ "$( rows "$WORK/o" "$LOCAL_CSV" )" = "$( rows "$WORK/nfA.xml" "$LOCAL_CSV" )" ]; then + ok "(N) $1: degraded (rc=$RC), LOCAL rows equal the unpressured run" + else no "(N) $1: rc=$RC kinds_skipped=$( attr "$WORK/o" kinds_skipped ) diff: $( diff <( rows "$WORK/nfA.xml" "$LOCAL_CSV" ) <( rows "$WORK/o" "$LOCAL_CSV" ) | head -c 300 ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 200 )"; fi +} +# an UNCHANGED symlink to a CHANGED file: the crawl follows it, so the full answer rows the link's path too +FS="$WORK/fs"; mkfx "$FS"; ln -s py/mod.py "$FS/link.py"; g "$FS" add -A; g "$FS" commit -qm link +printf 'def other():\n # TODO: link\n return 1\n' >"$FS/py/mod.py"; g "$FS" add -A; g "$FS" commit -qm todo +refused_by "an unchanged symlink to a changed file" "$FS" "symlink" +# an export-ignore FLIP: the target deletes .gitattributes, so gen/x.py (unchanged) appears in the target archive +FA="$WORK/fa"; mkfx "$FA"; mkdir -p "$FA/gen"; printf 'gen/** export-ignore\n' >"$FA/.gitattributes" +printf 'def gen():\n # TODO: generated\n return 1\n' >"$FA/gen/x.py"; g "$FA" add -A; g "$FA" commit -qm attrs +g "$FA" rm -q .gitattributes; printf 'def other():\n # TODO: flip\n return 1\n' >"$FA/py/mod.py"; g "$FA" add -A; g "$FA" commit -qm flip +refused_by "an export-ignore flip (.gitattributes deleted)" "$FA" ".gitattributes" +# export-subst rewrites archived bytes of files git calls unchanged +FU="$WORK/fu"; mkfx "$FU"; printf 'py/*.py export-subst\n' >"$FU/.gitattributes"; g "$FU" add -A; g "$FU" commit -qm subst +printf 'def other():\n # TODO: subst\n return 1\n' >"$FU/py/mod.py"; g "$FU" add -A; g "$FU" commit -qm todo +refused_by "an export-subst attribute" "$FU" "export-subst" +# the near miss: an UNCHANGED export-ignore attribute excludes the same paths from both sides — no refusal +FI="$WORK/fi"; mkfx "$FI"; mkdir -p "$FI/gen"; printf 'gen/** export-ignore\n' >"$FI/.gitattributes" +printf 'def gen():\n # TODO: generated\n return 1\n' >"$FI/gen/x.py"; g "$FI" add -A; g "$FI" commit -qm attrs +printf 'def gen():\n # TODO: generated\n # TODO: again\n return 1\n' >"$FI/gen/x.py" +printf 'def other():\n # TODO: kept\n return 1\n' >"$FI/py/mod.py"; g "$FI" add -A; g "$FI" commit -qm todo +same_as_full "an unchanged export-ignore attribute (a changed export-ignored file)" "$FI" +# a directory the crawl prunes because it holds CMakeCache.txt (unchanged), with a changed file inside it +FC="$WORK/fc"; mkfx "$FC"; mkdir -p "$FC/bld"; printf 'CMAKE_BUILD_TYPE:STRING=Debug\n' >"$FC/bld/CMakeCache.txt" +printf 'def built():\n return 1\n' >"$FC/bld/x.py"; g "$FC" add -A; g "$FC" commit -qm bld +printf 'def built():\n # TODO: pruned\n return 1\n' >"$FC/bld/x.py" +printf 'def other():\n # TODO: seen\n return 1\n' >"$FC/py/mod.py"; g "$FC" add -A; g "$FC" commit -qm todo +same_as_full "a changed file in a CMakeCache.txt-pruned directory" "$FC" +if grep -q 'bld/x.py' "$WORK/nfA.xml" || grep -q 'bld/x.py' "$WORK/o"; then no "(N) CMakeCache premise/result: a row names the pruned bld/x.py" +else ok "(N) CMakeCache: neither answer rows the pruned bld/x.py"; fi +# the export-attribute read FAILED (CodeRabbit, #413): a git whose `grep` exits 128 leaves the attribute question unanswered, +# and "could not read" is not "no attribute" — the degraded path refuses (exit 1, the git-could-not-list line), never a +# changed-files answer. The shim fails only `grep`; everything else reaches the real git. +GSHIM="$WORK/gshim"; mkdir -p "$GSHIM"; REALGIT="$( command -v git )" +printf '#!/bin/sh\nfor a in "$@"; do [ "$a" = grep ] && exit 128; done\nexec "%s" "$@"\n' "$REALGIT" >"$GSHIM/git"; chmod +x "$GSHIM/git" +qdin "$FU" PATH="$GSHIM:$PATH" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q '^ripwire: .*git could not list the changed files' "$WORK/e"; then + ok "(N) the export-attribute grep failed (exit 128): refused, never read as 'no attribute' (exit 1, nothing on stdout)" +else no "(N) a failed export-attribute grep: rc=$RC (want 1) stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +# its near miss, no shim: a pair with no .gitattributes at all, where git grep exits 1 (no hit) — still a clean answer +qdin "$FC" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ -n "$( attr "$WORK/o" kinds_skipped )" ] && [ "$RC" = "$( degrc "$WORK/o" )" ]; then ok "(N) control: exit 1 from git grep (no hit) still answers degraded (rc=$RC)" +else no "(N) control: rc=$RC kinds_skipped=$( attr "$WORK/o" kinds_skipped ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 200 )"; fi + +# ── (O) R3: ack identity healing reads tree-wide uniqueness, which the changed files cannot see ───────────────── +# Legacy-scheme ack key of a scope-less name = fnv1a64( bare name ) (quality.h addSchemeAliases: canonicalId degrades to the +# bare name). The full run re-keys it only when ONE file defines the name; two definitions poison it. +mkacks(){ printf '# ripwire quality acks v1\nack placeholder %s 5 legacy-scheme row\n' "$( fnv dupname )" >"$1/.ripwire_quality_acks"; } +# the finding rows () of a kind — stale-ack rows are a whole-answer fact the degraded answer names as skipped +frows(){ rows "$1" "$2" | grep '^"$FO/py/a.py"; printf 'def dupname():\n return 2\n' >"$FO/py/b.py" +g "$FO" add -A; g "$FO" commit -qm two; printf 'def dupname():\n # TODO: one\n return 1\n' >"$FO/py/a.py"; g "$FO" add -A; g "$FO" commit -qm todo; mkacks "$FO" +qdin "$FO" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/OA.xml" +qdin "$FO" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ -n "$( frows "$WORK/OA.xml" placeholder )" ] && [ "$( frows "$WORK/o" placeholder )" = "$( frows "$WORK/OA.xml" placeholder )" ] \ + && [ "$( attr "$WORK/o" acked )" = "$( attr "$WORK/OA.xml" acked )" ] && case ",$( attr "$WORK/o" also_skipped )," in *,ack-healing,*) true;; *) false;; esac; then + ok "(O) a legacy ack on a name two files define (one changed): the same placeholder rows and acked= as the full run; ack-healing named as skipped" +else no "(O) two definitions: rows full=$( frows "$WORK/OA.xml" placeholder | grep -c . ) degraded=$( frows "$WORK/o" placeholder | grep -c . ) acked $( attr "$WORK/OA.xml" acked )/$( attr "$WORK/o" acked ) also_skipped=$( attr "$WORK/o" also_skipped )"; fi +FO1="$WORK/fo1"; mkfx "$FO1"; printf 'def dupname():\n return 1\n' >"$FO1/py/a.py" +g "$FO1" add -A; g "$FO1" commit -qm one; printf 'def dupname():\n # TODO: one\n return 1\n' >"$FO1/py/a.py"; g "$FO1" add -A; g "$FO1" commit -qm todo; mkacks "$FO1" +qdin "$FO1" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/O1A.xml" +qdin "$FO1" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$( attr "$WORK/O1A.xml" acked )" = 1 ] && [ -z "$( frows "$WORK/O1A.xml" placeholder )" ] && [ -n "$( frows "$WORK/o" placeholder )" ] \ + && case ",$( attr "$WORK/o" also_skipped )," in *,ack-healing,*) true;; *) false;; esac && [ "$RC" = "$( degrc "$WORK/o" )" ]; then + ok "(O) a legacy ack on a name one file defines: the full run re-keys it; the degraded run names ack-healing as skipped and shows the row (rc=$RC)" +else no "(O) one definition: full acked=$( attr "$WORK/O1A.xml" acked ) rows full=$( frows "$WORK/O1A.xml" placeholder | grep -c . ) degraded=$( frows "$WORK/o" placeholder | grep -c . ) also_skipped=$( attr "$WORK/o" also_skipped ) rc=$RC"; fi +# no ledger: nothing to heal, so ack-healing is never named +if [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ]; then ok "(O) with no ledger, ack-healing is not named (nothing to heal)" +else no "(O) no ledger: also_skipped=$( attr "$WORK/B.xml" also_skipped )"; fi + +# ── (P) R4: a changed-files ingest never reads or writes an ingest cache ─────────────────────────────────────── +# The target is HEAD, whose full ingest uses the HEAD ingest-cache family. Run with the cache ON. +PT="$WORK/ptmp"; mkdir -p "$PT" +blobsum(){ find "$PT" -type f -name '*.bin' -exec cksum {} + 2>/dev/null | sort; } +( cd "$FX" && TMPDIR="$PT" "$BIN" . --quality-delta=HEAD~1..HEAD >"$WORK/P1.xml" 2>/dev/null ); p1=$? +s1="$( blobsum )" +( cd "$FX" && TMPDIR="$PT" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD >"$WORK/P2.xml" 2>/dev/null ); p2=$? +s2="$( blobsum )" +( cd "$FX" && TMPDIR="$PT" "$BIN" . --quality-delta=HEAD~1..HEAD >"$WORK/P3.xml" 2>/dev/null ); p3=$? +if [ -n "$s1" ] && [ "$p1" = "$ARC" ] && cmp -s "$WORK/P1.xml" "$WORK/A.xml"; then + if [ -n "$( attr "$WORK/P2.xml" kinds_skipped )" ] && [ "$p2" = "$( degrc "$WORK/P2.xml" )" ] && [ "$s1" = "$s2" ] && [ "$p3" = "$ARC" ] && cmp -s "$WORK/P3.xml" "$WORK/A.xml"; then + ok "(P) P1 with the cache on: degraded (rc=$p2), every cache blob unchanged, and the next cached run is byte-identical to (A)" + else no "(P) p2=$p2 degraded=$( attr "$WORK/P2.xml" ingest_scope ) blobs_unchanged=$( [ "$s1" = "$s2" ] && echo yes || echo no ) p3=$p3 identical=$( cmp -s "$WORK/P3.xml" "$WORK/A.xml" && echo yes || echo no )"; fi +else + no "(P) premise: the cached unpressured run wrote no blob or differs from (A) (rc=$p1, blobs: $( echo "$s1" | grep -c . ))" +fi + +# ── (Q) R5: the target phase stops after the projection said it fits ─────────────────────────────────────────── +# ingest:2 cuts the process's 2nd ingest (the target's full tree) at its first crawl entry: that ingest parses nothing and +# so prints no cache-stats line — 3 lines (base in full, both changed-file sets), and the stderr line names the target. +qd RIPWIRE_TEST_MEMGUARD=ingest:2 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/Q.xml" +if [ "$( attr "$WORK/Q.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/Q.xml" memory_stop )" = crawl ] && [ "$( attr "$WORK/Q.xml" memory_need )" = unmeasured ] \ + && [ "$( ingests )" = 3 ] && grep -q '^ripwire: memory limit .*stopped the crawl of the target commit' "$WORK/e" \ + && [ "$RC" = "$( degrc "$WORK/Q.xml" )" ] && [ "$( rows "$WORK/Q.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/A.xml" "$LOCAL_CSV" )" ]; then + ok "(Q) a stop in the target's full ingest falls to the changed files (rc=$RC), LOCAL rows equal (A); defect-shape kept (the base's macro set was read)" +else no "(Q) rc=$RC kinds_skipped=$( attr "$WORK/Q.xml" kinds_skipped ) stop=$( attr "$WORK/Q.xml" memory_stop ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + +# ── (R) R7: a peak over the cap is said; a replay with nothing it can run refuses ────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +pk="$( attr "$WORK/o" memory_peak )" +if [ "$RC" = "$ARC" ] && isnum "${pk%M}" && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" )" ] && ! hasattr "$WORK/o" ingest_scope \ + && [ "$( grep -c '^ripwire: memory peak ' "$WORK/e" )" = 1 ] && ! hasattr "$WORK/A.xml" memory_peak; then + ok "(R) a full answer whose peak went over the limit says so (memory_peak=\"$pk\" + one stderr line); rows and rc unchanged" +else no "(R) peak:1 rc=$RC (want $ARC) memory_peak=$pk stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_peak=' && validxml "$WORK/o"; then ok "(R) --legend=full defines memory_peak= when it rides, and parses as XML" +else no "(R) the legend lacks memory_peak= (or invalid XML)"; fi +# CodeRabbit #413: the peak sentence speaks for a changed-files answer too, never "complete" unconditionally. A degraded +# answer whose peak also went over is not reachable through one seam, so the wording is pinned here and the stderr +# twin keeps "the answer is complete" on the full answer above. +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_peak=[^.]*(complete, or the changed-files answer ingest_scope= names)'; then + ok "(R) the memory_peak= legend covers both a complete and a changed-files answer" +else no "(R) the memory_peak= legend still reads as complete-only"; fi +qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +grep -q '^ripwire: memory peak .*: the answer is complete, but' "$WORK/e" \ + && ok "(R) a full answer's peak line still says the answer is complete" \ + || no "(R) the full answer's peak line: $( grep '^ripwire: memory peak' "$WORK/e" | head -c 200 )" +# A selection the changed files could not answer (whole-tree kinds, with or without defect-shape) is never pre-empted by +# the projection — it is the replay kinds_next= names, and degrading it could only refuse: it runs in full. A real stop +# still refuses it, by name, and so does a defect-shape-only selection whose changed files cannot decide defect-shape +# (found on this repository's own HEAD~20..HEAD: the replay degraded again and answered EMPTY with exit 6). +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$WHOLE_KINDS" --no-cache +if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'whole-tree' "$WORK/e"; then + ok "(R) a whole-tree-only selection under a stop refuses (exit 5, one line naming why) — never an empty pass" +else no "(R) whole-tree-only selection under a stop: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=defect-shape --no-cache +if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'defect-shape' "$WORK/e"; then + ok "(R) a defect-shape-only selection the changed files cannot decide (a changed C++ file, no whole macro set) refuses (exit 5) — never an empty pass" +else no "(R) defect-shape-only under a stop: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +REPLAY_KINDS="dead-code,defect-shape,duplication,new-clone-of-reused-helper" +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$REPLAY_KINDS" --no-cache +if [ -s "$WORK/o" ] && ! hasattr "$WORK/o" ingest_scope && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" "$REPLAY_KINDS" )" ] && [ -n "$( rows "$WORK/o" )" ] \ + && { [ "$RC" = 0 ] || [ "$RC" = 2 ]; }; then + ok "(R) the replay's selection is not pre-empted by the projection (project:1): answered in full, exactly (A)'s rows of those kinds (rc=$RC)" +else no "(R) replay selection under project:1: rc=$RC scope=$( attr "$WORK/o" ingest_scope ) rows=$( rows "$WORK/o" | grep -c . ) want $( rows "$WORK/A.xml" "$REPLAY_KINDS" | grep -c . )"; fi + +# ── (S) R9: stale acks under --quality-kinds are checked for the selected kinds only ─────────────────────────── +FSS="$WORK/fss"; cp -R "$FX" "$FSS" +( cd "$FSS" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --ack-only=dead-code --no-cache >/dev/null 2>&1 ) +if grep -q '^ack dead-code' "$FSS/.ripwire_quality_acks" 2>/dev/null; then + qdin "$FSS" "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --show-stale --no-cache + if { [ "$RC" = 0 ] || [ "$RC" = 2 ]; } && [ -s "$WORK/o" ] && [ -n "$( rows "$WORK/o" complexity )" ] && ! grep -q ']*kind="dead-code' "$WORK/o"; then + ok "(S) --quality-kinds=complexity: a dead-code ack is not reported finding-gone (stale acks checked for the selected kinds only; rc=$RC)" + else no "(S) rc=$RC sa rows: $( grep -o ']*>' "$WORK/o" | head -c 300 )"; fi +else + no "(S) premise: --quality-ack --ack-only=dead-code wrote no dead-code ack" +fi + +# ── (T) S2: a limit near (above) the measured peak answers in full ───────────────────────────────────────────── +peak_mb="" +if /usr/bin/time -l true >/dev/null 2>&1; then + ( cd "$FX" && TMPDIR="$WORK/tmp" /usr/bin/time -l "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >/dev/null 2>"$WORK/t.err" ) + peak_mb="$( awk '/peak memory footprint/ { printf "%d", $1 / 1048576 + 1 }' "$WORK/t.err" )" + [ -n "$peak_mb" ] || peak_mb="$( awk '/maximum resident set size/ { printf "%d", $1 / 1048576 + 1 }' "$WORK/t.err" )" +elif /usr/bin/time -v true >/dev/null 2>&1; then + ( cd "$FX" && TMPDIR="$WORK/tmp" /usr/bin/time -v "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >/dev/null 2>"$WORK/t.err" ) + peak_mb="$( awk -F: '/Maximum resident set size/ { printf "%d", $2 / 1024 + 1 }' "$WORK/t.err" )" +fi +if isnum "$peak_mb"; then + lim=$(( peak_mb * 2 )); [ "$lim" -lt 64 ] && lim=64 + qd "$BIN" . --quality-delta=HEAD~1..HEAD "--max-memory=${lim}M" --no-cache + if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml"; then ok "(T) --max-memory=${lim}M (2x the measured ${peak_mb}M peak): answered in full, byte-identical to (A)" + else no "(T) --max-memory=${lim}M rc=$RC scope=$( attr "$WORK/o" ingest_scope ) — the projection degraded a run that fits"; fi +else + printf ' SKIP (T) no /usr/bin/time that reports a peak (-l or -v) — the near-the-line arm needs a measured peak\n' +fi + +# ── (V) R8: a degraded answer with rows and no gating row exits 6 ────────────────────────────────────────────── +FV="$WORK/fv"; mkfx "$FV"; printf 'def other():\n # TODO: later\n return 1\n' >"$FV/py/mod.py"; g "$FV" add -A; g "$FV" commit -qm todo +qdin "$FV" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$RC" = 6 ] && [ "$( attr "$WORK/o" gating )" = 0 ] && [ -n "$( rows "$WORK/o" placeholder )" ] && [ -n "$( attr "$WORK/o" kinds_skipped )" ]; then + ok "(V) a degraded answer with a placeholder row and gating=0 exits 6 (partial, nothing gating), stdout non-empty" +else no "(V) rc=$RC gating=$( attr "$WORK/o" gating ) rows=$( rows "$WORK/o" | grep -c . )"; fi + +# ── (W) C1: a selector's exclusions are not memory cuts ───────────────────────────────────────────────────────── +others(){ echo "$ALL12" | tr ',' '\n' | grep -vx -e "$1" -e "${2:-}" | paste -sd, -; } # the 12 kinds without $1 (and $2) +emptyskip(){ grep -q 'skipped *$' "$WORK/e"; } +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --no-cache +if [ "$( attr "$WORK/o" ingest_scope )" = changed-files ] && [ "$( attr "$WORK/o" kinds_skipped )" = "$( others complexity )" ] \ + && [ "$( attr "$WORK/o" skipped_why )" = selector ] && ! hasattr "$WORK/o" memory_skipped && ! hasattr "$WORK/o" kinds_next \ + && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" complexity )" ] && [ "$RC" = "$( degrc "$WORK/o" )" ] \ + && ! emptyskip && [ "$( memlines )" = 1 ] && grep -q '^ripwire: memory limit .*every selected kind ran' "$WORK/e"; then + ok "(W) project:1 --quality-kinds=complexity: the 11 left out are skipped_why=selector, no memory share, no kinds_next=, stderr says every selected kind ran (rc=$RC)" +else no "(W) complexity: rc=$RC why=$( attr "$WORK/o" skipped_why ) skipped=$( attr "$WORK/o" kinds_skipped ) next=$( attr "$WORK/o" kinds_next ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | od -c | tail -n 3 | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --no-cache +wnext="$( attr "$WORK/o" kinds_next )" +if [ "$( attr "$WORK/o" ingest_scope )" = changed-files ] && [ "$( attr "$WORK/o" kinds_skipped )" = "$( others complexity )" ] \ + && [ "$( attr "$WORK/o" skipped_why )" = selector,memory ] && [ "$( attr "$WORK/o" memory_skipped )" = dead-code ] \ + && case "$wnext" in "--quality-delta=$BASE..$TGT --quality-kinds=dead-code --max-memory="*M) true;; *) false;; esac \ + && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" complexity )" ] && ! emptyskip \ + && grep -q '^ripwire: memory limit .*; skipped dead-code — run them with ' "$WORK/e"; then + ok "(W) project:1 --quality-kinds=complexity,dead-code: skipped_why=selector,memory, memory_skipped=dead-code = exactly what kinds_next= replays" +else no "(W) complexity,dead-code: why=$( attr "$WORK/o" skipped_why ) memory_skipped=$( attr "$WORK/o" memory_skipped ) next='$wnext' stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --legend=full --no-cache +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q 'memory_skipped=' && validxml "$WORK/o"; then ok "(W) --legend=full defines memory_skipped= when it rides, and parses as XML" +else no "(W) the legend lacks memory_skipped= (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --legend=full --no-cache +if [ -s "$WORK/o" ] && ! sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q 'memory_skipped=' && validxml "$WORK/o"; then ok "(W) present-only: no memory_skipped= sentence when only the selector skipped" +else no "(W) the selector-only legend defines memory_skipped= (or invalid XML / no output)"; fi +# (no --json arm: --json is refused beside --quality-kinds, so memory_skipped= — which rides only beside a selector — has +# no JSON surface yet; the JSON twin is written by the same kindsDisclosure as the XML) + +[ "$fail" = 0 ] && echo "qddegradecheck: ALL PASS" || echo "qddegradecheck: FAIL" +exit "$fail" diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 189e81021..023caa705 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -82085ad1d2fbf866750fb7e3f928f611464f8ae1227e4453c9b6e12d6af122de +279718355de35b8e257c122756f07f97a0ccaa0106eac9adbb811668365ba2db diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 16ef175af..59b32e52d 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,12 +34,30 @@ 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-15, train 28 (grep-carrier, ruby-pair, qd-memory-degrade, githarden-speed, git-child-hardening, PR #380, forbudget-soft, +# for-body-slot, attrib-multidef, declined-sites merged onto main 1bcb6540): RE-DERIVED ONCE with UPDATE_GOLDEN=1 on the merged +# tree (hash 279718355d…5ba2db). Watched text: kParserVer 173 -> 176 (main's slice 4a 173 / rich 174, lane grep-carrier 173, +# lane ruby-pair 174 / rich 175, PR #380 145; 176 / rich file tag 177 is above every live build), kCacheVersion 31 -> 32 +# (RefRole gained Subscript 9 and Inert 10; see the kCacheVersion note), quality.h's two mirrors with them, kQSnapCacheScheme +# 20 -> 21 (the dead set's call inputs moved; see the v21 note), and the lanes' own watched-text moves logged below +# (qd-memory-degrade's computeSnapshot / defectSitesOf). # 2026-10-15, slice 4a (#400, Ruby mixins onto the shared base-relations path): RE-DERIVED with UPDATE_GOLDEN=1. # The only watched text that moved is the kParserVer declaration (and its mirror): 171 -> 173. Ruby's # `include`/`extend`/`prepend` are captured by queries/ruby/tags.scm as @reference.mixin and read by # captureTagsFacts instead of the retired captureRubyMixinBases; output is byte-identical, but the extraction # path changed, so the number moves. 173, not 172: 172 is 171's rich file tag. # kCacheVersion 31 and kQSnapCacheScheme 20 are unchanged. +# 2026-10-14, lane/ruby-pair (train 28): RE-PINNED with UPDATE_GOLDEN=1. The only watched text that moved is the kParserVer +# declaration (and its mirror): 171 -> 174 (Ruby/Python subscript references and the Ruby main-object mixin binding; 172 is +# 171's rich file tag; the train renumbers). kCacheVersion 31 and kQSnapCacheScheme 20 are unchanged: what a cached Snapshot +# represents is the same, and the parser version already keys every extraction fact it holds. +# 2026-10-14, lane/qd-memory-degrade: RE-DERIVED with UPDATE_GOLDEN=1. Manifest text moved in computeSnapshot (a KindScope +# parameter: the dead set and the clone/construct/defect/body passes run only for the kinds asked; the dead set moved to +# addDeadSet, same keys, sorted as before) and defectSitesOf (cFamilyMacroNames factored out; an optional macro override). +# Every Snapshot a qsnap/qheadsnap blob holds is computed with the default scope (all kinds, no override), which builds +# the same records in the same sorted order, so what a cached Snapshot means is unchanged: kQSnapCacheScheme 20, +# kCacheVersion 31 and kParserVer stay. The forward declaration's default is spelled KindScope() — braces there read as a +# body to extract_fn. # 2026-10-13, PR #349 lands (maintainer renumber commit on the contributor's branch): RE-DERIVED with UPDATE_GOLDEN=1. # The only watched text that moved is the kParserVer declaration (and its mirror): provisional 158 -> 171. The branch # built 156, 158 and 169 (rich file tag 170); main is 167 (rich file tag 168); 171 / rich file tag 172 is above all of @@ -114,6 +132,10 @@ PIN="$ROOT/test/qschemetrip.hash" # 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-05, issue #357 lane: RE-DERIVED with UPDATE_GOLDEN=1. kParserVer 143 -> 145 for +# Elixir quote live holes and counted inert call candidates (144 is main's rich-family tag; +# train renumbers). quality.h's parser mirror moved with it. kQSnapCacheScheme stays 17: +# Snapshot meaning is unchanged and its key carries the extraction identity. # 2026-10-04, train 25 review fixes: RE-DERIVED with UPDATE_GOLDEN=1 (hash 32240f4552…2dafcc). kParserVer 141 -> 143 (two # extraction changes: a declaration named like the JS global object is a JsShadow binding; value-reference slot text # is cut on a UTF-8 boundary and JS string keys are capped) with quality.h's mirror; 143 because 141's full-use file diff --git a/test/readmedriftcheck.sh b/test/readmedriftcheck.sh index 68db6a434..e26b079f0 100755 --- a/test/readmedriftcheck.sh +++ b/test/readmedriftcheck.sh @@ -103,12 +103,14 @@ fi # the core sentence, not the reference guide's restatement of it in §5. That is E9's failure one arm # over: a second copy of a gated number, free to drift alone. EVERY occurrence is checked now, and a # failure names the line. The loop reads a here-doc, not a pipeline, so `no` sets fail in THIS shell. -b_lines="$( grep -nE '[0-9]+ long flags advertised' "$README" || true )" +# 2026-10-16 (CodeRabbit, #413): the section summary `N long flags` restates it too, and drifted (190 vs 192) — +# it is a third site, read by the same loop. +b_lines="$( grep -nE '[0-9]+ long flags( advertised|)' "$README" || true )" b_seen=0; b_bad="" while IFS= read -r b_ln; do [ -z "$b_ln" ] && continue b_seen=$(( b_seen + 1 )) - b_num="$( printf '%s' "$b_ln" | grep -oE '[0-9]+ long flags advertised' | head -1 | grep -oE '^[0-9]+' )" + b_num="$( printf '%s' "$b_ln" | grep -oE '[0-9]+ long flags( advertised|)' | head -1 | grep -oE '^[0-9]+' )" [ "$b_num" = "$derived" ] || b_bad="$b_bad README.md:${b_ln%%:*}(states $b_num)" done < long flags advertised' site(s) in README.md state $derived, matching the derived count" + ok "(B) all $b_seen ' long flags advertised' / 'N long flags' site(s) in README.md state $derived, matching the derived count" fi # ── (C) mutation control — a wrong count in a COPY must be caught ─────────────────────────────────── diff --git a/test/recallshapecheck.sh b/test/recallshapecheck.sh index f55584c5b..35394f9a6 100755 --- a/test/recallshapecheck.sh +++ b/test/recallshapecheck.sh @@ -68,6 +68,10 @@ # a row (REG#arg0). # F6 a class used as a value (`{"x": MyClass}`) is out of scope: functions and methods only. # +# OP (L4, train 28): the walk climbs value-preserving operators (ingest_valuerefs.h kVrPassOps) — JS/TS ?? || &&, +# Python or/and, the JS logical assignments — with near misses (the left operand of && / and, C ||, a non-function +# name, a same-named parameter) that a looser walk would turn into rows. +# # MUTATION PROOF (checklist 1; sim/refval_mutate.sh, measured at the implementation head — the pre-registered list was # corrected to what each mutant measured): disabling the local/parameter shadow guard turns C5 C14 C2f C2g J2 J2g T2 P2 # P3 P2i G3 X3 red; disabling the keyed-element / keyword-LABEL guard turns P10 P12 P13 G7 G9 red (a C designated field @@ -1308,6 +1312,93 @@ arm "Z3 uses hot: 300 role=value sites, paged by the verb's own window (shown=10 arm_roots "Z4 multi-root (big c) callers hot: the same window on a two-root run" "big c" --callers=hot \ attr:value_refs=300 el:vrs:total=300 el:vrs:shown=64 el:vrs:capped=1 nvr:64 +# ── OP (L4, train 28): value-preserving operators — `a ?? f`, `a || f`, `c && f` (JS/TS), `a or f`, `c and f` (Python), +# and the JS logical assignments `x ??= f` — land f in the slot the whole expression fills, in assignment, return, +# argument and parameter-default positions. A function on the LEFT of && / and only decides (the result is the right +# operand): no row. C/C++ and Go || / && yield a truth value: no row. Ternaries and parentheses were already walked. ── +echo "-- value-preserving operators" +mkdir -p "$FX/opjs" "$FX/opts" "$FX/oppy" "$FX/opc" +cat >"$FX/opjs/a.js" <<'EOF' +function getPath(r) { return r; } +function noStrict(r) { return r; } +function defH() { return 1; } +function andH() { return 2; } +function andLeft() { return 3; } +function argH() { return 4; } +function laH() { return 5; } +function orH() { return 6; } +const plainVal = 3; +class App { + constructor(options, strict) { + this.getPath = (strict ?? true) ? (options.getPath ?? getPath) : noStrict; // @OJ_HONO + } +} +function withDefault(cb = opts.cb || defH) { return cb(); } // @OJ_DEF +function pick(flag) { return flag && andH; } // @OJ_RET +function leftAnd(flag) { return andLeft && flag; } // @OJ_ANDL +function reg(cfg) { return register(cfg.h ?? argH); } // @OJ_ARG +function logical(slot) { let s = slot; s ??= laH; return s; } // @OJ_LA +const t = orH || other; // @OJ_ORL +function negPlain(cfg) { const v = cfg.x ?? plainVal; return v; } // @OJ_NEG +function negShadow(getPath) { return other ?? getPath; } // @OJ_SHADOW +EOF +cat >"$FX/opts/a.ts" <<'EOF' +function tsH(): number { return 1; } +export function pickTs(o: { h?: () => number }) { return o.h ?? tsH; } // @OT_RET +export class Holder { run: () => number = (globalThis as any).x || tsH; } // @OT_FIELD +EOF +cat >"$FX/oppy/m.py" <<'EOF' +def py_h(): + return 1 +def py_and(): + return 2 +def py_left(): + return 3 +def py_def(): + return 4 +def py_arg(): + return 5 +PLAIN = 3 +def choose(o): + return o.h or py_h # @OP_OR +def gate(flag): + return flag and py_and # @OP_AND +def left_and(flag): + return py_left and flag # @OP_ANDL +def with_default(cb=None or py_def): # @OP_DEF + return cb() +register(None or py_arg) # @OP_ARG +def neg(o): + return o.x or PLAIN # @OP_NEG +EOF +cat >"$FX/opc/c.c" <<'EOF' +static int c_h(int v) { return v; } +static int c_t(int v) { return v + 1; } +int use_or(int f) { return f || c_h; } /* @OC_OR */ +int (*use_ternary(int f))(int) { return f ? c_t : 0; } /* @OC_TERN */ +EOF +arm "OP1 callers getPath: the hono shape — getPath inside ?? inside a ternary branch lands in this.getPath" \ + opjs --callers=getPath attr:value_refs=1 'vr:bind=@OJ_HONO;into=this.getPath;in_id=constructor' 'novr:bind=@OJ_SHADOW' +arm "OP1b callers noStrict (control): the ternary alternative was already a row" \ + opjs --callers=noStrict attr:value_refs=1 'vr:bind=@OJ_HONO;into=this.getPath' +arm "OP2 callers defH: a parameter default through || (JS)" opjs --callers=defH attr:value_refs=1 'vr:bind=@OJ_DEF;into=withDefault#cb' +arm "OP3 callers andH: the right operand of && is returned" opjs --callers=andH attr:value_refs=1 'vr:bind=@OJ_RET;into=(return)' +arm "OP4 callers andLeft (near miss): the LEFT operand of && only decides — no row" opjs --callers=andLeft attr:count=0 noattr:value_refs nvr:0 +arm "OP5 callers argH: an argument through ??" opjs --callers=argH attr:value_refs=1 'vr:bind=@OJ_ARG;into=register#arg0' +arm "OP6 callers laH: a logical assignment ??= stores the right operand" opjs --callers=laH attr:value_refs=1 'vr:bind=@OJ_LA;into=s' +arm "OP7 callers orH: the LEFT operand of || can be the result" opjs --callers=orH attr:value_refs=1 'vr:bind=@OJ_ORL;into=t' +arm "OP8 callees negPlain (negative): a non-function identifier in ?? is no value reference" opjs --callees=negPlain attr:count=0 noattr:value_refs nvr:0 +arm "OP9 callers tsH (TypeScript): a return through ??; FLOOR: a class FIELD initialiser is no value position at all (operator or not)" \ + opts --callers=tsH attr:value_refs=1 'vr:bind=@OT_RET;into=(return)' 'novr:bind=@OT_FIELD' +arm "OP10 callers py_h (Python): the right operand of or is returned" oppy --callers=py_h attr:value_refs=1 'vr:bind=@OP_OR;into=(return)' +arm "OP11 callers py_and (Python): the right operand of and is returned" oppy --callers=py_and attr:value_refs=1 'vr:bind=@OP_AND;into=(return)' +arm "OP12 callers py_left (near miss): the LEFT operand of and only decides — no row" oppy --callers=py_left attr:count=0 noattr:value_refs nvr:0 +arm "OP13 callers py_def (Python): a parameter default through or" oppy --callers=py_def attr:value_refs=1 'vr:bind=@OP_DEF;into=with_default#cb' +arm "OP14 callers py_arg (Python): an argument through or" oppy --callers=py_arg attr:value_refs=1 'vr:bind=@OP_ARG;into=register#arg0' +arm "OP15 callees neg (negative): a non-function name in or is no value reference" oppy --callees=neg attr:count=0 noattr:value_refs nvr:0 +arm "OP16 callers c_h (C, negative): || yields a truth value — no row" opc --callers=c_h attr:count=0 noattr:value_refs nvr:0 +arm "OP17 callers c_t (C control): the ternary branch is a returned value" opc --callers=c_t attr:value_refs=1 'vr:bind=@OC_TERN;into=(return)' + # ── Controls: no value reference → no new attribute, element, row or legend clause ─────────────────────── echo "-- controls" ctl_arm() { # ctl_arm ARGV PRESENCE-TOKEN… diff --git a/test/regression.sh b/test/regression.sh index b5cc0fd7b..96fafc8c6 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else 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 clojurecheck 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 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 forsigspancheck 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 langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck 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 ownerhopcheck 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 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 rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck 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 stdmembergatecheck 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 attribmultidefcheck 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 clojurecheck 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 declinedsitecheck 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 forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck 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 langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck 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 ownerhopcheck 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 qddegradecheck 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 rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck 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 stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck 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)" diff --git a/test/relevancefloorcheck.sh b/test/relevancefloorcheck.sh index efab47e7a..8b1d57305 100755 --- a/test/relevancefloorcheck.sh +++ b/test/relevancefloorcheck.sh @@ -177,12 +177,17 @@ echo "=== (4) byte ceiling on the fixture ===" # 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_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 ' ' )" +# RE-PIN 2026-10-15 (lane forbudget-soft round C, FOLD C; rule 4): the now carries page 0 of the --for file page — rows +# `` — and its full legend clause grew 391 -> 598 B to define them. (4) normalises those out too (the page rows, +# and the clause back to its old 391 B), so its 3000 B assertion still measures what it measured; (4b), the whole answer, +# moves 3300 -> 3400 B (measured 3366): still far below the 5307 B padded answer this arm exists to keep out. +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=//' \ + | python3 -c 'import re,sys; s=sys.stdin.read(); s=re.sub(r"","",s); m=re.search(r"; tail: [^\]]*?budget-trimmed row\)",s); s=s.replace(m.group(0),"x"*391) if m and len(m.group(0))>391 else s; sys.stdout.write(s)' | 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" +[ "$a_bytes" -lt 3400 ] && ok "(4b) the whole floored bundle is $a_bytes B (< 3400 with e=, the tail page rows and their legend clauses)" \ + || no "(4b) the whole floored bundle is $a_bytes B, over the 3400 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" \ diff --git a/test/routecheck.sh b/test/routecheck.sh index ba040aa3b..33fc5ac94 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -67,6 +67,8 @@ CONCEPT="how does resolution work" # the pre-routing golden (captured from plain subtoken+body). The DEFAULT run only ADDS a "[routed: # subtoken+body …]" header note, so we compare the --no-route run (identical bytes) to the golden AND # assert the DEFAULT run's ranking body matches by stripping just the header comment. ────────────────── +# RE-PIN 2026-10-15 (forbudget-soft round C, FOLD C; rule 4): golden_for.xml re-captured — the ONLY bytes that moved are the +# (page rows with sym= after the rank rows; total= every positive-score file), the tail legend clause and est_tokens=. # RE-PIN 2026-07-30 (CA4 fixup wave, §F1): est_tokens="696" -> "703", the ONLY bytes that moved (the document # is 1757 B before and after; every ranking byte identical vs the pre-wave binary). Same cause as anchorcheck's # re-pin, and the two together are what identify it: BOTH goldens moved by exactly +7, so this is the diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index ae45429ee..73d002f6d 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()/dev/null >"$TMP/callers_name" +# attrib-multidef: the 19 definitions form several groups, so each row also names its definitions (of_def=, defattrib.h) [ "$( callershead name )" = 'of="name" defs="19" count="2"' ] \ - && grep -q '' "$TMP/callers_name" \ - && grep -q '' "$TMP/callers_name" \ + && grep -qE '' "$TMP/callers_name" \ + && grep -qE '' "$TMP/callers_name" \ && ok 'defs-only: the 2 name callers are METHODS (render_label by name alone, lookup pinned by its own class) — the 9 columns contribute zero edges to the 19-def set; SingleColumn.new.name binds nothing (no in-tree def on that model)' \ || no "defs-only: name callers: $( callershead name )" diff --git a/test/selfcheckcheck.sh b/test/selfcheckcheck.sh index a6b1e3f75..50460833a 100755 --- a/test/selfcheckcheck.sh +++ b/test/selfcheckcheck.sh @@ -152,6 +152,10 @@ ALLOW = { "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", + "declinedCallsMadeBy": "read-only sum of the sources' Graph::declinedOut counts (graph.h); no state — the site-record postconditions (lane declined-sites)", + "inertDeclinedMadeBy": "read-only count of the sources among Graph::inertDeclinedCaller (graph.h); no state — the record postconditions with PR #380's inert quoted sites (train 28)", + "std::ranges::is_sorted": "read-only walk of the given range (graph.h rubyMainObjectMixins: the rows legoMainRowsOf binary-searches; lane ruby-pair)", + "has_value": "read-only std::optional state test (verbs_grep.h: grepReadsIngest named its stop; lane grep-carrier)", "inCheckout": "read-only member predicate (crossref.h WhereHit::inCheckout: fromWorktree || ref == \"HEAD\"); no state — labelJobRows' precondition that a parse job holds no checkout row", } # (T) ASSUMED-THEN-TESTED allowlist: "site -> reason", one line each. A site lands here only when the equality diff --git a/test/showcase_capture.py b/test/showcase_capture.py index 772ec407d..36538867c 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -494,6 +494,7 @@ def _strayScope( sub, kind ): 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. 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} . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code", "A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why=\"selector\"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.") add(S4, f"{BIN} . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text", "The same page with every t= line whole: the page above previews each line over 300 B (200 B and t_bytes=, the naming this call), and --whereis-full-text replays it with the same listing, window, rows and labels. MCP: full_text=true.") 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.") diff --git a/test/subscriptopcheck.sh b/test/subscriptopcheck.sh new file mode 100755 index 000000000..aeb526da2 --- /dev/null +++ b/test/subscriptopcheck.sh @@ -0,0 +1,385 @@ +#!/usr/bin/env bash +# subscriptopcheck.sh — gate for SUBSCRIPT = OPERATOR-METHOD CALL (Ruby `recv[ k ]` -> `[]` / `[]=`, Python `obj[ k ]` -> +# `__getitem__` / `__setitem__`) and for Ruby's MAIN OBJECT as a --lego implementor (``). +# +# Before this round an element access was no call at all: rake's Application#invoke_task does `t = self[ name ]`, which runs +# TaskManager#[] (Application includes TaskManager), and --callees=invoke_task listed only parse_task_string and invoke. +# And rake's dsl_definition.rb ends with `self.extend Rake::DSL` — the top level's self is the main object, which then +# answers DSL's methods — but --lego=DSL listed only classes. +# +# THE RULE (queries/{ruby,python}/tags.scm @reference.subscript; ingest_binds.h subscriptOperatorRef; graph.h +# subscriptOperatorProven; parser version 174): +# * an element access names its receiver class's subscript operator method, by the language's table +# (kSubscriptOperatorMethods): the write form as an assignment's whole target, the read form otherwise; +# * it is a CALL only where a receiver rule PROVES the class: Ruby's lookup on self in a def (through include/extend +# ancestry), a receiver the code builds (`r = Registry.new`, `Registry.new[ k ]`), a class constant's own +# `def self.[]`; Python's receiver evidence (`self` in a class or its bases, a typed parameter, a constructed local, +# a field the class assigns). Everything else — a Hash/Array/dict/list local, a literal, an untyped parameter — makes +# NO edge and NO hedge: no definition named `[]` anywhere decides it by name; +# * a Ruby file's top-level `include M` / `extend M` / `self.extend M` lists main as M's implementor in --lego=M. +# Stated floors, each pinned below: a compound assignment (`self[ k ] ||= v`, `o[ k ] += 1`) is recorded as the read form; +# a Ruby instance variable receiver is not typed (`@reg[ k ]`); a multiple-assignment target, `del o[ k ]` and an +# annotation record nothing; a Python class-object subscript (`Registry[ k ]`, `cls[ k ]`) runs the metaclass and binds +# nothing; a top-level directive inside `if`, `prepend`, and a mixin onto another object list no main row. +# +# Usage: test/subscriptopcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire test/subscriptopcheck.sh +# Exits non-zero on any failure. Self-contained via mktemp. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "subscriptopcheck: BIN=$BIN" +fail=0 +ok(){ echo " PASS $1" || { fail=1; echo " FAIL could not write the PASS line for: $1"; }; return 0; } +no(){ echo " FAIL $1"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } + +DIR="$( mktemp -d )"; trap 'rm -rf "$DIR"' EXIT +RB="$DIR/rb"; PY="$DIR/py"; mkdir -p "$RB" "$PY" # the corpora — outputs live in $DIR, never inside them + +cat > "$RB/registry.rb" <<'RUBY' +module Lookup + def [](name) + fetch_entry(name) + end + + def []=(name, value) + store_entry(name, value) + end +end + +class Registry + include Lookup + + def fetch_entry(name) + name + end + + def store_entry(name, value) + value + end + + def find_self(name) + self[name] + end + + def put_self(name, value) + self[name] = value + end + + def or_assign(name) + self[name] ||= 1 + end + + def self.[](name) + new.find_self(name) + end +end + +class Plain + def [](k) + k + end +end + +class Catalog + def initialize + @reg = Registry.new + end + + def get_built(name) + r = Registry.new + r[name] + end + + def get_value(name) + Registry.new[name] + end + + def by_class(name) + Registry[name] + end + + def by_plain_class(name) + Plain[name] + end + + def via_ivar(name) + @reg[name] + end + + def hash_only(name) + h = {} + h[name] = 1 + h[name] + end + + def array_only(i) + a = [1, 2] + a[i] + end + + def untyped(thing, name) + thing[name] + end + + def literal + { a: 1 }[:a] + end +end + +module Tasks + def task(name) + name + end +end + +module Inner + def inner_m + 1 + end +end + +module Prepended + def pre_m + 1 + end +end + +module Other + def other_m + 1 + end +end + +self.extend Tasks +include Lookup +extend ::Tasks +if true + include Inner +end +prepend Prepended +Object.new.extend Other +include Comparable +RUBY + +cat > "$PY/registry.py" <<'PY' +class Registry: + def __init__(self): + self.items = {} + + def __getitem__(self, key): + return self.items[key] + + def __setitem__(self, key, value): + self.items[key] = value + + def lookup_self(self, key): + return self[key] + + def store_self(self, key, value): + self[key] = value + + @classmethod + def by_cls(cls, key): + return cls[key] + + +class Sub(Registry): + def first(self): + return self["first"] + + +class Holder: + def __init__(self): + self.reg = Registry() + + def via_field(self, k): + return self.reg[k] + + +def use_typed(reg: Registry): + return reg["x"] + + +def use_built(): + r = Registry() + return r["y"] + + +def use_dict(): + d = {} + d["k"] = 1 + return d["k"] + + +def use_untyped(thing): + return thing["z"] + + +def use_literal(): + return {"a": 1}["a"] + + +def use_list(xs): + return xs[0] + + +def remove(reg: Registry): + del reg["x"] + + +def class_side(): + return Registry["x"] + + +def multi(other: Registry): + other["a"], other["b"] = 1, 2 + + +def aug(other: Registry): + other["n"] += 1 + + +def annotated(x: "dict[str, int]") -> None: + y: dict[str, int] = {} + return y +PY + +line_of(){ grep -n -- "$2" "$1" | head -1 | cut -d: -f1; } +RGET=$( line_of "$RB/registry.rb" 'def \[\](name)' ) +RSET=$( line_of "$RB/registry.rb" 'def \[\]=(name, value)' ) +RCLS=$( line_of "$RB/registry.rb" 'def self.\[\](name)' ) +PGET=$( line_of "$PY/registry.py" 'def __getitem__' ) +PSET=$( line_of "$PY/registry.py" 'def __setitem__' ) +for v in RGET RSET RCLS PGET PSET; do + case "${!v}" in ''|*[!0-9]*) echo " FAIL fixture line for $v is not numeric: '${!v}'"; exit 1 ;; esac +done + +# one callees answer's rows; the answer must have exited 0 and carried its root +callees(){ # dir name tag + local out="$DIR/$3.xml" + "$BIN" "$1" --callees="$2" --no-cache > "$out" 2> "$DIR/$3.err" + local rc=$? + if [ $rc -ne 0 ] || ! grep -q ']*>' "$out" > "$DIR/$3.rows" + return 0 +} +has_row(){ grep -qF -- "n=\"$2\" p=\"$3\"" "$DIR/$1.rows"; } +no_op_row(){ ! grep -qE -- 'n="(\[\]|\[\]=|__getitem__|__setitem__)"' "$DIR/$1.rows"; } + +echo "== Ruby: an element access is a call of the receiver class's []/[]= where a rule proves the class" +pos_rb(){ # name tag op line what + if callees "$RB" "$1" "$2"; then + if has_row "$2" "$3" "registry.rb:$4" && ! grep -q 'via=' "$DIR/$2.rows"; then ok "R $5: $1 -> $3 registry.rb:$4 (proven, no via=)" + else no "R $5: $1 should call $3 at registry.rb:$4 with no via=: $( tr '\n' ' ' < "$DIR/$2.rows" )"; fi + fi +} +pos_rb find_self r1 '[]' "$RGET" "self[ k ] through an included module" +pos_rb put_self r2 '[]=' "$RSET" "self[ k ] = v names the writer" +pos_rb get_built r3 '[]' "$RGET" "a local the code builds (r = Registry.new)" +pos_rb get_value r4 '[]' "$RGET" "a value receiver (Registry.new[ k ])" +pos_rb by_class r5 '[]' "$RCLS" "a class constant reaches its def self.[]" +pos_rb or_assign r6 '[]' "$RGET" "floor: self[ k ] ||= v is recorded as the read form" +if [ -s "$DIR/r5.rows" ] && ! has_row r5 '[]' "registry.rb:$RGET"; then ok "R class side: Registry[ k ] does not reach the instances' []" +else no "R class side: Registry[ k ] must not reach Lookup#[]: $( tr '\n' ' ' < "$DIR/r5.rows" 2>/dev/null )"; fi + +neg_rb(){ # name tag what + if callees "$RB" "$1" "$2"; then + if no_op_row "$2"; then ok "R negative $3: $1 has no []/[]= row" + else no "R negative $3: $1 must make no edge: $( tr '\n' ' ' < "$DIR/$2.rows" )"; fi + fi +} +neg_rb hash_only n1 "a Hash local (h = {})" +neg_rb array_only n2 "an Array local" +neg_rb untyped n3 "an untyped parameter, with in-tree [] namesakes (no hedge either)" +neg_rb literal n4 "a literal receiver" +neg_rb via_ivar n5 "floor: an instance variable receiver is not typed" +neg_rb by_plain_class n6 "a class constant whose class defines only an instance []" + +echo "== Ruby: a top-level include/extend lists the main object in --lego" +lego(){ # type tag + "$BIN" "$RB" --lego="$1" --no-cache > "$DIR/$2.xml" 2> "$DIR/$2.err" + local rc=$? + if [ $rc -ne 0 ] || ! grep -q '' "$DIR/l1.xml" | tr '\n' ' ' ) + if [ "$m" = " " ]; then + ok "L1 self.extend Tasks and extend ::Tasks: two main rows at their constants" + else no "L1 --lego=Tasks main rows: got '$m'"; fi + # train 28 (the lane review's carried item; by design, rule 4): the two directives are two ROWS of ONE implementor — both mix + # into the same main object — so implementors= counts it once. Was implementors="2" (one per directive). The two rows stay + # asserted above; L2 below keeps a class beside main at 2. + if grep -q ']* implementors="1"' "$DIR/l1.xml"; then ok "L1 implementors= counts the main object once (two directives, two rows)" + else no "L1 implementors= should be 1 (one main object): $( grep -o ']*>' "$DIR/l1.xml" )"; fi + if grep -q '.*//' )" - if [ "$TB_BYTES" -le "$TB_ALLOW" ]; then - ok "#17 --for 900-char task --token-budget=$TB_B: $TB_BYTES B fits the $TB_ALLOW B allowance" - elif printf '%s' "$TB_HDR" | grep -q 'over_ceiling'; then - ok "#17 --for 900-char task --token-budget=$TB_B: $TB_BYTES B over $TB_ALLOW B and DISCLOSES over_ceiling" - else - no "#17 --for 900-char task --token-budget=$TB_B: $TB_BYTES B past the $TB_ALLOW B allowance in SILENCE" - fi + TB_VERDICT="$( python3 - "$TMP_TB_DIR/abs.$TB_B" "$TB_B" "$TB_LONG_TASK" <<'PY_EOF' +import re, sys, html +s = open( sys.argv[ 1 ] ).read(); n = int( sys.argv[ 2 ] ); task = sys.argv[ 3 ] +T = int( n * 2.36 * 0.9 ) +root = re.search( r']*)>', s ); m = re.search( r']*>(.*?)', s, re.S ) +if not root or not m: print( "BAD no root or no block" ); sys.exit() +rows = re.findall( r']*)>.*?', m.group( 1 ), re.S ) +elided = sum( n2 for a in rows for k, n2 in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +charged = len( m.group( 1 ).encode() ) + elided +t = re.search( r'\btask="([^"]*)"', root.group( 1 ) ) +head = s.split( ' comment — the default dropped the ceiling it applied" done -# (T3) an overrun is LABELLED. A budget this small cannot fit the first whole signature, so the honest -# answer is the complete bundle + over_ceiling — never a silent 15x overshoot, never a mutilated bundle. +# (T3) forbudget-soft (rule 4): an explicit --from-trace --token-budget is a SOFT target. It used to be a hard ceiling whose +# overrun was LABELLED (over_ceiling, the header ladder ready to drop the echo); now the answer is the complete bundle +# with NO over_ceiling (est_tokens may exceed N), the trace source echo is never dropped, and nothing is mutilated: +# every suspect row ships (shown == total) and the innermost in-corpus body is served. RED on base (d4325a8c): base +# labels these overruns over_ceiling="1". The premise (the bundle really is past the old allowance) stays numeric. for tb in 50 100 500; do out="$( tb_run "$tb" )"; n=$( printf '%s' "$out" | wc -c | tr -d ' ' ); want="$( allowance_of "$tb" )" - if [ "$n" -gt "$want" ]; then - printf '%s' "$out" | grep -qE 'over_ceiling(="1"|:)' \ - && ok "(T3) --token-budget=$tb: ${n}B over a ${want}B allowance AND says over_ceiling" \ - || no "(T3) --token-budget=$tb: ${n}B over a ${want}B allowance with NO over_ceiling label" - else - no "(T3) --token-budget=$tb did not overshoot ($n <= $want) — this arm no longer measures anything" - fi + case "$n$want" in ''|*[!0-9]*) no "(T3) --token-budget=$tb: no byte count / allowance to compare (n='$n' allowance='$want')"; continue ;; esac + if [ "$n" -le "$want" ]; then no "(T3) --token-budget=$tb did not overshoot ($n <= $want) — this arm no longer measures anything"; continue; fi + t3bad="" + printf '%s' "$out" | grep -qE 'over_ceiling(="1"|:)' && t3bad="$t3bad; over_ceiling on a soft target (NOLBL)" + printf '%s' "$out" | grep -q '^]*task="traces/py.txt"' || t3bad="$t3bad; root task= echo missing (HL)" + printf '%s' "$out" | grep -q '/dev/null )" +printf '%s' "$T3M" | grep -qE '^]*max_tokens="50"[^>]* over_ceiling="1"' \ + && ok "(T3 twin) --max-tokens=50 overrun carries over_ceiling=\"1\" (the hard ceiling twin)" \ + || no "(T3 twin) --max-tokens=50 overrun lacks max_tokens=50 + over_ceiling=\"1\" on the root" # (T4) the label is not unconditional: a budget the bundle FITS under must stay silent (a lens that always # cries over_ceiling is as useless as one that never does). 20000 tokens is ~54 KB of allowance. diff --git a/test/truncvocabcheck.sh b/test/truncvocabcheck.sh index b8e013af8..8fc74d950 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 )" diff --git a/test/w3fixbudgetcheck.sh b/test/w3fixbudgetcheck.sh index 05d11b504..9ccac8cbe 100755 --- a/test/w3fixbudgetcheck.sh +++ b/test/w3fixbudgetcheck.sh @@ -18,7 +18,8 @@ # # H2 — --for's header floor (fixed legend + the task echoed twice) can exceed the ceiling --help promises it # "trims to fit", and it shipped the overrun silently: 5.3x at a 900-char task. An absolute-ceiling arm, -# which is what monotonicity could never catch — a floor is monotone. +# which is what monotonicity could never catch — a floor is monotone. (forbudget-soft: the explicit budget is +# a soft target now, so the H2 arms assert the soft contract instead — see below.) # M1 — --pack-task charged the verbatim task= attribute and not the comment's echo of the same text, so a # fixed 1024-byte reserve was bounding user-length bytes; and its "route_attr: dropped (ceiling)" note # claimed a remedy that had not been measured. Same class at --recall --max-tokens, where the byte @@ -29,16 +30,19 @@ # M3 — the comment echo was dash-scrubbed and nothing else, so a C0 byte or an invalid UTF-8 sequence in the # task made xmllint reject the whole document. # -# THE ONE INVARIANT the ceiling arms assert (and the reason they can be absolute at all): a bundle is -# conformant at or under ceiling x 1.15 — the single-entry overshoot the design allows, because the ranking -# section emits its first entry WHOLE (src/serialize.h kCeilingFirstEntryTolerance; the same 1.15 -# bundleidcheck.sh and partitioncheck.sh already commit to). Past that bar the lens has provably failed to trim -# to fit, and must SAY SO. So the assertion is a biconditional, not a bound: -# -# delivered > ceiling * 1.15 <=> the document discloses over_ceiling -# -# A one-sided bound would be satisfiable by a lens that never trims and never admits it (the pre-fix state) or -# by one that labels everything over_ceiling (honest but useless). Both directions are checked. +# forbudget-soft (rule 4, owner decisions 2026-10-14) RETIRED the absolute-ceiling invariant these arms were built on. +# An explicit --token-budget on --for / --pack-task is now a SOFT target: T = N x kMinBytesPerToken x kBudgetHeadroom bytes +# charged against the ranked ROWS only (--for), or each section stopping once its share is reached (--pack-task). There is +# no byte promise, so nothing is labelled over_ceiling and nothing in the header — the verbatim task echo, route=, any +# legend clause — is ever dropped "(ceiling)" to fit. So the H2 arms no longer assert "fits ceiling x 1.15 <=> discloses +# over_ceiling"; at every (task length x budget x lens) point they assert what must hold now: +# ST (--for) the rows' charged bytes inside fit T, or are only the rank 1..4 floor; +# RP the served rows are a rank prefix of the default answer's rows, growing with the budget; +# NX (--for) every cut element (capped="1") carries next=; +# NOLBL no over_ceiling= on the root and no over_ceiling ladder note; +# HL no "(ceiling)" drop and no "task_echo: dropped" in the header; task= verbatim; route= kept when the default has it. +# The user-text findings (M2/M3 hostile bytes, the M1 charge, the --recall disclosure, determinism) are unchanged: +# --recall --max-tokens keeps its hard ceiling and its biconditional (arm 5). set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" @@ -132,17 +136,13 @@ PY_EOF HOSTILE="nl tab cr c0 badu8 mixed" # The disclosure lives in the header COMMENT. Grepping the whole document for it would also match a body that -# quotes the word — this gate's own corpus does, deliberately, so that mistake cannot pass unnoticed. Every -# ceiling arm below reads the header region ONLY: everything up to the first "-->". +# quotes the word — this gate's own corpus does, deliberately, so that mistake cannot pass unnoticed. The charge +# arm (4) reads the header region ONLY: everything up to the first "-->" (arms 3 and 7 read every header comment). hdr(){ python3 -c " import sys d = open( sys.argv[1], 'rb' ).read() i = d.find( b'-->' ) sys.stdout.buffer.write( d if i < 0 else d[ :i + 3 ] )" "$1"; } -# M11 (2026-09-04): the legends now DEFINE over_ceiling= ("over_ceiling=1 when …"), so a bare word-grep reads -# the definition as the label. The label is the root attribute over_ceiling="1" or the ladder's own colon -# note (" over_ceiling: the header floor …"); the map spells over_ceiling=1 in its stats comment. -saysOverCeiling(){ hdr "$1" | grep -qE 'over_ceiling(="1"|=1|:)'; } # ═══ 1) M2 + M3 — G4 AND xmllint under hostile task bytes, every verb that echoes user text ═══════════════════ # RED (build_base): xmllint FAILS on {--for,--pack-task} x {c0,badu8}; the newline arms emit a literal \n @@ -223,73 +223,145 @@ if key != 'anchor--the\n\x01budget\tceiling�': sys.exit( 0 ) PY_EOF -# ═══ 3) H2 + M1 — THE ABSOLUTE CEILING BICONDITIONAL, across task length x budget ════════════════════════════ -# tokenbudgetcheck asserts MONOTONICITY, which a floor satisfies trivially (5.3x over at every budget is -# perfectly monotone). This is the absolute arm: for every (task length, budget) point, the delivered document -# either fits ceiling*1.15 or says over_ceiling — never neither, never both. -# -# RE-PIN 2026-09-05 (capture-audit verify-wave2 F2, lane V2) — to the NEW contract stated precisely, not to a -# loosened assertion. over_ceiling= used to be a statement about BYTES vs the 1.15-tolerance allowance, which -# is why the reverse half could be written in bytes. It is now a statement about the two numbers the ROOT -# ITSELF PRINTS, in the unit it prints them in: over_ceiling="1" exactly when est_tokens > budget_tokens, on -# every rung (F2: `--for --token-budget=1600` shipped `budget_tokens="1600" est_tokens="1665"` and no label, -# because the flag fired only on the ladder's last rung). The two halves are therefore asymmetric on purpose: -# FORWARD (unchanged, still in bytes): delivered > allowance => over_ceiling. Sound, because -# allowance = N x 2.36 x 1.15 and the emitter prices at 2.50 B/tok, so bytes past the allowance -# are >= 1.085 x N tokens — over the budget by construction. -# REVERSE (re-pinned, now in tokens): over_ceiling => est_tokens > budget_tokens. The old byte form -# would now fire on every HONEST label inside the 15% tolerance band, which is precisely the -# window F2 lived in. -# A label with no overshoot behind it still fails, so the arm cannot be satisfied by labelling everything. -CEILING_ARM_POINTS=0 -CEILING_ARM_BAD=0 +# ═══ 3) H2 — THE SOFT TARGET, across task length x budget ════════════════════════════════════════════════════ +# forbudget-soft (rule 4) — REPLACES the absolute ceiling biconditional (delivered > B x 2.36 x 1.15 <=> over_ceiling, and +# over_ceiling <=> est_tokens > budget_tokens; re-pinned 2026-09-05 for F2). An explicit --token-budget is a soft target +# now, so a bundle past B x 2.36 x 1.15 is legitimate (the header always ships; the floor rows always ship) and a label is +# never legitimate. Kept from the old arm: the 24-point grid (tasks mid/long x 300..3000 x both lenses) and the premise +# that the root prints a numeric est_tokens=. Per point: ST + RP + NX (--for), RP (--pack-task), NOLBL + HL (both). +srcnum(){ sed -n "s/^inline constexpr [a-z:_ ]*$1 *= *\([0-9.]*\);.*/\1/p" "$ROOT/src/serialize.h" | head -1; } +MINBPT="$( srcnum kMinBytesPerToken )"; HEADROOM="$( srcnum kBudgetHeadroom )" +case "$MINBPT$HEADROOM" in ''|*[!0-9.]*) no "H2: cannot read kMinBytesPerToken/kBudgetHeadroom from src/serialize.h ('$MINBPT' '$HEADROOM') — the ST arm has no target to judge" ;; esac +SOFT="$TMP/soft.py" +cat > "$SOFT" <<'PY_EOF' +# soft.py DOC DEFAULT_DOC TASKFILE LENS T — prints "k=", one "row p:l:n" per served row in rank order, then +# one line per violation; exit 1 on any +import re, sys, xml.etree.ElementTree as ET +doc, ref, taskFile, lens, T = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], int( float( sys.argv[5] ) ) +raw = open( doc, 'rb' ).read() +bad = [] +if not raw: + print( 'k=0' ); print( ' no document' ); sys.exit( 1 ) +s = raw.decode( 'utf-8', 'replace' ) +comments = ' '.join( re.findall( r'', s, re.S ) ) +def strip( t ): + t = re.sub( r'', '', t, flags = re.S ) + return re.sub( r'', '', t, flags = re.S ) # the legend quotes and defines attributes; it is not the document's tags +body = strip( s ) +root = re.search( r']*>', body ) +if root is None: + print( 'k=0' ); print( ' no root' ); sys.exit( 1 ) +root = root.group( 0 ) +# a row's CHARGE is the ladder's (serialize.h entryCost): its printed bytes plus the zero cx=/ccx=/in= the lean spelling elides +def charge( a, printed ): + return printed + sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +def rowsOf( t ): + m = re.search( r']*)>(.*?)', t, re.S ) + if not m: return None, None, 0 + rows, total = [], 0 + for d in re.finditer( r']*)>.*?|]*)/>', m.group( 2 ), re.S ): + a = d.group( 1 ) or d.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, '?' ] )[ 1 ] + r = g( 'r' ) + c = charge( a, len( d.group( 0 ).encode() ) ) + total += c - len( d.group( 0 ).encode() ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( 'p' ) + ':' + g( 'l' ) + ':' + g( 'n' ) ) ) + rows.sort() + return m.group( 1 ), rows, len( m.group( 2 ).encode() ) + total +sigAttrs, rows, charged = rowsOf( body ) +refAttrs, refRows, _ = rowsOf( strip( open( ref, 'rb' ).read().decode( 'utf-8', 'replace' ) ) ) +k = len( rows ) if rows is not None else 0 +print( 'k=%d' % k ) +for _, x in ( rows or [] ): print( 'row ' + x ) +# est_tokens present and numeric (CHECKLIST 14: a missing premise is a FAIL) +if not re.search( r' est_tokens="\d+"', root ): bad.append( 'the root carries no numeric est_tokens=' ) +# NOLBL +if ' over_ceiling="1"' in root: bad.append( 'over_ceiling="1" on a soft target (est_tokens may exceed budget_tokens; no label)' ) +if re.search( r'over_ceiling:', comments ): bad.append( 'an over_ceiling ladder note rides the header' ) +# HL +if '(ceiling)' in comments: + dropped = sorted( set( re.findall( r'([^\[\]]{0,40}) dropped \(ceiling\)', comments ) ) ) + bad.append( 'a header piece was dropped "(ceiling)": %s' % ( ', '.join( dropped ) or 'see header' ) ) +if 'task_echo: dropped' in comments: bad.append( 'the task echo was dropped' ) +want = open( taskFile, 'rb' ).read().decode( 'utf-8' ) +try: + got = ET.fromstring( raw ).attrib.get( 'task' ) +except Exception as exc: + got = None; bad.append( 'the document does not parse as XML: %s' % ( exc, ) ) +if got is not None and got != want: bad.append( 'task= is not the verbatim task (%d chars vs %d)' % ( len( got ), len( want ) ) ) +refRoot = re.search( r']*>', strip( open( ref, 'rb' ).read().decode( 'utf-8', 'replace' ) ) ) +if refRoot and ' route=' in refRoot.group( 0 ) and ' route=' not in root: bad.append( 'route= dropped (the default answer carries it)' ) +# RP +if rows is None or refRows is None: bad.append( 'no block (served %s, default %s) — no rows to judge' % ( rows is not None, refRows is not None ) ) +else: + if k == 0: bad.append( 'no row served — the rank 1..4 floor always ships' ) + if [ r for r, _ in rows ] != list( range( 1, k + 1 ) ): bad.append( 'rows are not the ranks 1..%d: r=%s' % ( k, [ r for r, _ in rows ] ) ) + # the default answer may move its doc rows below its code rows (docs_after_code=, renumbering r=; an explicit budget's + # answer does not, on base as on head), so against a reordered default the rows are judged as a SET there and the rank + # PREFIX is judged across budgets by the caller (rows(N) a prefix of rows(N')) + if refAttrs is not None and 'docs_after_code=' in refAttrs: + extra = set( x for _, x in rows ) - set( x for _, x in refRows ) + if extra: bad.append( 'rows the default answer does not serve: %s' % sorted( extra )[ :3 ] ) + elif [ x for _, x in rows ] != [ x for _, x in refRows[ :k ] ]: bad.append( 'rows are not the default answer\'s first %d rows' % k ) + if k < min( 4, len( refRows ) ): bad.append( 'only %d row(s): the rank 1..4 floor always ships' % k ) +if lens == 'for' and rows is not None: + # ST: the rows' charged bytes fit T, or are only the floor + if not ( charged <= T or k <= 4 ): bad.append( 'ST: %d rows charge %d B past the %d B soft target (beyond the 4-row floor)' % ( k, charged, T ) ) + # NX: every cut section carries next= (the ranked rows, the hop rows, the file tail — the continuations --for promises) + for m in re.finditer( r'<(sigs|hops|tail)\b([^>]*)\bcapped="1"([^>]*)>', body ): + if ' next="' not in m.group( 2 ) + m.group( 3 ): bad.append( 'NX: <%s capped="1"> carries no next=' % m.group( 1 ) ) +for line in bad: print( ' ' + line ) +sys.exit( 1 if bad else 0 ) +PY_EOF +SOFT_POINTS=0; SOFT_BAD=0; SOFT_BINDS=0 for TASKFILE in mid long; do T="$( cat "$TMP/task.$TASKFILE" )" - for B in 300 600 900 1200 1600 3000; do - for V in for pack-task; do + for V in for pack-task; do + "$BIN" "$CORPUS" --no-cache "--$V=$T" >"$TMP/c.def" 2>/dev/null # the default answer: RP's reference rows + PREVK=0; : > "$TMP/rows.prev" + for B in 300 600 900 1200 1600 3000; do "$BIN" "$CORPUS" --no-cache "--$V=$T" --token-budget=$B >"$TMP/c.out" 2>/dev/null - BYTES="$( wc -c < "$TMP/c.out" | tr -d ' ' )" - # the bar, in shell, from the same two constants the code uses (2.36 B/tok x 1.15 tolerance) - ALLOW="$( python3 -c "print(int($B*2.36*1.15))" )" - OVER=0; saysOverCeiling "$TMP/c.out" && OVER=1 - # the root's own price, in the unit budget_tokens= is stated in (comments stripped: the legend - # DEFINES est_tokens= and a definition is not a measurement) - EST="$( perl -0pe 's###gs' "$TMP/c.out" | grep -oE ']*)?>' | head -1 \ - | grep -oE ' est_tokens="[0-9]+"' | head -1 | tr -dc '0-9' )" - CEILING_ARM_POINTS=$(( CEILING_ARM_POINTS + 1 )) - if [ "$BYTES" -gt "$ALLOW" ] && [ "$OVER" = "0" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: $BYTES B > allowance $ALLOW B and SILENT about it" - elif [ -z "$EST" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: the root carries no est_tokens= — the reverse half has no number to check" - elif [ "$OVER" = "1" ] && [ "$EST" -le "$B" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: claims over_ceiling but est_tokens=$EST is inside the $B-token budget" - elif [ "$OVER" = "0" ] && [ "$EST" -gt "$B" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: root prints budget_tokens=$B est_tokens=$EST and SILENT about the overshoot" + TB="$( python3 -c "import sys; print(int(float(sys.argv[1])*float(sys.argv[2])*float(sys.argv[3])))" "$B" "${MINBPT:-0}" "${HEADROOM:-0}" )" + SOFT_POINTS=$(( SOFT_POINTS + 1 )) + OUT="$( python3 "$SOFT" "$TMP/c.out" "$TMP/c.def" "$TMP/task.$TASKFILE" "$V" "$TB" )"; RC=$? + K="$( printf '%s\n' "$OUT" | sed -n 's/^k=//p' | head -1 )" + if [ "$RC" != 0 ]; then + SOFT_BAD=$(( SOFT_BAD + 1 )) + echo " --$V task=$TASKFILE budget=$B (T=$TB B):"; printf '%s\n' "$OUT" | grep -v '^k=\|^row ' + elif [ "${K:-0}" -lt "$PREVK" ] || [ "$( printf '%s\n' "$OUT" | sed -n 's/^row //p' | awk -v n="$PREVK" 'NR <= n' )" != "$( cat "$TMP/rows.prev" )" ]; then + SOFT_BAD=$(( SOFT_BAD + 1 )) + echo " --$V task=$TASKFILE budget=$B: the $K row(s) served do not extend the $PREVK served at the budget below — RP is not a growing rank prefix" fi + printf '%s\n' "$OUT" | sed -n 's/^row //p' > "$TMP/rows.prev" + # non-vacuity: a point whose est_tokens is past its budget (so NOLBL was tested on a blown target, not assumed) + EST="$( perl -0pe 's###gs' "$TMP/c.out" | grep -oE ']*)?>' | head -1 | grep -oE ' est_tokens="[0-9]+"' | tr -dc '0-9' )" + [ -n "$EST" ] && [ "$EST" -gt "$B" ] && SOFT_BINDS=$(( SOFT_BINDS + 1 )) + PREVK="${K:-0}" done done done -[ "$CEILING_ARM_BAD" = "0" ] \ - && ok "ceiling biconditional holds at all $CEILING_ARM_POINTS (task length x budget x lens) points" \ - || no "ceiling biconditional VIOLATED at $CEILING_ARM_BAD of $CEILING_ARM_POINTS points" +[ "$SOFT_BAD" = "0" ] \ + && ok "H2 soft target: ST/RP/NX/NOLBL/HL hold at all $SOFT_POINTS (task length x budget x lens) points" \ + || no "H2 soft target: VIOLATED at $SOFT_BAD of $SOFT_POINTS points" +[ "$SOFT_BINDS" -gt 0 ] \ + && ok "H2 soft target: non-vacuity — $SOFT_BINDS point(s) deliver est_tokens past their budget (NOLBL/HL were judged on blown targets)" \ + || no "H2 soft target: VACUOUS — no point delivers past its budget, so no-label/header-kept was never tested on a blown target" -# the RED case named in the finding, asserted on its own so a regression names itself: a 900-char task at a -# tight budget must not be silently 2x+ over. +# the case named in the finding, asserted on its own so a regression names itself. forbudget-soft (rule 4) — REPLACES +# "a 900-char task at --token-budget=600 fits B x 2.36 x 1.15 or discloses over_ceiling": the soft target makes no byte +# promise, so what must hold is that the floor rows are served and the header ships whole (task= verbatim, echo kept, +# route= kept, nothing dropped "(ceiling)"), with no label. T="$( cat "$TMP/task.long" )" for V in for pack-task; do + "$BIN" "$CORPUS" --no-cache "--$V=$T" >"$TMP/c.ldef" 2>/dev/null "$BIN" "$CORPUS" --no-cache "--$V=$T" --token-budget=600 >"$TMP/c.long" 2>/dev/null - BYTES="$( wc -c < "$TMP/c.long" | tr -d ' ' )" - ALLOW="$( python3 -c "print(int(600*2.36*1.15))" )" - if [ "$BYTES" -le "$ALLOW" ]; then - ok "--$V: 900-char task at --token-budget=600 fits the allowance ($BYTES <= $ALLOW B)" - elif saysOverCeiling "$TMP/c.long"; then - ok "--$V: 900-char task at --token-budget=600 is $BYTES B over $ALLOW B and DISCLOSES over_ceiling" + TB="$( python3 -c "import sys; print(int(600*float(sys.argv[1])*float(sys.argv[2])))" "${MINBPT:-0}" "${HEADROOM:-0}" )" + OUT="$( python3 "$SOFT" "$TMP/c.long" "$TMP/c.ldef" "$TMP/task.long" "$V" "$TB" )"; RC=$? + K="$( printf '%s\n' "$OUT" | sed -n 's/^k=//p' | head -1 )" + if [ "$RC" = 0 ]; then + ok "--$V: 900-char task at --token-budget=600 serves its $K floor row(s) with the header whole and no over_ceiling ($( wc -c < "$TMP/c.long" | tr -d ' ' ) B; no byte promise)" else - no "--$V: 900-char task at --token-budget=600 delivers $BYTES B past the $ALLOW B allowance in SILENCE" + no "--$V: 900-char task at --token-budget=600:"; printf '%s\n' "$OUT" | grep -v '^k=\|^row ' fi done @@ -376,39 +448,44 @@ done && ok "det-gate: byte-identical run-to-run across every hostile byte x ceiling rung" \ || no "det-gate: $DET_BAD argument set(s) are NOT byte-identical run-to-run" -# ═══ 7) the ladder's rungs are REACHABLE and correctly ordered ════════════════════════════════════════════════ -# A remedy nothing ever selects is dead code that reads as a feature. Sweep a band of budgets and require that -# the cheap rung (drop the DUPLICATE comment echo, keeping the verbatim task= attribute) is actually taken -# somewhere — and that whenever it is, the verbatim attribute is still present. That ordering is the whole -# point: reclaim the duplicate before dropping unique information. +# ═══ 7) the header is NEVER dropped for a soft target ═══════════════════════════════════════════════════════════ +# forbudget-soft (rule 4) — REPLACES "the ceiling ladder's task_echo rung is REACHABLE" (and folds in its second half, "every +# echo-drop kept the verbatim task="). The header ladder existed to land a HARD ceiling; an explicit --token-budget is a soft +# target now, so the remedy that arm required to be reachable must now be unreachable. HL, across the same band and in the +# full dialect (the only one that echoes the task into the header comment): at every point `task_echo: dropped` is ABSENT, +# no header piece is dropped "(ceiling)", the comment echo is PRESENT, and the verbatim task= attribute is intact. T="$( cat "$TMP/task.long" )" -ECHO_RUNG_SEEN=0 -ECHO_RUNG_BAD=0 +ECHO_POINTS=0 +ECHO_BAD=0 for B in 900 1100 1200 1400 1600 1800 2000; do for V in for pack-task; do - # L1: the task_echo rung drops the header COMMENT's echo of the task, which only the full legend prints (the compact - # dialect never echoes it), so the rung is observable — and this arm meaningful — in the full dialect. "$BIN" "$CORPUS" --no-cache "--$V=$T" --token-budget=$B --legend=full >"$TMP/l.out" 2>/dev/null - hdr "$TMP/l.out" | grep -q 'task_echo: dropped' || continue - ECHO_RUNG_SEEN=$(( ECHO_RUNG_SEEN + 1 )) - # the verbatim copy must survive the drop of its scrubbed twin - python3 - "$TMP/l.out" "$TMP/task.long" <<'PY_EOF' || { -import sys, xml.etree.ElementTree as ET + ECHO_POINTS=$(( ECHO_POINTS + 1 )) + python3 - "$TMP/l.out" "$TMP/task.long" <<'PY_EOF' || { ECHO_BAD=$(( ECHO_BAD + 1 )); echo " ^ --$V budget=$B"; } +import re, sys, xml.etree.ElementTree as ET +raw = open( sys.argv[1], 'rb' ).read() want = open( sys.argv[2], 'rb' ).read().decode( 'utf-8' ) -got = ET.parse( sys.argv[1] ).getroot().attrib.get( 'task' ) -sys.exit( 0 if got == want else 1 ) +if not raw: + print( ' no document' ); sys.exit( 1 ) +s = raw.decode( 'utf-8', 'replace' ) +com = re.findall( r'', s, re.S ) +bad = [] +if any( 'task_echo: dropped' in c for c in com ): bad.append( 'task_echo: dropped — the comment echo was dropped for a soft target' ) +if any( '(ceiling)' in c for c in com ): bad.append( 'a header piece was dropped "(ceiling)"' ) +if not any( want[ :60 ] in c for c in com ): bad.append( 'the header comment does not echo the task' ) +try: + got = ET.fromstring( raw ).attrib.get( 'task' ) +except Exception as exc: + got = None; bad.append( 'the document does not parse as XML: %s' % ( exc, ) ) +if got is not None and got != want: bad.append( 'the verbatim task= is gone or altered' ) +for line in bad: print( ' ' + line ) +sys.exit( 1 if bad else 0 ) PY_EOF - ECHO_RUNG_BAD=$(( ECHO_RUNG_BAD + 1 )) - echo " --$V budget=$B: dropped the comment echo but the verbatim task= is gone or altered" - } done done -[ "$ECHO_RUNG_SEEN" -gt 0 ] \ - && ok "ceiling ladder: the task_echo rung is REACHABLE ($ECHO_RUNG_SEEN point(s) in the 900..2000 band)" \ - || no "ceiling ladder: the task_echo rung was never selected at any budget — an unreachable remedy" -[ "$ECHO_RUNG_BAD" = "0" ] \ - && ok "ceiling ladder: every echo-drop kept the VERBATIM task= attribute intact" \ - || no "ceiling ladder: $ECHO_RUNG_BAD echo-drop(s) also lost the verbatim task= — wrong bytes dropped" +[ "$ECHO_POINTS" -gt 0 ] && [ "$ECHO_BAD" = "0" ] \ + && ok "header kept: at all $ECHO_POINTS points of the 900..2000 band (full dialect) the task echo, every header piece and the verbatim task= ship" \ + || no "header kept: $ECHO_BAD of $ECHO_POINTS point(s) dropped part of the header for a soft target" # ═══ 8) the MCP SIBLING — the same hostile bytes through the live server ══════════════════════════════════════ # M3's scrub had to be applied at SIX echo sites, two of which are MCP-only (mcpverbs.h `for` and `exemplar`).