diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index f16c8b459..9e06f3e73 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -274,6 +274,7 @@ "legendrefcheck.sh": 4.3, "legobundlecheck.sh": 10.2, "legocheck.sh": 9.6, +"legotransitivecheck.sh": 2.0, "lintbudgetcheck.sh": 16.7, "lintcatalogcheck.sh": 1.7, "lintcheck.sh": 3.2, @@ -293,6 +294,7 @@ "manifestcheck.sh": 62.9, "mapdatasectioncheck.sh": 55.0, "mapdiffcheck.sh": 3.3, +"mapinventorycheck.sh": 2.5, "matchcapturecheck.sh": 11.8, "matchgrammarcheck.sh": 0.6, "maxfilesizecheck.sh": 6.8, @@ -378,6 +380,7 @@ "oswin32logiccheck.sh": 30.0, "outlinecheck.sh": 1.5, "overbudgetcommentcheck.sh": 0.5, +"ownerhopcheck.sh": 53.2, "ownerscheck.sh": 5.2, "packcallersharecheck.sh": 0.9, "packtaskcheck.sh": 20.8, @@ -544,6 +547,7 @@ "spectimingcheck.sh": 1.8, "staleackcheck.sh": 6.1, "statgatecheck.sh": 10.2, +"stdmembergatecheck.sh": 10.0, "structlayoutcheck.sh": 12.0, "sublistcountcheck.sh": 9.6, "substrfiltercheck.sh": 42.0, diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index e029c5a56..8cd08cd07 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -7,6 +7,7 @@ ack api-surface 060a064b6ffa7775 7 cid=605cbb1f768828e0 P2.2 register-macro dead ack api-surface 085e3d408c2c4a35 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 0a20676f246f053a 5 cid=d4f1ceec6378a259 t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign ack api-surface 0a3d16d6f3139408 10 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. +ack api-surface 0d64cc4708da0ab4 5 cid=104918b8416000b6 now=5 was=3 facet=contract-change p=src/ingest_crawl.h:1656 cli-perf-068: probeIgnoreSet is a section-local function of the ingest translation unit (ingest_crawl.h) with one caller, collectSources. It gains the ingest's IgnoreProbeSpeculation (the git ignore probe running beside the walk, checked against the fresh answer before the parse) and the canonical root the speculation's sidecar is keyed by; both are passed by collectSources, whose own new parameter is defaulted. No other caller exists to break. ack api-surface 0d6e1106ea5949d5 4 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 1039e3c8e0fc3667 4 cid=4efcfe9cb7f739a2 M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack api-surface 105c48e20c80c896 3 cid=720fab31ea99ebde A2: unmeasuredHintNote gained the AbsHintFrame parameter one commit after this lane introduced it (4db6fb3). It is a header-inline helper in namespace mcpedit with exactly one caller, resolveOneForEdit, in the same file; no consumer outside this lane ever saw the 2-arg form. The widening is what makes the never-parsed disclosure and the symbol scan agree about which files a hint names -- two copies of that rule is the defect this replaces. @@ -153,6 +154,7 @@ ack api-surface d1e50dc6d815cf27 6 cid=574545e642c44ece lane B1 cap disclosure: ack api-surface d3b5cec59d7a7684 5 cid=5b456bf45dd0d9c9 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface d7844daea0ff8817 2 cid=fb4e3ccac4afb00e assignment types (narrowcheck arms 44-51): api-surface buildFieldNarrowTables gains the class-name set (1 to 2 params) because its local-name set must drop an assignment's callee-read type that no class is called, the same rule Rule 2's varType table applies, and the set is built once in buildGraph and once in collectFieldUseSites rather than re-derived per call. duplication/new-clone-of-reused-helper classNameSet ~ layout.h buildAggIndex (93 tokens): classNameSet is the Class/Struct/Interface loop buildGraph already carried inline for Rule 2c, hoisted into resolve.h so collectFieldUseSites can share it; buildAggIndex maps a name to its ids in layout.h, which includes graph.h, so resolve.h cannot reuse it and a common helper would invert the include order for a 7-line loop with a different value type. +ack api-surface d8491d9b4cbead9f 4 cid=38f037dbe0fd9d7d now=4 was=3 facet=contract-change p=src/crossref.h:2561 whereis-lean (t= preview + parse read-back), one change: labelBranchRowsByParse gains the repo root so the parse batch reads back only the blobs it chose (internal, one caller computeWhereis); runCrossRef +2 cx is the --detail lift of the preview; writeWhereisListedPage +10 LOC the PREVIEW clause and ; kHelpTail +3 help lines and kCompactAttributeReadings +2 readings (t_bytes=, t_cut rows=) ack api-surface da0f13e11b19d034 5 cid=7050dbe3657ff0c3 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface db9af56c7a3f5bee 4 cid=bec957a0aa99147d P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface dd2f935e1818171a 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -445,7 +447,7 @@ ack complexity 5d8f5288f0df10f7 191 cid=f31f8078003db35b capture-audit 2026-09-0 ack complexity 5ec38fbd414fa4d4 56 cid=35883c203ba33e28 lane/tc-sliceat MCP half: dispatchMcpLine +9cx/+21LOC is the per-verb dispatch cost every advertised tool pays (the var/flow/depth arg reads, the depth band static_assert, and the one slice branch); the verb logic itself lives in mcpverbs.h sliceText (new-symbol, mirrors the CLI runSlice refusal-for-refusal with sliceBundleText as the one shared emitter). Gate-covered red-first in test/mcpslicecheck.sh ack complexity 5f7ae7ac9bbef3c5 33 cid=8662c6730ed46798 now=33 was=30 p=src/darkflags.h:522 0.6.6 D5 review B1: forEachCodeByte gains the per-file TemplateStack (JS/TS template literals span lines); its one caller, harvestEnvReads, is updated in the same commit ack complexity 61e5df9e1e40ff70 19 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) -ack complexity 639de1c3670999f9 69 cid=bbf7c6fe9dbbe217 wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement +ack complexity 639de1c3670999f9 84 cid=f7810071b0e9a62b now=84 was=82 p=src/verbs_change.h:1454 whereis-lean (t= preview + parse read-back), one change: labelBranchRowsByParse gains the repo root so the parse batch reads back only the blobs it chose (internal, one caller computeWhereis); runCrossRef +2 cx is the --detail lift of the preview; writeWhereisListedPage +10 LOC the PREVIEW clause and ; kHelpTail +3 help lines and kCompactAttributeReadings +2 readings (t_bytes=, t_cut rows=) | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack complexity 685ade09a168535e 30 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack complexity 6979fa794f2a5154 416 cid=027d9c38f3666ebb now=416 was=415 p=src/ingest_sidecap.h:1732 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: fn-literal bodies: captureTagsFacts gains the defBodyNodeOf call that hands a literal-bound name its literal (the @name and Lang ride along) and reads params/cx from it: a +3 cx / +3 verbosity call site at the def-extraction seam ack complexity 6a38d70787d09e64 28 cid=f6a0c09dfc153ece Kotlin port (PR #126): the leading whitespace sample is walked by codepoint (jsonesc::utf8SeqLen) so invalid UTF-8 folds into errNodes/errBytes, deduplicated against the ERROR spans tree-sitter recovery already counted; without it a garbage byte run swallowed by recovery reads as a healthy file in --skipped. @@ -512,7 +514,7 @@ ack complexity e1b4964cea59171b 18 cid=8ba981522099e145 H14/M13: symbolQueryJson ack complexity e514a69013d0934c 33 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B ack complexity e7f50422948f2c09 17 cid=6c7e771d42d81c16 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch ack complexity e836c18ca4452097 19 cid=d0f46b9a11a3932c now=19 was=17 p=src/mcpverbs.h:475 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows -ack complexity ec2848a4801493a2 51 cid=ed0825d14daf74d4 H11 (lane ca-L2): validateModifierGuards grows one more modifier arm (--allow-dirty, refused alone) in the flat if-chain this function IS; every sibling arm is the same 6 lines and splitting one out would hide it from the guard roster | prior: capture-audit 2026-09-04 lane L1 (flag-table universe), the seven gating rows of this lane's own five commits, each the documented cost of the guard/table it lands: parseArgs +4 cx is the --exclude= empty-value guard, one branch in the hand-written residue, the exact shape of its --and=/--not= siblings (M6); validateConfig +4 cx/+17 LOC is the bare --quality-ack pairing refusal plus the =REASON implication moved out of the parser arm (H10); validateModifierGuards +4 cx/+21 LOC is two pairing guards (--naming-locals refuse, --no-stable notice) in the helper every prior guard grew the same way (M16); kShapingVerbs 19->62 LOC is the ledger of every verb outside honorsPaging — 36 rows for 36 verbs, a table is its length, and test/shapingflagcheck.sh arm (F) is what stops it rotting (H3); main +11 LOC is the --json check moved ahead of the CLI edit bridge plus one call to refuseInertMainModifiers (H2/M16) — the two guards themselves and jsonUnsupportedVerb's allow-list walk live in their own helpers. Gates for all five written red first; battery green at this head. churn=self rows on jsonUnsupportedVerb/kViewFlags/writeAckRecords are this lane's own rewrites and are invisible in ref-pair mode by construction. +ack complexity ec2848a4801493a2 68 cid=82839ee962ffe806 now=68 was=66 p=src/cli.h:4627 whereis-lean fix round 1 (review C1): the --whereis-full-text flag and its MCP full_text twin, one change: validateModifierGuards gains its with-whereis refusal, whereisText its trailing defaulted fullText parameter, kBoolFlags one row and kHelpTail the flag's four help lines (PREVIEW now names the same-page replay) | prior: H11 (lane ca-L2): validateModifierGuards grows one more modifier arm (--allow-dirty, refused alone) in the flat if-chain this function IS; every sibling arm is the same 6 lines and splitting one out would hide it from the guard roster ack complexity ed05ee0357d016f1 49 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack complexity ed10de1685178c0e 21 cid=18c2d19fcd3157cf R1: +2 ccx on editplan::prepare and +1 on editpreview::run — ONE guarded early-return each (the redaction-marker gate moved to where the replaced bytes are known); both were already over the bar before this change and neither gained a nested branch ack complexity eea83c3db0f03d69 247 cid=6fe919c0dd008ef5 by=src/* capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. @@ -876,6 +878,7 @@ ack params 28c51bbd0032f9ec 9 cid=bdd33965f128de92 bench/capsweep write_screen r ack params 3478654139c90f0f 7 cid=8524565c31be122d review of #219: the run= relativity claim is CONDITIONAL, so the predicate has to reach every site that states it — prLegendText and writeFlipHeader take rootRelativeRuns (and its root= attribute) because the legend sentence and the command spelling must be decided by ONE fact, testmap.h runsAreRootRelative; a legend that claims a relativity the spelling does not have is the defect this round fixes. The duplication row pairs that 19-token predicate with mcp.h mcpOmitsGitVerbs, which asks a different question about a different field and shares only the shape of a two-term boolean return — merging them would name neither. ack params 3561d0281d324276 17 cid=6013544ad9e00162 now=17 was=15 p=src/serialize.h:6226 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path ack params 382f69e9fab00162 7 cid=3f6e5cc8c76d7cb6 now=7 was=0 p=src/graph.h:5207 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack params 39588f57bd7b46b5 9 cid=9adfbd942f397ffd now=9 was=8 p=src/mcpverbs.h:561 whereis-lean fix round 1 (review C1): the --whereis-full-text flag and its MCP full_text twin, one change: validateModifierGuards gains its with-whereis refusal, whereisText its trailing defaulted fullText parameter, kBoolFlags one row and kHelpTail the flag's four help lines (PREVIEW now names the same-page replay) ack params 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack params 453ce415b663d773 6 cid=27e74116f23adc21 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 45bb9f7c5bc37916 12 cid=c8ed741b38b4c0a9 now=12 was=10 p=src/serialize.h:6655 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows @@ -1586,6 +1589,7 @@ ack verbosity 2bea60141008e4d0 80 cid=0f6117d2b7b8058c now=80 was=0 p=src/receiv ack verbosity 31105421c3ce88ae 249 cid=86626441189f2998 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack verbosity 3298be65bf6058ef 1265 cid=7327ba02472bb8c7 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. | prior: M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. ack verbosity 3561d0281d324276 277 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path +ack verbosity 3797b511eae7d123 94 cid=604f2f07ebaf6c80 now=94 was=93 p=src/cli.h:3133 whereis-lean fix round 1 (review C1): the --whereis-full-text flag and its MCP full_text twin, one change: validateModifierGuards gains its with-whereis refusal, whereisText its trailing defaulted fullText parameter, kBoolFlags one row and kHelpTail the flag's four help lines (PREVIEW now names the same-page replay) ack verbosity 380b7de5df1cfd73 100 cid=5c6aaf66598466c8 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack verbosity 3877dd1e9b4ae997 66 cid=c7ed26e6f4cf641f P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack verbosity 38c814a492cc9c8a 176 cid=a9f0f1c829b158bd by=src/* Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: A2 (dropped_positive, 2026-09-03): emitForLensJson gained the droppedPositiveStanza, mirroring the existing overCeiling/notesStanza envelope-key shape; self-churn is this round's own fresh edit. @@ -1663,8 +1667,8 @@ ack verbosity a783c75feea9f253 114 cid=7345e4b8de2407c9 2026-09-06 stranger-audi ack verbosity a8b774025a21bdc6 338 cid=2fdcb1fc6fa90041 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: P17 slice+edit_check join MCP batch: runBatchSub gains two else-if arms (each calling the standalone builder, sliceText/editCheckText, so a batched answer cannot drift from the live one) plus three argument reads; dispatchMcpLine gains the post_check boolean read for the three edit verbs. Both are dispatch chains growing in kind. runBatchSub is NOT decomposed in this commit on purpose: lane L6 is changing this same function's sub-query GRAMMAR in this wave, and restructuring it here would guarantee a merge conflict over a concurrent lane's work — the decomposition is recorded as a follow-up, not skipped silently. ack verbosity a92c1b3a424113ab 196 cid=ff7446895e1ecbfe lane xcode-arm64 (4f679ede): release.yml's build job (and the jobs key that holds it) carry the deployment-target export with its empty-target refusal, the exact-compare otool minos read-back, and the Xcode/clang++/llvm-profdata record step with its empty-DEVELOPER_DIR refusal and physical-path (pwd -P) prefix check, each fail-loudly guard pinned by portablebuildcheck #2i | prior: lane xcode-arm64 (f3fd03ca): release.yml's build job gains the MACOSX_DEPLOYMENT_TARGET export, the exact-compare otool minos read-back and the Xcode/clang++/llvm-profdata record step, each failing loudly on a missing pin with its rationale comment; the first two ran on the macos-x64 leg through 0.6.1 ack verbosity a9f76a08efdb3d50 84 cid=93c35f3d948f24a3 F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. | prior: capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept -ack verbosity aa0c0f6198e30964 517 cid=d2abc06e563ab941 t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign -ack verbosity ac9a2be19aa5cd79 653 cid=842f93fca4a9a61f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack verbosity aa0c0f6198e30964 597 cid=8853bb2128080242 now=597 was=594 p=src/compactlegend.h:669 whereis-lean fix round 2 (review D1): a kind-filtered page discloses its dropped filter, one change: writeWhereisListedPage +the present-only PREVIEW sentence and the escaper passed to writeWhereisTextCut; kCompactAttributeReadings +1 reading (t_cut uncarried=) | prior: whereis-lean (t= preview + parse read-back), one change: labelBranchRowsByParse gains the repo root so the parse batch reads back only the blobs it chose (internal, one caller computeWhereis); runCrossRef +2 cx is the --detail lift of the preview; writeWhereisListedPage +10 LOC the PREVIEW clause and ; kHelpTail +3 help lines and kCompactAttributeReadings +2 readings (t_bytes=, t_cut rows=) +ack verbosity ac9a2be19aa5cd79 793 cid=d6dc6cd206172755 now=793 was=785 p=src/cli.h:2053 whereis-lean fix round 1 (review C1): the --whereis-full-text flag and its MCP full_text twin, one change: validateModifierGuards gains its with-whereis refusal, whereisText its trailing defaulted fullText parameter, kBoolFlags one row and kHelpTail the flag's four help lines (PREVIEW now names the same-page replay) | prior: whereis-lean (t= preview + parse read-back), one change: labelBranchRowsByParse gains the repo root so the parse batch reads back only the blobs it chose (internal, one caller computeWhereis); runCrossRef +2 cx is the --detail lift of the preview; writeWhereisListedPage +10 LOC the PREVIEW clause and ; kHelpTail +3 help lines and kCompactAttributeReadings +2 readings (t_bytes=, t_cut rows=) ack verbosity b496a273ae1564ef 70 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack verbosity b8c5550b5e3dc150 65 cid=200408e3ce35c6b8 lane T 2026-09-05: install.sh --hook banner re-worded to disclose the v3 capture (Edit/Write targets, MCP symbol/file arguments); the matcher rewrite is the fix for MCP rows being invisible (hookcheck section 14) ack verbosity bb42e692522f57ff 101 cid=aa3e0713df809f6d capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement @@ -1691,6 +1695,7 @@ ack verbosity dda0db55532bd5e1 135 cid=8515eb8f5b5e8953 answer-safe --edit-check ack verbosity dda343d7d36edaba 205 cid=e8803c763bec269d L10: printLintRuleTallyRow's compiled= param and runLintRules' uncompiled-query mapping loop disambiguate a lint-rules query that failed to compile from one that legitimately found zero matches (see docs/PLAN lane L10, finding 3) — deliberate, backward-compatible (defaulted param, incompatible=0) ack verbosity e14feca13c7ae680 234 cid=1c4b1193a2ce87a8 capture-audit 2026-09-04 wave-2 merge: L2 M4 (+24 LOC: verifiedNoteTargets note rows, candidates=/capped= accounting, detached=1 — handoffcheck M4) + L8 M21(b) (hoRunners/hoEsc and runAttrDisclosed so a row carries run= or run_unknown=1 — testrowruncheck 6); the merge kept ONE runner index and spliced kRunHintLegendClause into L2's legend constant (Head/Tail) so no seventh wording of the run rule exists | prior: M4 (lane ca-L2): writeHandoffPacket +24 LOC for three emitted facts (run= on , detached=1, candidates=/capped=) and the comments recording why the note match changed; complexity held at 56->57 minor by extracting verifiedNoteTargets() and kHandoffLegend ack verbosity e1bd5678b68badf3 630 cid=c66bec3708824489 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. +ack verbosity e31941708575f25e 120 cid=da5e6a258c290171 now=120 was=105 p=src/crossref.h:4300 whereis-lean fix round 2 (review D1): a kind-filtered page discloses its dropped filter, one change: writeWhereisListedPage +the present-only PREVIEW sentence and the escaper passed to writeWhereisTextCut; kCompactAttributeReadings +1 reading (t_cut uncarried=) | prior: whereis-lean (t= preview + parse read-back), one change: labelBranchRowsByParse gains the repo root so the parse batch reads back only the blobs it chose (internal, one caller computeWhereis); runCrossRef +2 cx is the --detail lift of the preview; writeWhereisListedPage +10 LOC the PREVIEW clause and ; kHelpTail +3 help lines and kCompactAttributeReadings +2 readings (t_bytes=, t_cut rows=) ack verbosity e419c663ed6ad8b5 276 cid=23921af3717c11d0 lane xcode-arm64 (4f679ede): release.yml's build job (and the jobs key that holds it) carry the deployment-target export with its empty-target refusal, the exact-compare otool minos read-back, and the Xcode/clang++/llvm-profdata record step with its empty-DEVELOPER_DIR refusal and physical-path (pwd -P) prefix check, each fail-loudly guard pinned by portablebuildcheck #2i | prior: 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit ack verbosity e514a69013d0934c 90 cid=9f0bcb151f03e21f L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites ack verbosity e7f50422948f2c09 70 cid=6c7e771d42d81c16 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e190411..837ce6a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,390 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +## [0.6.7] — 2026-10-11 + +### Fixed — `--impact`: a selector matching several definitions keeps the `via="name"` hedge in its per-file rollup + +When the symbol window is cut, `--impact` folds the whole reach set into one `` row per file. A bare selector such as +`--impact=arrange` can match several definitions (a method and a free function of the same name), and the fold dropped +the `via="name"` hedge its symbol rows carry, so a file reached only by name read as a plain dependant. On a selector +that matches more than one definition, an `` row now carries `via="name"` when every reached symbol in the file is a +`via="name"` row, and `by_name=K` when K of its `syms=` are (JSON: `"via":"name"` / `"by_name":K`; the MCP `impact` tool +the same). It does not say which definition a file depends on, and an all-proven row stays plain. A selector matching one +definition (bare or qualified) folds byte-identically to before. + +### Changed — the default map names the files and entry points its ranked rows cut: an inventory tier after the rows + +The map prints the top 200 symbols by rank, so a file whose best symbol ranks just below the cut, and a program's `main` +(called by nothing, so ranked last), were absent without a word. When the ranked rows left out a file with code, the map +now ends with an `` tier, after rows that are unchanged. It holds `` rows for a top-level +`main` the index can prove (with its first callees by name; a script language's `main` only when its own module scope +calls it, as Bash's `main "$@"`; Swift none), and an `` row per directory with the code files not shown, names only; +test, doc, example and bench directories and directories with no code print a count alone. A map that shows every code +file carries no tier, byte-identical to before. The tier is off under `--max-tokens`, for an `in=DIR` map and for a +multi-root workspace, and `--no-inventory` (new) prints the previous shape. Two runaway guards disclose themselves +(`entries_capped=` past 256 entries, `names_capped=` past 2,000 names, whole directories dropped; `entries_next=` and `names_next=` list the rest). Entries are found by name +convention only; a manifest's declared entry (`package.json` `main`, `console_scripts`, Cargo `[[bin]]`) is not read, and +the legend says so. Measured with the built binary on the 11 permitted public repositories: the median map grows 7.0% +(+2.2% to +31.3%; koa's is byte-identical), the ranked rows are identical on all 11, and tmux `input.c`, +`key-bindings.c` and `main`, hono's `src/validator`, textual's `drivers/` and `command.py` and htop's `main` and its +signal, affinity and scheduling files are now named. Whether naming a file lets an agent find it is not measured here. + +### Changed — `--grep` serves a text-shaped literal's string tier, `--impact` rolls its reach up per file, a clean-tree `--quality-delta` prints its verdict + +Three ways a lean answer was missing what the question named. +- **`--grep`.** A pattern such as `split-window` has few code hits and one string literal that registers the command + (`cmd.c:206`), and the code tier left the defining file out. The string tier is now served beside the code tier when + fewer than 8 classified code hits (source and test files; not docs or shell, YAML, TOML or JSON) exist, ranked, and + `ref=` names the first site in code, never one in a comment or a string. An identifier-shaped pattern with 8 or more + code hits is byte-identical. tmux `--grep=split-window`: 16,108 → 5,453 B, defining line first. +- **`--impact`.** Beside a cut window, the whole reach set is folded into one `` row per file (20 at most), with + umbrella importers counted. tmux `--impact=grid_set_cell` now names `input.c` and `window-client.c` (it named neither); + 5,907 → 5,099 B. A selector that reaches only through a type (`--impact=grid_cell`) still reaches nothing. +- **`--quality-delta` and `--edit-check`.** On a tree identical to HEAD, a clean answer prints its verdict and the + per-kind count of the stale acks it folds, not the acks. The fold ships only when the answer is at least 25% shorter + than the full document; `--show-stale` (new) prints the acks, and the `--edit-check` verdict's `rows_next=` names the + `--limit` that lists every caller row. A dirty tree, a non-git root and every answer carrying a regression print + everything, byte-identical to before. This repository's clean-tree `--quality-delta`: 14,594 → 978 B. `--edit-check=escapeXml`: + 3,322 → 1,122 B. Both stay above the sizes the lane aimed at (600 and 400 B), because the verdict keeps the floor + disclosures. + +### Changed — `--seams` counts links between documents instead of reporting them as untested seams, and `--report` ranks production god files + +A markdown link from one document to another is a call edge between two Section symbols, so `--seams` listed +`. -> docs untested=30` on this repository (66 on code-review-graph, 17 on aislop), every one a doc-to-doc link. An edge +whose caller and callee are both document sections (a markdown heading or a data key) is now skipped and counted in +`doc_links=` (53 on this repository; 69 and 17); the same sweep finds no such edge left. `--report`'s god-file list put +`test/lib/clean-env.sh` first: depended-on files under a test, tests or bench directory whose dependents are all test +paths are now counted on one `layer=test` line (285 more files here; top file named) and the list starts with +`src/model.h`. `--deps` godfiles rows carry `layer=` when the file has a built-in layer. An answer with no doc links and +no such test file is byte-identical to before. + +### Changed — `--whereis`: a long line's `t=` is a 200-byte preview, and the branch-label parse holds only its batch + +A `--whereis` row's `t=` repeated its whole source line, so on a hot name the long lines were most of the answer. On this +repo's own tree (165 local refs), `--whereis=lego` was 129,314 B, and 38 of its 60 rows held lines of 206 B to 7.3 KB: +a Python dict, markdown paragraphs, a legend literal. + +- A line over 300 bytes prints its first bytes, cut on a UTF-8 boundary, plus an ellipsis: 200 bytes at most, the + ellipsis included. The row carries `t_bytes=`, the whole line's size. A line of 300 bytes or less prints whole, so a + cut always drops at least 100 bytes. A 202-byte line cut to 200 saved 2 bytes and cost its page ~330 B of disclosure. +- A page prints its previews only when that makes it shorter. The served page is rendered cut and whole, and the cut + one is served only when it is strictly shorter both as written and as the compact dialect delivers it; then every + long line on it is cut. A page whose long lines sit just past 300 B (one 395-byte line saved 92 B and its disclosure + cost 250 B) prints whole, byte-identical to before. The default listing is chosen on the whole-line pages, so a + preview never changes it. +- A page that cuts carries ``. Its `next=` replays THAT page with `--whereis-full-text` (new; MCP + `full_text`, a boolean): the listing the page served, its `--limit`/`--offset`, `--with-history` when given, and every + line whole. Nothing else is lifted: the parse guard, the row cap and the window stay, so the replay is the same rows + with the same `kind=` labels. `--detail=1` still lifts the preview with the row cap and the parse guard, but it is a + different answer (it relabels a guard-left `kind="text"` call site as a ref, and unpages a refs or all listing: 25 MB + for `--whereis=EXPECTS --whereis-listing=all`), so `next=` does not name it. The full legend's PREVIEW clause and the + compact `t_bytes=` / `t_cut rows=` readings ride only on a page that cuts. MCP `whereis` serves the same page; its + `next=` is the CLI spelling, as ``'s is. +- Measured on this repo (165 refs), base vs head back to back: `--whereis=lego` 129,314 → 18,616 B (−86%), every row + kept, and its `next=` replay is 129,337 B (the base page plus the 23 B of the listing it names). + `--whereis=EXPECTS --whereis-listing=all` 40,241 → 18,540 B, replay 40,241 B (was 25.2 MB). Over 25 names × 4 + listings (100 answers, 2,869,969 → 1,145,411 B): no answer is longer than before, no default listing changed, 43 are + byte-identical, and in the 57 that cut, a cut `t=` is a prefix of its line and each `next=` replay lists the same rows + as the page and as the base answer. Gates: `crossrefcheck` 5h, 5h-C2 (7 line widths × 2 listings × 2 legends: every + page is its whole twin or strictly shorter) and 5h-X2 (a guard-bound page's 44 `kind="text"` rows replay whole). +- Peak memory: the scan kept every candidate blob's bytes, and copied them into each (blob, path) job, until the runaway + guard picked its batch. Now it records sizes, and the batch reads back only the blobs it chose. `--whereis=lego` + peaks at 0.88-0.94 GB (was 1.42 GB) and `--whereis=DISCLOSE` at 0.85-0.90 GB (was 1.16-1.22 GB). What remains is + the index (~0.45 GB on a name with no parse batch) plus the parse of up to 256 blobs / 64 MiB. A chosen blob that does + not read back whole confirms nothing: its rows read `kind="text"` and `` counts it. The second read costs a + little time: interleaved base/head ×5 at 1-min load 4-7 (medians), `--whereis=lego` 4.73 → 4.91 s, `--whereis=DISCLOSE` + 4.61 → 4.64 s, `--whereis=lego --detail=1` 18.15 → 19.71 s (+9%), at peaks of 1.42-1.60 → 0.90-1.06 GB (lego). + +### Fixed — `--for`: the `` budget is charged for the ``/`` stub it serves, not the full render + +The default answer collapses ``/`` to a ~120 B counted stub, but the `` budget was charged the full +render before the collapse. On a corpus whose question names many interfaces, the full `` filled the budget: one +measured Python corpus served 4 of 40 signature rows in a ~4.6 KB answer under the 7.5 KB ceiling. + +- A section served as a stub is charged at the stub's size (the CLI lens and the MCP `for` tool share one rule). +- Without an explicit ceiling, `--sections=lego,compose` keeps the stubbed answer's `` budget, so the stub's `next=` + restores both sections with the same `` rows, byte-identically. Under `--token-budget` (MCP `budget_tokens`) + the restored sections are charged in full, so the hard bound holds. +- Measured on 32 `--for` questions over 8 corpora: 19 answers byte-identical, 13 carry more signature rows + (4 to 29-33 on the corpus above), and every row the old answer showed is still shown. Gate: `forsectioncollapsecheck` (13). +- Under `--token-budget` with `--with-graph` or `--detail`, the `` budget now leaves room for the graph block, or for + the first `--detail` body (whole up to a quarter of the budget, else cut with a `next=` for the rest). Before, they rode + past the budget and the first body shrank to its first line. With BOTH flags nothing is reserved: on the measured + questions that cut a needed signature row, so those answers keep their rows, serve the first body at its floor, and say + `over_ceiling="1"` when they overshoot. Without `--token-budget` nothing changes. Gate: `estchargecheck` #11 A7R. +- By-design break, owner ruling 2026-10-09: `--token-budget=2000 --detail=20 --with-graph` on this repo's `src` no longer + fits its allowance (6029 B vs 5428 B). It keeps every signature row the same budget shows without the two flags, says + `over_ceiling="1"`, and the cut first body carries a `next=` that returns exactly the lines it cut. The over-budget note + now names the kept rows, the graph block and the first body's floor. It used to blame the header floor, which was false + here: the header alone is ~1.8 KB. The old bar still holds for each flag alone and for both flags when the rows fit. + Gates: `estchargecheck` #11 A7 twin / A7B / A7R kill+next, `ceilingverdictcheck` (2b). +- Under `--token-budget`, a spent ceiling no longer drops the compact `` section without a trace: it ships as + ``, where `next=` is the same question without the budget. A partly cut + `` carries the same `next=`, and a cut `` carries the `--for … --limit=N` file page. These bytes are reserved + from the signature rows, so measured at 2000 tokens over 32 questions: 192 hop rows named on 32/32 answers (none before), + 65 signature rows fewer (still disclosed by ``'s own `next=`). Gates: `estchargecheck` #11 A7D, `fordisclosurecheck` + #1b/#1c. +- The default (compact) answer fits an explicit `--token-budget` where `--legend=full` does on the measured band below, and + says why where it cannot. Charging the stub as served + removed slack that had been absorbing the compact header's extra bytes (its row budget is charged no more header than + the full dialect's), so on this repo's `src` the default `--for='rank symbols by pagerank'` answer went over budgets + 1300-1500 and 1600-1700 that the full answer met. Four changes, each only on an answer past its exact budget, except the + two shorter compact clauses, which ride whenever the hops cut or the clause drop does: + - A compact answer that is still over after paying for `` `next=` drops rows to pay the measured overrun too, and + is served only when that fits. The rows it drops are the ruled exemption (`compactlegendcheck` (P1-B)). The + `--detail` + `--with-graph` shape keeps its rows, as ruled. + - The ``/`` readings drop when ``/`` is served as a stub: no such row is printed. + - The compact hops-cut clause and the compact clause-drop note no longer repeat `total= shown= capped=` / `next=` + readings the dialect defines anyway; the compact echo rung no longer claims a `task_echo` drop (it has no echo). + - The last-rung note blames the header floor only when the header alone is past the allowance; otherwise it says the + header fits and the rows and sections served with it exceed the budget. Both dialects. + Measured on `src`, `--token-budget=1100..1700` step 50: the default fits at every budget the full answer fits (13/13 + read); at 1100-1250 both are over, each with a true reason. Not everywhere: where both dialects sit at the rank 1..4 + row floor and the full answer fits by a few tokens, the larger compact header can still go over + (`--for='serialize the map'` at 1200: 1435 vs 1196 tokens); it then says so, with the true reason and the `` + `next=`. Gates: `compactlegendcheck` (P1-B), (P1-S), (P1-S2), `estchargecheck` #11 A7D (+ the `--legend=full` twin), + C3 re-pinned to a doc-less twin corpus. + +### Changed — `--for`: the function a question names gets the first hop row, with all its callee names + +The compact `--for` answer hop-expanded only the six best-ranked rows, inside a ~1 KB allowance. A question that names +the function it is about ("How does the JWT middleware verify a token?") often ranks that function below a dozen symbols +that repeat the question's other words, so its callees — the names an agent needed next — were never served. + +- A ranked function or method DEFINITION (not a test symbol, not a C prototype) whose whole name, leading `_ # $ @` + removed and ASCII case folded, equals a word of the question is that question's owner, when the word has such a + definition in the top 24. It gets a `` row first in ``, past the six-row cap; every same-named + definition in the ranked pool is its own row. At most three owner rows; the rest are disclosed as + ``. A small list of common English words (get, then, write, …) ranks after the + other owner words, so verbs a question merely uses do not take the owner's slot. +- An owner row lists its callees up to 100 names (a runaway guard); past it, or under an explicit budget that cuts it, + `` names the full list. The hop-slot rule still applies: an owner with no proven callee + edge has no row and is counted in `noedge=` once. Name-only callees keep `via="name"`. +- By name, not by resolution: `qword=` says the name equals the word, never that the symbol is the only or the true + implementation. Default regime: owner rows ride on top of the hop allowance, so every earlier `` row is still + served (a moved owner can only free room), and ``/`` are unchanged: the legend clauses the owner rows + switch on (`qword=`, and `via="name"` when only an owner row carries it) are not charged to the signature budget. Under `--token-budget` they are funded first. A question that names no + owner, `--json --for` and the MCP `for` verb are byte-identical; `RIPWIRE_NO_OWNER_HOP=1` restores the previous + answer. Measured on the 32 `--for` rows of the round-1 comparison set: the two in-scope rows now name 5 of 5 gold + callee items they missed (were 0 of 5); 23 of 32 answers are byte-identical, 1 differs only in `noedge=`, and 8 grow by 0.2–1.1 KB (+1.4% over all 32). Gate: + `test/ownerhopcheck.sh`. + +### Changed — `ripwire wrap` recommends `--for` at its default budget + +The CLI-first paste block (claude, codex, opencode, openclaw, hermes) and the aider repo-map line now read +`--for=""` instead of adding `--token-budget=2000`. That budget is a ceiling. Over 40 `--for` questions in +eight open-source repositories (one release binary, `--no-cache`) it halved the median answer (4.6 KB against 9.2 KB). +It dropped signature rows on 35 questions and call-hop rows on 24. Only the signature cut carried `capped="1"` and a +`next=`. The file tail and two partial hop cuts carried `capped="1"` alone, and on 24 questions the whole call-hop block +was left out with no marker (22 of them had hop rows). The default budget and every other knob are unchanged, and +`--token-budget=N` still works when passed by hand. Only the printed `wrap` text changes. +### Changed — `--expand` / `--outline` ship no ride-along map when every definition they name is served + +`--expand=NAME` on a name with several definitions, and `--outline=NAME`, used to carry the generic top-200 ranked map +beside bodies that already answered the request (on this repository `--expand=subtokens`: 26,752 B, of which 19,444 B +was map rows that never mention the symbol; `--expand=emitTo` served the whole 24,788 B file because the whole-file +comparison priced the bundle with that map). When every requested definition is served the map now defaults to +`top-k=0`: the root carries `topk_default="0"` and `map_next="ripwire ROOT..."` (the call that prints the map, for a session +that skipped orienting: every root of the call plus its crawl-scope flags, shell-quoted). When a byte budget cut some +definitions the root names exactly those in `unserved_total=N` (the true count) and a shell-quoted +`unserved_next="--expand=FILE:LINE:NAME,..."` instead of shipping the map; past 16 cut definitions it serves the first 16, +`unserved_listed="16"` says so, and a larger `--pack-budget-bytes` serves the rest. The whole-file comparison therefore prices the +payload only. An explicit `--top-k=N` keeps the previous shape. Language-neutral: the decision reads the record of which +requested definitions were emitted. + +### Fixed — the cross-branch verbs refuse when the blob batch cannot run + +`--stray-content`, `--stray-content --abi`, `--stray-content --plan`, `--eval-stray` and the MCP `stray_content` tool read +every branch's content through one `git cat-file --batch` whose sha list is written under the cache directory. With `TMPDIR` +naming a directory that does not exist the list could not be written, every blob read as absent, and the answer was +`merged="1"` for a branch holding unmerged work (`--abi`: exit 0, no drift). Each now exits 1 (MCP: `-32603`) naming the +cause and `TMPDIR`, with nothing on stdout, the way `--whereis` refuses on the same condition. + +### Fixed — a git history walk that could not be read says so, and is retried + +A `git log --name-only` that failed (an unreadable object, a bad ref) came back as an empty stream or as the newest part of +the window and was stored under the (repo, HEAD, window) key, so `churn=` and `amp=` read as "no change history", or as the +whole window's count, until HEAD moved, with no tell. A git that would not run at all (not on `PATH`) in a repository read +the same way, as an unborn branch. A walk git could not finish is now never cached, so the next call walks again (a blob an earlier build stored from such a +walk is not read either: the history-walk cache scheme moves, see the versions note), and every +document that prints `churn=` or `amp=` (the map under `--metrics`, `--for`, `--pack-task` and `--grep` with `--metrics`, +`--around`) carries `history_unread="1"` on its root (`"history_unread":true` in JSON) beside a comment that defines it and +names the cause. It means `churn=` and the co-change half of `amp=` count only the commits that were read, so both are +floors: when git did not run, `churn=` is absent and `amp=` counts callers only; when git stopped part-way, they count the +newest part of the window it read. It does not mean the other numbers are affected, and a walk that read an empty window (a +repository with no commit) stays silent; a HEAD that is corrupt rather than unborn still reads as that empty window. Not yet +covered: `--cochange` (and the MCP `cochange` tool), `--rank-by=churn` and `--owners` run their own git walks and do not yet tell a +failed one from an empty one (`commits="0" partners="0"`; "found no commits in its window"; exit 1 with "git unavailable / +no history"). + +### Fixed — a non-ASCII `--help-task` prompt no longer trips the sanitizer build + +`lowerAscii` converted every byte of the prompt through an implicit `char` to `unsigned char` conversion, which the +sanitizer build (`implicit-integer-sign-change`) aborts on for any byte at or above 0x80. The same shape sat in the +router's name-shape and `FILE:LINE` predicates, the LSP header parser and the extension test; each now narrows +explicitly. + +### Changed — `--whereis` labels another ref's row by parsing its blob, lists a line several refs share once, and reads refs from tree objects + +A row on another ref used to take `kind="def"` from a lexical reading of its one line, and any such reading errs both ways: +a looser one called call sites definitions (`if( !task.empty() ) { out += escapeXml( task, esc ); }`, `go f(a)`, `@f(1)`), +a stricter one lost real definitions (`#define NAME(`, `decltype(auto) NAME(`, `@Test(…) public void NAME()`, +`#[derive(Debug)] struct NAME`). Measured on this repository (162 local refs), `--whereis=escapeXml`: the default page +went from 60 rows of `total="2197"` (54 of them call sites) to 2 rows, both the declaration. + +- **Another ref's `kind="def"` comes from a parser.** A ref holding HEAD's own blob of a path takes HEAD's index labels, + line for line. Any other blob is parsed once per distinct blob and path by the index's own extraction — the same + language rule, tags query and symbol model — and a definition it captures for the name labels the nearest row the way + HEAD's index labels HEAD's. Only a blob holding a line a loose lexical test calls worth parsing is parsed; a blob + without one reads `kind="ref"`. The root says `ref_labels="parsed"` whenever another ref's row is in the answer. + Arms: one well-formed file per language family (C, C++, ObjC, Python, TypeScript, JavaScript, Java, Ruby, Go, Rust, + Swift, C#, Bash, Markdown) whose every marked line reads as its marker says and as HEAD's index reads the same bytes; + the twelve definition shapes the stricter reading lost; a branch-only `#define` and annotated Java method. +- **A runaway guard on that parse, disclosed.** At most 256 blobs or 64 MB per answer, most rows first, under every + listing, the default page included. A blob the guard leaves, or one the parser cannot finish, confirms nothing: its + parse-worthy lines read `kind="text"` (listed beside the definitions, never `kind="def"`) and an `` element counts them; `--detail=1` lifts the guard. Its `next=` replays the same question with + `--detail=1` (an explicit `--whereis-listing=` rides with it); an MCP `kind` ref filter is not carried, as `` does not carry it: the CLI has no whereis ref filter. +- **A definition line several refs hold is one row.** Under `listing="defs"` a `kind="def"` (or `kind="text"`) row outside + the checkout whose path and line text other refs hold too is printed once, for the first of those refs by name, with + `refs="N"`; the root's `folded=` counts the rows it stands for (on the first page, `shown + more + folded + refs count = + hits`). `hits=` still counts every copy and `--whereis-listing=all` prints each ref's own row. +- **Faster.** Every ref's tree is read from git's content-addressed tree objects, each distinct tree once and a level at a + time through one `git cat-file --batch`, instead of one `git ls-tree -r` per distinct tip (the same rows in ls-tree's + order and spelling — an arm compares quoted and unquoted names with ls-tree itself); a blob several refs share is + scanned once. The parallel per-tip listing stays the fallback, used when ripwire runs on a directory below the top of + the work tree. + +What stays a floor: a definition the loose lexical test misses (a signature wrapped before its parameter list closes, a +name alone on the line under its return type, or a definition with no parameter list and no declaration keyword before +the name: `name: function (`, `pub const NAME`, a C# property) is a counted ref when nothing else in its blob earns the +blob a parse. A parsed blob is labelled as HEAD's index labels the same bytes, so a file its grammar cannot parse +cleanly (a fragment, a file mid-edit) can lose a definition or show a false one exactly as HEAD's index would. +MCP `whereis` takes no `detail` argument, so there the guard's `next=` names the CLI flag. Its description keeps its 701 +characters, the sentence calling a branch row's label lexical replaced by the parser one, and the +`tools/list` manifest shrinks by 5 B, to 47,109 B on this release's tree (the dropped em dash is six bytes of JSON; every +other tool is unchanged). Gates: `test/crossrefcheck.sh` (5b–5e), +`test/selectorhonestycheck.sh` (HEAD's rows, labelled by the index), `test/pagingsweepcheck.sh`. + +### Changed — a C++ call named like a standard container member binds no in-tree namesake unless a rule ties its receiver to one + +`out.push_back( c )` on a `std::vector&` parameter, `s.size()` on a `std::string`, `getV().empty()`: a C++ member +call whose receiver no rule typed bound by NAME to whatever in-tree class defines the member — a hedged via="name" row, +but a row, and every graph reader counted it. On this repository's own map 14 of the top 20 PageRank rows were such +names (a small-vector's `push_back`, a path wrapper's `c_str`, every `empty` and `find` under `src/`), and +`--callees=escapeXml` listed 8 of them beside its 4 real callees. + +- A standard container is a concrete, non-virtual class template, so a receiver of standard type never dispatches into + the tree. Such a call is now DECLINED after the ladder decides it: no edge; counted in the header's `declined=` and + in `declined_calls=` on every definition it would have bound (so `--callers` / `--impact` / `--safe-delete` say the + count, and no definition becomes a dead-code candidate for losing a by-name caller); a legend clause of its own, + charged only where it fired. `--callees` (and the MCP `find_symbol` twin) names those calls once: + `` (JSON `"stdm":{"n":"…","calls":11}`). +- The names are data (`src/externalnames.h` `kCppStdMemberNames`, 78 container / string / view / span / optional / + smart-pointer members, generic verbs such as `get`, `count`, `swap`, `read`, `write` excluded). A receiver a rule + types is never touched — a typed local, parameter, pointer or member field, `this->`, an implicit-this call — and + neither is one whose recorded written type names an in-tree class that may define the callee: an alias of the in-tree + container, a field written as one, a namespace-scope, file-static or extern variable (`ns::List g; g.size()`), the + argument of a `std::unique_ptr` / `std::shared_ptr` / `std::optional` reached through `->`, a `->` through an + in-tree class with its own `operator->` or a class the tree does not define (a Qt or boost pointer), or a `->` through + a `typedef` / `using` alias of a standard smart pointer or optional (`typedef std::shared_ptr ListSP; ListSP + p; p->size()` — the alias record keeps no template argument, so `typedef std::unique_ptr StrUP` keeps its + hedge too) or of one of those wrapper classes (`using QP = QSharedPointer`) — each keeps its hedge. `std::unique_ptr p; p->size()`, `std::vector v; v.size()` + (the container's own member) and `IntVec* p; p->size()` through an alias of `std::vector` are declined. +- The stated floor — declined even where the object is an in-tree class, because the index records no written type for + the receiver: a call result (`h.list().size()`, `makeList().size()`), an iterator (`auto it = v.begin(); it->size()`, + `it->second.empty()`), an `auto` local, a dereference, a second element access (`m[ a ][ b ].empty()`), a range-for + `auto`, a template parameter, a field two hops off its root, the template arguments of a type written unqualified (`QList l; l.size()`), a member + field written `std::optional<…>` reached through `->` (`std::optional o_; o_->size()` — a field's template + argument is read for `std::unique_ptr` / `std::shared_ptr` alone), and a `typedef` declared inside a function body + (its alias target is not recorded). +- ONE element access — `m[ k ]`, `m.at( k )`, `v.front()`, `v.back()` — of a named container or a field one hop off a + typed root reads the element type off the container's recorded written text (a local's, parameter's or namespace-scope + variable's qualified type, a member field written as a standard sequence container or map, a `typedef` / `using` alias + of one, alias to alias): `rule->bindings_[ "rspfile" ].empty()` through `typedef std::map + Bindings` keeps its hedge on `EvalString::empty`, while `std::vector& v; v[ 0 ].empty()` and + `std::map> m; m[ k ].empty()` (an element written in `std`) stay declined. This reads a written + type, not a resolved one: it keeps the ladder's via="name" hedge, never proves the edge. +- C++ only, each other language for a stated reason: C has no member functions; Rust receivers carry no declared type + yet; Go, Java, Kotlin, C# and Swift name their standard members after interfaces in-tree types implement; ObjC + message sends carry no receiver shape; Python, JS, TS and Ruby keep the builtin-method name gate. +- Measured on this repository (`--no-cache`, the same tree before and after): call edges 40,567 → 25,020, `ambiguous=` + 12,576 → 2,774, `declined=` 12,240 → 25,840; the top 20 PageRank rows carry no standard member name (14 before); + `--callees=escapeXml` count 12 → 4 plus the `` line. Gate: `test/stdmembergatecheck.sh`. + +### Changed — every `--callers` and `--safe-delete` caller row names its call sites as `file:line` + +"List every call site that must change" took two calls: a `--callers` row's `p=` is where the CALLER is defined, and the +lines to edit were only in the `--uses` follow-up. Each caller row now carries `sites_at=`, its calls of the called +leaf name (any receiver or qualifier) as pasteable `file:line` tokens (space-separated, ascending; two calls on one line +are one site), so the first answer holds them. A file-scope (``) caller, such as a test file calling a helper at top level, lists +its sites the same way. `p=`, `count=` and the rows are unchanged. + +- **Every dialect:** XML ``, `--json` `"sites_at":[...]`, the columnar `` column, and the MCP + `find_referencing_symbols` / `find_symbol` `calledBy` rows (with a `sites_note` reading). The sites are the `--uses` + call rows inside that caller: matched by name, so they are not proof each one binds to this definition. +- **A long list is cut, never silently:** a row lists at most 16 sites; a cut row carries `sites_total=N`, and `next=` + (the `--uses` call on the same selector) lists every site. +- **`--safe-delete`:** the bare-line `sites_l=` on its caller rows becomes the same `sites_at=`. `--edit-check` keeps + `sites_l=`. + +### Changed — `--lego` lists the implementors below a direct one + +`--lego=TYPE` (and the MCP `lego` tool) now walks the extends/implements edges to their closure: a class that extends a +direct implementor (`class Default( SimpleFormatter )` under `BaseFormatter`) is listed too, once, at its shallowest +depth, as `` — `via=` names the type it extends that reached it, `depth=` its hops below the +interface. The `` carries `transitive=N`; `implementors=` still counts the direct rows alone, and an answer with no +deeper row is unchanged. The deeper rows are one page of at most 200 (a runaway guard): a cut says +`transitive_shown=` and `has_more="1"` with a `next=` that pages on, and `--limit`/`--offset` (MCP `limit`/`offset`) page +them. `--lego` honors `--limit`/`--offset` only for this window; without one it keeps its `--max-tokens` notice. The +`tools/list` manifest grows 47,109 → 47,293 B on this release's tree (+184 B: the declared `limit` and `offset` arguments on +`lego`; 46,930 → 47,114 B on the lane's base). + +The walk below the direct rows follows only extends/implements clauses the index PROVES name the type above: a class +declared in a scope enclosing the clause (`class Leaf( Mid )` beside its own `Mid`, `b::Mid` inside `namespace b`), the +file an import binds the name to (Python `from m import Mid`, followed through a package's re-export; a JS/TS named import +or `const { Mid } = require( './m' )`), a C-family include, the Java/Kotlin/Go package directory, a Java qualifier naming +the package, or Ruby's constant scope. A clause bound by the NAME alone — no such evidence — is listed hedged, +`base_by="name"`, and the walk does not continue below it; a clause the evidence binds to a DIFFERENT same-named type adds +no row at all, and the `` counts those types as `transitive_other_base=N` (they are listed on the answer of the type +they do extend). A Java or Kotlin `import` and a C# `using` are not proofs. The direct rows are listed as before, and the +walk does not descend below a direct row whose own extends clause is not proven to name the interface (a cross-package +Java `import`, a Kotlin `import`, a C# `using`, an ES import of a CommonJS base). When such a row has subtypes, the row +carries `unwalked="1"`, the `` counts those rows as `transitive_unwalked=N`, and `unwalked_next=` names the first +row's own `--lego` call, which lists its subtypes. + +Every `` row of the targeted answer now says where the class is: `p="file:LINE"`, the `--uses`/`--callers` +spelling, so the implementor opens at its definition. The `` row and the ranked `--for` `` section keep the +bare `p="file"`. + +A Ruby module mixed in with `include M`, `extend M` or `prepend M` is now an implements edge as well as an ancestor: +`--lego=M` lists every class (and module) that mixes it in — `--lego=DSL` lists the classes that `include Rake::DSL`, +where it answered `implementors="0"`. A mixin inside a method body or at a file's top level still adds nothing. + +A JavaScript/TypeScript class FACTORY, `const Sub = Base.extend('Sub', …)` (nunjucks' node classes), is now a class +definition that extends its receiver: `--lego=BinOp` answers instead of "type not found", and `--lego=Node` lists the +classes made from it. The name must be the string the call is given — `$.extend({}, a)`, an unnamed Backbone-style +`Base.extend({…})` and `const A = B.extend('C')` stay what they look like (an object merge, no class) — and the parser must +show a class: the receiver is a class the same file declares (or another factory this rule keeps), or the call hands it a +class body (an object literal holding a method). So the `debug` package's sub-logger `const error = log.extend('error')` +and a string-first merge such as `_.extend('merged', { a: 1 })` stay no class; a factory whose receiver is imported from +another file and gets no class body is not read either (a stated floor), nor is a member-chain receiver +(`ns.Base.extend('X')`). + +A Python base clause is now read as Python evaluates it: `class Inner( Outer.Changed )` extends `Changed` (it used to +extend both `Outer` and `Changed`), `class Box( Generic[Key] )` extends `Generic` and not its type argument `Key`, +`metaclass=Meta` is no base, and a computed base (`with_metaclass( M, B )`, `namedtuple( … )`) names none. The +JavaScript twin: `class A extends ns.Base` extends `Base` (it read `ns`), and `extends mixin( B )` names no base. Parser +version 164, cache format 31 in this release (162 and 30 on the branch; see the versions note): re-index on first run. + +A bare name now resolves to a CODE definition whenever one matched: `--lego=Loader` answered about a `docs/api.md` +heading (`defs="3" implementors="0"`) while `src/loader.js` held the class, because the heading had the lower id. The +same pick serves `--around` and `--connect` (and their MCP twins). A heading or config key is still reached by its +`file:name` selector, and is still the answer when nothing else is named that. + ### Changed — a call bound by name alone keeps its rows, marked `via="name"`; typed receivers resolve A member call `x.m()` (or, where the receiver is implicit, a bare `m()`) bound to every in-repo definition spelled @@ -69,7 +453,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. change); the `--max-tokens=3000` maps of the eight repositories keep 337 rows (331 before the change). The gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses for every evidence rule). Ingest records the receiver chain and the Java/C#/Kotlin/Swift declarations - (`kParserVer` 156 in this release, see the versions note; `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. + (`kParserVer` 167 in this release, see the versions note; `kCacheVersion` 31), so a cache written by an earlier build is re-parsed. ### Added — `--quality-delta` gains a twelfth kind, `defect-shape`: four defect shapes code review kept finding by hand `kind="defect-shape"` rows name a known defect SHAPE the change added, in `defect=` (`src/defectshape.h`): @@ -118,9 +502,95 @@ an ALL_CAPS name followed by `(`), a call with a preprocessor directive inside i that spells a brace. Two floors of that skip: the tree's macro list holds function-like macros only, so a lower-case object-like macro defined in another file (`#define xyargs p.x, p.y` in a header) is read as one argument and its call can be a false format-arity row; and only a literal `#if 0` is skipped — `#if false`, `#if (0)`, `#if 0 && …` and the -`#else` of `#if 1` are read as live code. The qsnap blob scheme moves (19 in this release, see the versions note); the +`#else` of `#if 1` are read as live code. The qsnap blob scheme moves (20 in this release, see the versions note); the legend dictionary gains `r defect=`. +### Changed — a warm CLI call costs about a quarter less, `--whereis` on a many-branch checkout about 70% less, and `--whereis` refuses instead of printing a zero it never measured + +Every answer is byte-identical to before except the one `--whereis` refusal below. Measured on 2026-10-08 on a +10-core arm64 Mac, Release builds, median of 5 warm calls, against main at 60dd3b3f: + +| call | before | after | +| --- | ---: | ---: | +| `--callers` on a 3,100-file snapshot of this repository, no `.git` | 319 ms | 269 ms | +| the same snapshot as a one-commit git repository | 437 ms | 341 ms | +| five verbs on this repository's checkout (median of medians) | 448 ms | 342 ms | +| `--whereis` on a checkout with 139 local branches | 12.3 s | 3.7 s | + +- **The git ignore probe runs beside the crawl.** Every call on a git checkout asked `git ls-files --others --ignored` + before the crawl could start. The crawl now walks with the previous call's answer for that root while git computes + this call's, and the two are compared byte for byte before anything is parsed; when they differ, the crawl and the + cache load are redone on git's answer. Every answer is still git's own answer for this call. Under `--no-cache` the + probe runs before the crawl, as it did. +- **No shell is started for a markitdown that cannot run.** The doc pass started `/bin/sh -c markitdown …` for every + pdf/docx/pptx/xlsx on every call, and on a machine without markitdown the shell's "not found" was never cached. The + PATH is now searched the way the shell searches it, once per ingest, so installing markitdown is still noticed on + the next call. +- **The resolver reads dense arrays for a name with many definitions.** A module-scope call has had a caller node since + 0.6.2, so the test scripts' top-level `ok "…"` calls (about 650 shell definitions of `ok`) reach the candidate walks. + Those walks now read per-symbol arrays instead of whole symbols. +- **`--whereis` reads each git listing in blocks and lists each distinct tip once**, the listings in parallel, and + scans each blob once however many branches share it. + +### Fixed — `--whereis` refuses when it could not read the blobs, instead of answering `hits="0"` + +`--whereis` reads every blob through one `git cat-file --batch`, whose sha list is a file under the cache directory. +When that directory was unusable (for example `TMPDIR` naming a directory that does not exist), the batch never +started and the answer said `on-head="0" hits="0"`, with a near-miss note suggesting the name it had been given, on +a repository whose HEAD defines it. The CLI now exits 1 and names the cause on stderr, and the MCP `whereis` tool +answers error -32603. A genuine zero over a working cache directory is answered as before. +The cross-branch verbs read blobs the same way and now refuse the same way (the entry above); both verbs, CLI and MCP, +say it in one sentence that differs only in what was not read and names the cache directory and `TMPDIR`. + +### Added — a Ruby `db/schema.rb` renders its columns as Section definitions +A Rails schema is recognized BY CONTENT, never by path: a Ruby file whose tree holds a +`create_table "x", … do |t| … end` call — the call's do-block carries one block parameter — is a +rendered schema, wherever it lives. Each `t. "name"` / `t. :name` in a table block mints +ONE `SymKind::Section` def at `Lang::Ruby` — the data-kind slot a doc heading or YAML key already +occupies (model.h: a Section's isolation is a language property; the Ruby ones are still no call +edges) — span = the name token, both quote spellings and the `:symbol` form. Rails-generated attribute +uses (`product.price`) then resolve to the column as a definition: it appears in `--uses`, `--grep`, +`--whereis` and the map. THE COLUMNS ARE DEFINITIONS ONLY, by maintainer decision: buildGraph's +byName skips Section-Ruby, so a column admits NO call edge (it still takes the restart share of the ranking +that every definition gets, so a Rails app's `k=` values are smaller; its map rows do not change) — an untyped +`response.code` must not become a table's caller, and 200 `name` columns must not damp a model's real +`def name` (both measured at review; edges may return behind an evidence rule, e.g. a receiver proven +to be the owning model). The id rule covers the grounded spellings: no id column/spelling → an +implicit `id` def (the anchor is the table-name string — no `id` token exists there); `id: false` → +no id def; `id: :uuid` → an `id` def at the pair key (the uuid is a TYPE of the id column, never a +column of its own; the type is not modelled); `primary_key: "x"` → a def named the string instead. +An ARRAY or symbol `primary_key` (a composite key, or a hand-written migration) mints NO implicit id — +the rendered composite columns are the block's own explicit `t.` lines. `t.timestamps` mints +`created_at` AND `updated_at` (the DSL names them literally, symmetric with the id rule) — the two +defs anchor at the method token's edges. The DSL CALLS keep their reference posture exactly like the +attr family: `string`/`datetime`/`create_table` stay external-surface names. Duplicate columns across +tables stay SEPARATE defs (same name, distinct identity bytes): `id` on N tables is `defs="N"`, and +the map row merges them with `overloads`, never hiding the multiplicity. MIGRATIONS never interfere: a +class-wrapped create_table — the migration shape, string- or symbol-named — is refused by a +class/module-nest gate (a rendered schema's tables live at file level or under `Schema[].define`, never +inside a class body), so the rendered `db/schema.rb` stays the ONE source of column names and indexing +the migration beside its schema cannot double any def; `add_column` / `remove_column` / +`change_column` are argument data and read nowhere — a column added then dropped never registers +(pinned by the three migration fixtures). Rails 8's `db/queue_schema.rb`, `db/cache_schema.rb` and +`db/cable_schema.rb` are matched by the same content gate. Ingest records the schema columns (`kParserVer` 167 in this release, see the versions note; carried by the branch as +122 → 126 → 144 → 149 across its merges with `main`), so a cache written by an earlier build is re-parsed; no record +layout change (`kCacheVersion` and `kQSnapCacheScheme` unchanged). +Gate: `test/rubyschemacheck.sh` on `test/rubyschemafix/` — schema text from a real, running Rails +8.1 app's `bin/rails db:schema:dump` (verified `db:schema:load`-able), original domain tables scrubbed +to `spike_*` names; two spellings no dump emits are restored by hand and marked in-file (`id: :uuid`, +which that dump actually failed on, and a literal `t.timestamps`). Stated capture floors, each pinned +by a gate arm: `t.index` / `t.references` / `t.belongs_to` / `t.polymorphic` / `t.check_constraint` / +`t.exclusion_constraint` / `t.unique_constraint` name no column (a `t.references`/`t.belongs_to` mints +no `_id`, and a `create_join_table` mints nothing — Rails dumps render these as explicit columns); +a column call must be ON the block parameter (`helper.string "x"` names nothing); a symbol-named table +(`create_table :users`) is a migration spelling, not a schema surface; `where("price > ?")` string +fragments stay opaque; the picker splits honestly (`rec.name` on a plain local and a rich `X.new.name` +receiver are `graph_ambiguous`, `self.name` inside the defining class pins its own def). The +before-evidence: on the pre-change binary every column was `defs=0 external=1` — the fixture's +`--uses=name` read `defs="10"` (9 attr/def defs plus the YAML key) with the schema columns invisible; it +now reads `defs="19" external="0"` and the column uses resolve to definitions only. + + ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` The entry below left one floor open, (i). A call to self, or on a receiver the code builds, read a class's defs from both @@ -1410,9 +1880,9 @@ the `quality.h` mirror and `test/qschemetrip.hash` move with it. ### Changed — shallow-clone qualification reaches the composed surfaces, the unfetched-rev refusals and the empty windows -0.6.6 put `shallow="1"` on the owners / hotspots / cochange roots of a depth-limited clone and a deepen hint on the +An earlier change in this release put `shallow="1"` on the owners / hotspots / cochange roots of a depth-limited clone and a deepen hint on the pr-context and merge-scout unknown-ref refusals. Four surfaces that answer from the same history were left out -(found on three depth-1 clones in the 0.6.6 command sweep; every item reproduced on the current base before the fix): +(found on three depth-1 clones in this release's command sweep; every item reproduced on the current base before the fix): - `--pr-context` embeds `` and `` per changed file; `--situ`'s `[3] co-change` line and the MCP `situational_awareness` twin carry the same window. On a shallow clone the pr-context root now carries @@ -1445,6 +1915,24 @@ false-confidence rate, next-clue usefulness) and the rule that answering comes b gains §9.2, which fixes the two roles a size limit may have (runaway guard, stair-step target) and the capped-versus-uncapped grading rule. Documentation only; no behaviour change. +### Changed — Go import edges come from the shared `@import.path` capture (#358, Go slice) + +Go is the second language onto the shared import-capture vocabulary that the C-family slice introduced. A Go +`import_declaration` — single or grouped, aliased (`import f "fmt"`), dot (`import . "x"`), blank (`import _ "x"`), and +cgo's `import "C"` — is now matched by `(import_declaration) @import.path` in `queries/go/tags.scm` and read by one +`DepDialect::Go` normaliser in `src/ingest_importcap.h`; `captureIncludes` no longer walks Go files. The captured node is +the whole declaration because that is the unit the Include record has always been built from: its target is the +declaration's clause (`importClauseTarget`, shared with Swift, which still reads it from the walk), so a grouped block +stays ONE record and `resolveGoImport` still takes the first quoted token. Splitting a group into one record per spec, +and moving the alias onto `@import.alias`, would add rows and are left for their own slice. + +**Output is byte-identical.** Dependency and resolve output over the Go fixtures and repos in the lane report is +unchanged, the go.mod `module` / `replace` resolution reads the same records, and a cache written by the previous binary +is accepted untouched, so no parser-version bump is owed. `test/importcapcheck.sh` gains six Go arms (embedded capture, +21 byte-exact rows covering every form above plus tab, semicolon, raw-string, UTF-8 cut and error-recovery spellings, no +widening for a function-body import, replace-based resolution with a bogus-target negative, the import-role use-site +ref, and the Go cache round-trip). + ### Changed — the C-family import edges come from one shared capture instead of two per-language extractors (#358, C-family slice) `src/ingest_relations.h` held about 21 per-language import extractors (`csharpUsingTarget`, `phpUseTarget`, @@ -1495,6 +1983,53 @@ on both `--no-cache` and `--deps`. The new `test/importcapcheck.sh` pins the sha `#import` spellings, a macro include, guarded arms, both dead arms, the `#pragma`/`#error` gate, the import-role use-site half, cache round-trip, determinism) over a 14-edge fixture it builds itself. +### Changed — the TypeScript / JavaScript import edges come from the shared capture too (#358, JS/TS slice) + +The second language family onto the `@import.path` vocabulary the C-family slice introduced. `import … from 'x'`, the +re-exports (`export * from`, `export * as n from`, `export { a as b } from`, `export type { T } from`) and the CommonJS / +dynamic `require('x')` / `import('x')` are now three patterns on the one capture name in `queries/{typescript,tsx, +javascript}/tags.scm`, normalised by a `DepDialect::Web` arm in `src/ingest_importcap.h`. `captureIncludes` no longer +walks a TypeScript or JavaScript file at all, and the three branches of `directiveTargetOf` that read it are gone. +`jsModuleLoadTarget` stays: it is the text gate for the call spelling, and `captureJsImportFacts` (the bindings a +destructured `require` makes) still reads it. + +A query cannot say "the callee's text is `require`" (tags-pass predicates never run), so the pattern matches any bare +call with a leading string and the three guards that keep an ordinary call out of the graph stay C++: the callee is +the bare name `require`/`import`, there is exactly one argument, and it is a string. Two more things the walk owned +are re-derived from the captured node's ancestry, not dropped: the **container reach** (the walk entered only an +allowlist, so `require('x').y`, `[require('x')]`, `a ? require('x') : b`, a class method body and a `declare module` +body were never edges, and still are not) and the **lazy bit** (a `require`/`import` call written inside a function +body is `lazy="1"` on `--impact`'s import tier, from the same ancestor read that checks the reach). + +**Not changed, deliberately.** `import x = require('y')` (TypeScript) is not an edge, before or after: the grammar +gives it its own `import_require_clause`, which the walk never read and no pattern here matches. `@import.alias` and +`@import.names` are not used: the Include record of a JS/TS import names a module and nothing else; the bound names +are a different record (`captureJsImportFacts`, untouched). + +**Disclosure differences from the walk, in `--skipped` only** (the C-family slice's two, plus a third in the function both languages share). +`extract-partial` for import nesting now fires only when a captured import is actually cut at the bound. A `require('x')` +is itself a container to the old walk, so it announced at 254 and 255 nested blocks although the edge was kept; this +does not. A call the pattern captures but the `require`/`import` text gate drops (`foo('x')`) under 300 nested blocks is +not announced either, and neither is a C-family `#pragma once` under 300 `#ifdef` — the text gate now runs before the +bound test, in the one function both languages share, so an import was never cut. In the other direction, an import +under 300 nested blocks inside a class method is announced (the ancestor read reaches the bound before it can learn the +walk would never have entered the class). All three are pinned in `test/importcapcheck.sh`. + +**The ancestry is read in one descent from the root.** `ts_node_parent` is itself a descent from the root, so walking up +k ancestors costs k of them. The first version of this slice did that, bounded to 256 hops, and a synthetic file of +2 000 nested `foo( "x", foo( "x", … ) )` calls took 152 s against 0.5 s for the walk it replaced (Apple M2 Pro, one run +each, loaded machine). The final version reads reach, depth and the lazy bit off one `ts_node_child_with_descendant` +descent, after the text gate, and the same file takes 0.9 s. On three TypeScript repositories and the CommonJS tree of a +global npm install the `--deps` wall time is unchanged to within run-to-run noise. + +**Dependency edges are byte-identical.** Verified with both binaries against unchanged trees: `--deps`, `--uses`, +`--impact`, `--callers`, `--zoom`, `--report` and `--for` over three TypeScript repositories, the CommonJS tree of a +global npm install, two npm cache trees, a `.astro` tree and this repository's `test/` fixtures. `test/importcapcheck.sh` +gains a JS/TS section (J1–J8): an exact edge list, in source order, for `.ts .js .tsx .mjs .cjs .jsx` and a `.astro` +frontmatter (68 edges, with the `neg_*` shapes that must stay out), the lazy bit, the use-site half, the depth bound at +254/255/256 blocks, the disclosure differences, and cache round-trip with a pre-change cache accepted unchanged +(no `kParserVer` bump). + ### Changed — `--affected` and `--test-gate` mark which tests to run first (`run_first=`) A tests-to-run answer listed every reaching test in evidence order (changed, partner, then `hops=` ascending), 80 to @@ -1613,7 +2148,7 @@ per page instead. Gate: `test/impactdepthcheck.sh`. ### Changed — the versions this release moves, stated once -`kParserVer` 124 → 156 (the function-literal fix takes 128; #338 and #325 take 129; the body-less C/C++ type-specifier +`kParserVer` 124 → 167 (the function-literal fix takes 128; #338 and #325 take 129; the body-less C/C++ type-specifier span fix and the TypeScript `await f(x)` / `!f(x)` calls each took a number of their own on their branches, as did the false-edge resolution (134, 135), the value-reference rows (140) and the Ruby method-lookup changes (#373: one per step from 130 to 137 — bare-word calls, mixins, typed receivers, RSpec targets, Rails declared calls, Rake and Jbuilder @@ -1623,12 +2158,19 @@ shadow and the value-reference slot text take 143, above 141's full-use file tag 148, renumbered from its branch's 145: other branch builds in flight have used up to 147, and 148's full-use file tag is 149. The Go named-type kinds and the train 25 re-review's extraction fixes each took 145 on their branches, and the receiver-evidence records took 145 to 154 on theirs; all three land together at 156: branch builds have used up to 154, -whose full-use file tag is 155, so no cache such a build wrote is read as this release's), `kCacheVersion` 25 → 29 +whose full-use file tag is 155, so no cache such a build wrote is read as this release's; the Rails `db/schema.rb` +columns (#339, carried as 122 to 149 on its branch) land at 158, above 156's full-use file tag 157; the `--lego` base-clause and +class-factory changes, carried as 157, 160 and 162 on their branch, land at 164: branch builds have used up to 162, whose full-use +file tag is 163; the C++ element-access receivers, carried as 166 on their branch, land at 167, above it: that build wrote 166 and its full-use file tag 167), `kCacheVersion` 25 → 31 (the function-literal fix's record changes, then the false-edge fix's member-call fields, then the receiver chain -`memberPath`/`memberCtor`; the Ruby branch's appended binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 19 +`memberPath`/`memberCtor`, then the inherit reference's same-file base binding, numbered 30 on its branch and 31 here, above +every branch build; the Ruby branch's appended binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 20 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions -held as values, then on a function that only stores itself, then the defect-shape records of the twelfth kind). Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. The session legend dictionary is `dictv=593769ab691623d8 entries=806`. +held as values, then on a function that only stores itself, then the defect-shape records of the twelfth kind, then the +Python base clauses the dead kind's inherited-call evidence walks), and the +history-walk cache's `kQChurnCacheScheme` 2 → 3 (an earlier build stored a `git log` walk that git could not finish as if it +were the window). Every ingest cache written by an earlier build is refused and re-indexed once, every +cached quality snapshot is recomputed, and every repository's history is walked once more. The session legend dictionary is `dictv=80b7e589253a8e76 entries=855`. ### Fixed — a call the language resolves outside the tree no longer binds to a same-named in-repo definition @@ -2210,6 +2752,39 @@ five times, round-robin across the sizes, and compares medians. The thresholds a stall (one 640 KB run delayed 0.6 s) fails the old arm and passes the new one. A deliberately quadratic per-word rescan in `scanAsanWordBoundaries` still fails B1, B2 and B3 (medians 1033 / 14094 ms / timeout). +### Added — the budgeted-bundle candidate page: `--for`/`--pack-task` under a budget take a resumable `--limit`/`--offset` window; MCP `explore` gains `limit`/`offset` (issue #294, PR #362) + +`--for=TASK --token-budget=N` with an explicit `--limit`/`--offset` now answers with the **candidate +page**: one `` window over the same ranked candidate set the bundle cuts — the pageview quintet +(`shown=/total=/capped=/has_more=/next_offset=`) on the root, `above_cliff=` the head-tier count, +`tier=` one tier per page (a head window ends at the cliff), `r=` each row's GLOBAL candidate rank, +`at=` the git index answered from, `next=` the pasteable continuation argv (dropped when +`has_more="0"`), `over_ceiling="1"` when the budget cannot fit the window (fewer rows served, never +silently more bytes), and the page's own legend in both dialects (`--legend=full|compact`, the central +compact table). `--pack-task` under a budget takes the same window; a window with no budget stays the +`--for` file page; `--partition` and a window refuse together. The un-paged bundles are byte-identical +(`forbudgetmonotoncheck`), and `--offset=0` alone remains the un-paged answer — its resume point is +its own ``. + +MCP `explore` declares `limit`/`offset` (the same bounded pair `for` takes) and answers the same +page shape; previously it silently ignored `budget_tokens` beside a window and served the budgetless +file page. All five page paths — CLI `--for`, CLI `--pack-task`, MCP `for`/`explore`/`pack_task` — +page ONE candidate list: the CLI pages force the ranking's full distribution under a window (the +rule `--for`'s bundle path already followed), exactly the exhaustive scoring the MCP twins always +used, so the `ranking=` attribute the first round carried is gone — there is nothing left to +distinguish. Every page, CLI or MCP, carries the same pasteable `next=`/`next_tier=`: an MCP page's +handle is a CLI argv now, and it walks the same list. Every accepted ranking flag +(the CLI's `--no-route`, `--no-mention-boost`, `--no-doc-mention`, `--cochange-boost`; the twins' +`no_route`) rides `next=`, so a pasted continuation re-ranks identically — the +`test/pagingsweepcheck.sh` walk arms pin all five surfaces to `has_more="0"` with identical row +sequences and pin each flag's echo. The `tools/list` manifest grows 46,930 → 47,109 B (measured +on this release's tree: the +184 B of schema is `explore`'s `limit`/`offset` alone — `for` already +declared the pair, `pack_task` is a dispatch alias, not a listed tool — and `whereis`'s description +is 5 B shorter, the parser-label entry above), and the ceiling moves 46,950 → 47,150 B +- the one sanctioned case that moves it: `explore`'s two declared schema properties +(`test/mcpmanifestcheck.sh` arm `(1b)` asserts the figure against a live measurement). The `--whereis` preview entry above +then declares whereis's `full_text`: the `tools/list` manifest grows 47,293 → 47,383 B on this release's tree (+90 B, that one +schema property, after the `--lego` window's +184 B; 47,109 → 47,199 B on the lane's base), and the ceiling moves 47,150 → 47,420 B. ### Fixed — the Linux G1 sanitizer ritual completes: five string_view comparator lambdas stop wrapping, and the GCC ASan path builds (#342) `LSAN_OPTIONS=… ./asan/ripwire .` — the sanitizer ritual AGENTS.md requires before a PR — aborted on any @@ -2257,7 +2832,7 @@ Markdown input is byte-identical. Code in a language the scanner has no network- or another `#!` — is read as before and disclosed: ``, defined in the compact and full legends and counted on the stderr tally; `clean` does not cover those files. Python and JavaScript flow shapes (`requests.post` or `urllib` with `os.environ`, which is -missed even inside a fenced `python` block) are 0.6.7 work. `ripwire wrap`'s pre-install scan read only `.md` +missed even inside a fenced `python` block) are later work. `ripwire wrap`'s pre-install scan read only `.md` files, so it emitted the recipe (rc 0, empty stderr) for a skill whose `scripts/helper.sh` uploads a credential; it now scans every regular file of `./skills` and `.agents/skills` through the same kind-aware scan, following directory symlinks as `--scan-skills` does (each directory entered once, by device and inode, so a link loop ends), refuses such a skill diff --git a/CMakeLists.txt b/CMakeLists.txt index 47d1aa7c9..b809e770d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) # The version string lives in exactly ONE place — this project() call. --version (main.cpp) # reads it via the generated version.h below; test/versioncheck.sh asserts the two never drift. -project(ripwire VERSION 0.6.5 LANGUAGES C CXX) +project(ripwire VERSION 0.6.7 LANGUAGES C CXX) # ---- language standards (C++23 for our own code / C11 for the vendored C dependencies) ---- set(CMAKE_CXX_STANDARD 23) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0471c3f6..343a1ff0f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -486,12 +486,127 @@ rules it enforces: - **Reuse the shared mechanism.** Scope walks, shadowing (`VarDecl` bindings with spans) and imports (`Binding`/`Include`) each exist once. Extend them; a second copy under a new language's name is a review finding, and `--clones` shows it. No language-neutral qualified-call resolver exists yet - (`src/elixir_resolve.h` is Elixir-specific); raise generalising it in your PR. -- **Refuse, don't guess.** A call whose qualifier (`Mod.f`, `ns/f`) cannot be resolved is counted as - unresolved, never laddered to every definition of that short name. + (`src/elixir_resolve.h` is Elixir-specific); generalising it is slice 6b in the map below. +- **Refuse, don't guess: the target rule, not yet true for every language.** A module-qualified call (`Mod.f`, + `ns/f`) whose qualifier the resolver cannot place should be counted as unresolved, never laddered to every + definition of that short name. Today only three languages set a call qualifier, each with its own rule: Elixir + refuses (`ElixirResolver`), Rust keeps only the candidates the qualifier can name (`keepRustQualifiedCandidates` + in `src/graph.h`), and C++ refuses only a `std::`-rooted miss (`keepStdQualifiedCandidates`), because a C++ + qualifier can legitimately miss through a namespace alias, a using-declaration or a base class. **Known gap:** + a language that newly sets `Reference::qualifier` gets none of these. In `buildGraph` (`src/graph.h`), when the + canonical tier (`appendCanonicalCandidates`) finds no `qualifier::name` definition, the call falls through to the + bare-name ladder, so #349's Clojure `str/join` binds an in-tree `join`. A shared fix is planned (slice 6a). Until + it lands, this rule does **not** mean a qualified call in a new language is free of wrong edges. Pin the wrong + edge as a known-gap arm (see the definition of done below), and do not add a guard for your language alone. - **Known gap.** Tags-pass predicates (`#eq?`/`#any-of?`) do not run yet, so keyword-headed definitions (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; it is not a pattern to extend. + mechanism lands (#355, slice 5a); it is not a pattern to extend. + +#### 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 +import extractors). The target is **one shared handler per feature family**, with languages differing only by data: their +`tags.scm` captures in the shared vocabulary plus small per-language tables. The families: + +1. **Modules and imports**: import forms, re-exports, conditional and `try` imports, dynamic `import()`/`require`, includes, + package entry files (#358). +2. **Names and aliases**: renames (`import a as b`, `use x as y`), name lists, type aliases (`typedef`, `using`, `type X =`), + re-exports. +3. **Type definitions**: class/struct/union/enum/interface/trait/protocol, generics and templates, factory-made classes + (`Node.extend(...)`, `type(...)`, `defineComponent(...)`). +4. **Inheritance and composition**: extends/implements, mixins (`include`/`extend`/`prepend`), trait impls, protocol + conformance, extensions and categories, partial classes. +5. **Callables**: functions, methods, nested functions, lambdas and closures, decorators and annotations, properties + (getters/setters), operator overloads, constructors. +6. **Dispatch**: static, virtual/override, interface, duck typing, function pointers, delegates and method references, + reflection (`getattr`, `send`, `__call__`), `eval`. +7. **Data-driven dispatch**: function tables, struct-field initializers (`.exec = fn`), naming conventions (`on_*`, + `handle_*`, `test_*`), decorator and route registration, event emitters, dependency-injection containers. +8. **Preprocessing and generation**: macros, conditional compilation, code-generation markers, templates. +9. **Scope and state**: globals and statics, `nonlocal`, module state, receivers (`this`/`self`), assignment rebinding + (`f = g` then `f()`). +10. **Entry points and build**: `main`, CLI entry declarations, package scripts, build manifests. +11. **Tests**: test discovery per framework (it feeds `--affected` and `--test-gate`). +12. **Async and concurrency**: async/await, goroutines, callbacks, futures. + +These families are also the columns of a **language feature matrix**: one row per (language, form), each with a +3–10 line fixture and one ripwire call that answers four questions. Is the form indexed as a definition? Do calls and +references through it resolve (resolved, hedged, silently missed, or a false edge)? Which verbs does it feed +(`--callers`, `--callees`, `--whereis`, `--for`, `--path`, `--dead-code`)? Which code implements it (file and +function; two or more sites for one concept is the duplication signal)? The matrix is the feature spec for shared +code, and every fixture is a ready-made red/green test arm for the slice that fixes it. A language PR that adds +the fixtures for its own forms fills its own row of the matrix. That is welcome, and it is the cheapest way to see +which families your language already gets for free. + +Work on a family goes in this order, each step its own small PR with byte-identical output on the languages already handled: +(a) **fold duplicates**: forms already handled in two or more languages by separate code move into the shared handler; +(b) **port by data**: a form handled in one language whose mechanism generalises gets captures for the others; +(c) **new mechanism**: only then, a new shared handler for a form nobody handles yet. +A per-language function for a concept another language also has (for example a `captureBases` beside the shared +base-relations path) is a candidate for step (a). Claim a family or a slice of one in an issue before starting, as the #358 slices were claimed. + +#### Where each family lives today, and what you can claim + +Paths are under `src/`. **State**: *shared* = one mechanism, languages differ by data; *partly* = a shared path exists +and some languages still bypass it; *duplicated* = the same concept written per language; *none* = no handler yet. +Sizes: **S** ≈ a day, **M** ≈ a few days. Slices marked *maintainers* are ours; ask before starting one. +Adding a language is a natural time to take one. If your language needs a family whose state below is *partly* or +*duplicated*, folding one slice of it (its own small PR, before or after the language PR) is welcome, and the +review will credit it, but it is never a condition for merging the language. + +| Family | Shared mechanism today | State | Claimable slices | +|---|---|---|---| +| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS and Go read the capture; Python is in review (#382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. | +| 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: `capturePythonImportBinds` (`ingest_relations.h`), `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). | +| 3. Types | `captureTagsFacts` (`ingest_sidecap.h`) reads `@definition.*` captures for every language. | shared, but language decisions are `Lang::` branches inside the loop: 36 non-comment `Lang::` mentions, 15 of them Elixir. | **3a** #374: one constexpr per-language hook table that `captureTagsFacts` calls once per capture, one language per commit, Elixir first. M. | +| 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins have their own `captureRubyMixinBases`. | **4a** Ruby `include`/`extend`/`prepend` as captures read by the shared path, retiring `captureRubyMixinBases`: S–M, Ruby (offered on #339). **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first. | +| 5. Callables | Definitions come from `tags.scm`, read by `captureTagsFacts`. | partly. Keyword-headed definitions keep keyword tables in C++: `elixirFunctionKeyword` and `elixirMetadataAttribute` (`ingest_elixir.h`) repeat `macroBodyKeyword` (`ingest_relations.h`), 0.82 and 0.96 similar by `--clones`. | **5a** #355: the tags pass evaluates `#eq?`/`#any-of?`/`#not-any-of?`, then Elixir's table moves into its `tags.scm`. M; Elixir, then Clojure. | +| 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. | +| 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. | +| 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 + +1. **Claim it first**, in the slice's issue (or a new one naming its row and letter above). One slice per PR. +2. **Byte-identical output on every language already handled.** Build `main` and your head, run one argv list + against both with `--no-cache` (or a fresh `--cache` per binary), and diff. Cover the map, `--deps --limit=100000`, + `--skipped`, and `--callers`/`--callees`/`--impact`/`--uses` on symbols the slice touches, over the fixtures of + every language the slice touches plus at least one real repository per language. Put the argv list and the + result in the PR: an empty diff, or each difference named as intended. This is item 7 of the + [pre-review list](#6-submitting-a-change), scoped to the slice. +3. **The family's gates pass**, with any new arm shown red on `main` first. Run them as + `bash test/.sh "$PWD/build/ripwire"`. Imports: `importcapcheck`, `depsprecisecheck`, `resolvecheck`, + `nestedimportcheck`. Scopes and tests: `testscopecheck`, `langcheck`. Qualified calls: `elixirnamearitycheck`, + `rustqualcheck`, `stdqualcheck`, `resolverhonestycheck`. Bases: `chacheck`, `rubyinheritcheck`. Routes: + `routecheck`. Macros: `macroedgecheck`. When extraction output moved, also run `bash test/qschemetripcheck.sh` + (it reads the source, not a binary) and bump `kParserVer` and its mirror to any number above `main`'s. + Maintainers set the final number when they merge. +4. **The duplicate is gone.** `ripwire src --clones` no longer lists the group the slice folded, the old + per-language function is deleted rather than left beside the shared path, and + `ripwire . --quality-delta=origin/main..HEAD` gates 0 on your committed branch (a ref range compares commits + only). +5. **A wrong answer the slice does not fix is pinned, not hidden.** Write a known-gap arm that asserts today's + behaviour and names the slice that will flip it (`test/fieldnarrowcheck.sh` shows the form). When that arm + fails, the gap has moved: flip the arm. Never delete it. +6. **What a reviewer checks:** no new `Lang::` branch in shared code, no language-named capture kind, the + byte-identity diff, the deleted duplicate, and the rest of the [pre-review list](#6-submitting-a-change). + +#### Languages without a grammar: imported, not parsed + +A language with no vendored grammar can be indexed from an external symbol source, through **one** shared path +rather than per-language code: a standard SCIP index, read by the reader behind `--scip=FILE` (`src/scip.h` +builds the `ScipOverlay` declared in `src/scipoverlay.h`). Today that overlay only pins call edges between +symbols ripwire's own parsers extracted. Letting it contribute definitions for files no grammar reads is an +opt-in extension under discussion in #70, not a per-language feature. An imported language is a second tier. +It gets definitions, bodies, references and grep. It does **not** get complexity, clones, lint, `--match`, +parse health, grep that tells code from comments and strings, or confirmed calls, since a SCIP reference does +not say whether it is a call. Every imported row says where it came from. In one sentence: a **parsed** language +is read by ripwire's own grammar and every verb applies to it; an **imported** language is read from another +tool's index and only the verbs above apply. ### Interfaces diff --git a/README.md b/README.md index 0f9f2afd6..812283a54 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ claim cannot quietly drift. The row-by-row ledger is JavaScript · Java · Ruby · PHP · Lua · Elixir · Dart · Kotlin · GDScript · Bash · C# · JSON · TOML · YAML · Markdown — see [language support and limits](#languages). -**Latest: 0.6.5** — TypeScript alias imports resolve, and fixes from Windows testers. [Release notes](#release-notes) · +**Latest: 0.6.7** — more complete answers, faster calls, fewer bytes. [Release notes](#release-notes) · [the presentation](present/ripwire-showcase.pdf) · [the changelog](CHANGELOG.md) — with thanks to the contributors named there; this release is largely theirs. @@ -190,6 +190,16 @@ you need — no follow-on grep, no three more whole-file reads to fill in what i followed by three greps is the same search paid for twice: it does not save you tokens and it does not make the coding faster. +**Two jobs, one goal.** An agent meeting a codebase does two things, and ripwire is shaped for both: + +1. **Orient — `ripwire .`** One call gives the ranked map: the main subsystems, the program entry points, + the symbols that matter most, and a count of every file the ranking left out. The most important + information about a new tree, in as few calls as possible. +2. **Ask — `--for`, `--expand`, `--callers`, `--path`, …** After that, each question gets a lean answer: + as few bytes as it can spend while still answering the question fully, so no follow-up grep is needed. + +That is the goal, and not every answer reaches it yet; the stair-steps below are how we get there. +
The two stair-steps that make it reachable — honest about what is missing, priced in what it spends @@ -215,6 +225,9 @@ clue where to look next. So every change is judged on more than "was it right": - **False-confidence rate:** wrong answers that claim to be complete — the worst case, held near zero. - **Next-clue usefulness:** for partial and wrong answers, whether following the answer's first suggested next step reaches what the question needed, within one hop. Graded blind. +- **Recoverability:** for every cut answer, whether following its `next=` returns everything that was cut. +- **Tokens per complete answer:** total tokens an agent spends across a scenario, divided by the questions it got fully answered. +- **Cut-row recoveries:** how often a needed item arrived only through a continuation. **Answering comes first; efficiency is how we get there.** We aim for answers about as lean as the leanest tools, but a byte budget is never allowed to cost an answer. We have gone too far toward short @@ -222,6 +235,27 @@ answers before and cut information that was needed, so now every size limit has runaway guard far above typical answers (it catches an output bug, and says so when it trips), or a stair-step target where anything over it must be explained. A capped answer is always measured against the same answer uncapped, and the one that answers better wins. + +**A cut is only honest if you can get the rest back.** Disclosing a cut is the floor, not the bar: a capped +answer plus the `next=` it prints must return everything the uncapped answer had. A test that checks this for every +budget option on every command is being built, so that "the answer was cut" always comes with "and here is the rest". + +**What a budget may spend first.** When a budget is tight, ripwire drops bytes that carry no answer before +it drops answers: repeated legend text, duplicated paths, bodies whose signature already says what you need. +Only then the lowest-ranked rows, each counted. A whole section never disappears without a marker. + +**Budgets are yours to set, never ours to impose.** The defaults and the setups `ripwire wrap` writes carry no +budget that cuts a typical answer; a budget is a choice the caller makes, with its cost disclosed. + +**Efficiency is counted per complete answer, across the whole task.** Bytes per answer can fall while the task +gets more expensive, if the agent then has to ask again. So the efficiency figure is tokens per complete answer +over a whole scenario — orient, then the questions that follow — not bytes per call. + +**Ranking is the tool's best guess; the next= is the safety net — and we measure how often the net is needed.** ripwire cannot always +know which row answers the question; it goes by ranking. When a needed item appears only after following an answer's next=, the +honesty layer saved the answer but the ranking put that item below the cut. We count those per round: +- **Recovered through next=:** the item was cut but recoverable — one extra call. Driving this down is ranking work. +- **Lost:** the item was cut with no marker or no working next= — a bug, fixed first.
### Same answer, a fraction of the tokens — read this table first if your agent is on a budget @@ -243,7 +277,7 @@ Ordered understand → navigate → review-the-change: | "Where is X handled?" | `ripwire . --for="…"` | **~2.1K tok** | ~4.9K–20K tok — `grep -rn src/`, then read the file it points at | 2.3×–9.3× | | "What do I already know?" | `ripwire . --recall="…"` | **~15K tok** | ~445K tok — read all 119 markdown docs this repo carries | 29.2× | | "Set me up for this task" | `ripwire . --pack-task="…"` | **~2.1K tok** | ~16K–80K tok — read every relevant file, whole | 7.7×–37.7× | -| "Show me this one function" | `ripwire . --expand=SYM --top-k=0` | **~260–16.5K tok** body (+~5.7K for the ranked-neighborhood bundle) | ~43K–174K tok — read the whole file it lives in | 2.6×–670× | +| "Show me this one function" | `ripwire . --expand=SYM` | **~260–16.5K tok** body (+~5.7K for the ranked-neighborhood bundle) | ~43K–174K tok — read the whole file it lives in | 2.6×–670× | | "Who calls this function?" | `ripwire . --callers=SYM` | **~580 tok** | ~40K–52K tok — `grep -rn SYM src/` (mostly noise), then open 2–3 files to sort real calls from mentions | 69.2×–89.1× | | "Is it safe to change this?" | `ripwire . --impact=SYM` + `--uses=SYM` | **~1.3K tok** | ~18K tok — open every direct-use file, whole | 14.4× | | "I have a stack trace" | `ripwire . --from-trace=FILE` | **~1.4K tok** | ~124K–298K tok — grep all 7 frame names, then open the innermost file(s) | 86.9×–208.6× | @@ -260,7 +294,7 @@ three `docs/ARCHITECTURE.md` names as central; the `--for` row lands `mcpStale` (`src/mcpindex.h:633`), the actual staleness check, 5th-ranked; `--recall` lands the container-rule doc (`AGENTS.md`) that states, verbatim, the same "no `std::map`" rule `CONTRIBUTING.md` explains in full; `--pack-task` names the same three touch points a human would — `cachelint.h`, `mergeCachePack` -(`src/main.cpp:1787`), the `lintrules.h` helpers it reuses; `--expand --top-k=0` hands back the +(`src/main.cpp:1787`), the `lintrules.h` helpers it reuses; `--expand` hands back the requested function's complete, unmodified body — the ranked-neighborhood addition costs the same ~22.6 KB regardless of which function you ask for, confirmed on two (a fixed floor, not per-function variance); `--callers` on `langOfPath` names its 2 real callers, the same ones a `grep` hit-list @@ -799,7 +833,7 @@ to do with it.** Two shapes get the most out of it, and one gets nothing. |---|---|---| | `ripwire . --for="…" --top-k=5` | `ripwire . --for="…" --signatures-only` (or `--token-budget=N`, `--detail=N`) | `--top-k` is inert on `--for`: the run warns on stderr and emits the full bundle anyway, so the agent *believes* it narrowed the output and did not. | | `ripwire . --query="…"` as the default lens | `ripwire . --for="…"` | `--query` is the raw BM25 ranking — the binary's own help calls it debug and says "use --for". It is the right tool for hunting a vocabulary, the wrong default for a task. | -| `ripwire . --expand=SYM` where SYM is an **ambiguous** bare name | `ripwire . --expand=SYM --top-k=0` (or name it exactly: `--expand=FILE:NAME`) | A multi-match name keeps the ranked map — there IS something to disambiguate — so ~9K est_tokens of map ride along with the bodies. An **unambiguous** single match already defaults to `--top-k=0` on its own (disclosed as `topk_default="0"`); no flag needed there. | +| `ripwire . --expand=SYM --top-k=0` (a habit from before the lean default) | `ripwire . --expand=SYM` | `--expand` and `--outline` ship no ride-along map whenever every definition asked for is served — one exact match or an ambiguous bare name alike (disclosed as `topk_default="0"`, with `map_next=` naming the call that prints the map if you never oriented). Definitions a byte budget cut are named in `unserved_total=` / `unserved_next=`. An explicit `--top-k=N` still brings the map back. | | `--callers=` expecting routes | find the URL in the project's own docs (e.g. a feature map), then `--expand` the handler | Framework route handlers have no callers in the graph — the decorator reaches them, not project code. Empty `--callers` on a handler is the design, not a bug. | | `ripwire …` (ONE root that happens to contain checkouts) | `cd` into ONE checkout first | Nothing refuses this: the crawl silently walks the nested repos and merges them into one corpus, so you pay for a map of everything and rank across unrelated codebases. Distinct from the real multi-root feature, which is N **explicit** positional roots (`ripwire dir1 dir2 … `, 2–16 checkouts merged on purpose). | @@ -948,9 +982,9 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers
-188 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 +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 -Around the core sit 188 long flags advertised in `--help`, across seven families — plus an MCP +Around the core sit 191 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. @@ -1065,7 +1099,7 @@ every dependency is vendored in-tree, so the build completes with the network of git clone https://github.com/redhat-et/ripwire.git cd ripwire cmake -S . -B build && cmake --build build -j -./build/ripwire . --max-tokens=3000 # the ranked map — start here on an unfamiliar repo (bare it is ~22 KB here; this keeps the head at ~6 KB) +./build/ripwire . # orient once: the ranked map of an unfamiliar repo (add --max-tokens=N only if your context is tight — it cuts the map and says so) ```
@@ -1109,7 +1143,7 @@ skills/install.sh --codex # Codex CLI: the task-shaped skills that say whe Four commands worth learning first: ```bash -ripwire . --max-tokens=3000 # the ranked map — start here (bare it is ~22 KB / ~9K est_tokens on this repo; this keeps the head at ~6 KB / ~2.5K) +ripwire . # orient once: the ranked map of an unfamiliar repo (add --max-tokens=N only if your context is tight — it cuts the map and says so) ripwire . --for="incremental cache invalidation" # the task lens: what to touch, ranked ripwire . --callers=someFunction # who calls it ripwire . --test-gate # before you commit: which tests must run @@ -1408,18 +1442,19 @@ trailing `…`. **Ten seconds, no index server, no embeddings, no API key** — a parse and a call graph, built on the spot. The rows below are a real capture: the callers and their files are gate-held current (`test/readmeexamplecheck.sh`), the `:line` suffixes were true when captured and drift as the files -grow — nothing can keep a line number true in a document, so it is not claimed here. +grow — nothing can keep a line number true in a document, so it is not claimed here. `p=` is where each caller is +defined; `sites_at=` lists its call sites as pasteable `file:line` tokens, so the lines to edit come with the first answer. ``` $ ripwire . --callers=rankGraphTeleport - - - - - - - + + + + + + + ``` @@ -2085,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-678 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +683 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 **678 gate scripts** and is the authoritative list; +`test/regression.sh` names **683 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 @@ -2609,12 +2644,14 @@ every request cross the network in the clear: put a TLS-terminating reverse prox Change to the repository. Run the following command: ```bash -ripwire . --max-tokens=3000 # start here on an unfamiliar repository +ripwire . # orient once on an unfamiliar repository ``` -The command writes the top of the ranked map within the token budget. Remove `--max-tokens=3000` -for the complete map. The complete map is larger. On a large tree, use `--top-k=N` to limit the row -count. +The command writes the ranked map: the main subsystems, the entry points, the most important symbols, +and a count of every file the ranking left out. Read it once per session, then ask lean questions +(`--for`, `--expand`, `--callers`). If your context is tight, `--max-tokens=N` keeps only the head of +the map; the cut is disclosed (`shown=` of `symbols=`), but it can drop what you need, so prefer the +full map when it fits. The first call on a tree parses it; every call after that reads a cache and answers in a fraction of the time. Judge the cost of the tool by the second call, not the first. If the first call fails, or @@ -2625,7 +2662,7 @@ identity of the index, and says which one is at fault. | Task | Command | | --- | --- | -| Orient in a new tree | `ripwire . --max-tokens=3000` | +| Orient in a new tree | `ripwire .` | | Prepare for a task | `ripwire . --for=""` | | Get everything for one task in a single call | `ripwire . --pack-task=""` | | Ask which command to run | `ripwire . --help-task=""` | @@ -2647,7 +2684,7 @@ identity of the index, and says which one is at fault. ### 5. Command families -The `--help` output groups 188 long flags advertised in `--help` into seven families. The `--help=` +The `--help` output groups 191 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. @@ -2694,13 +2731,13 @@ The leading legend comment is elided here; the line numbers are a capture and mo ```xml - - - - - - - + + + + + + + ``` @@ -2841,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 240 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 253 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. @@ -3054,6 +3091,24 @@ terms. ## Release notes +**ripwire 0.6.7 — more complete answers, faster calls, fewer bytes.** Each `--callers` and `--safe-delete` row now +names its call sites as `file:line`, so one answer holds the lines to edit (`--callers=escapeXml` on this repository: +its first answer shows 40 of 142 callers, every one with its call lines, 79 in all; 0.6.5 showed none). `--lego` lists implementors below a direct one (a two-level +hierarchy: 5 of 5, was 2); `--impact` names the files its reach lands in (tmux `--impact=grid_set_cell` now names `input.c` +and `window-client.c`); the default map keeps directories and entry points it used to cut (tmux `input.c`, +`key-bindings.c`, `main`); `--for` gives the function a question names its own call-hop row (8 of 32 questions over +8 public repositories gain it, 23 are byte-identical); and `--whereis` labels other branches' definitions with the parser +(`--whereis=escapeXml` here: 60 rows, 54 of them call sites, became the 2 declarations). A warm `--callers` call is faster (319 → 269 ms on a 3,100-file +snapshot), and `--whereis` is about three times faster: on this repository's 596-ref checkout, `--whereis=lego` takes +4.7 s instead of 13.3 s and returns 18,616 B instead of 56,075 B (0.6.5 vs 0.6.7), every row kept. It now parses other +branches' files to tell definitions from call sites, which costs memory: about 1.0 GB at peak, up from 0.6 GB. A C++ `empty()` or `find()` no longer +binds to an in-tree namesake without proof (14 of the top 20 map rows here carried such a name; now 0). Imports for +JS/TS and Go now come from one shared capture (C-family from @lennix1337). Rails schema columns are definitions (thanks +@mpapis, #339), and a budgeted `--for` pages over CLI and MCP (thanks @pt-act, #362). Ruby calls follow Ruby's method +lookup (thanks @andriytyurnikov), and the Linux sanitizer build runs again (thanks @llvm-x86). Reports from +@ahmedibrahim085 (#381: the router now keeps a control group by default), @KilimcininKorOglu (#350) and @toasterbook88 +(#353). New in CONTRIBUTING.md: a map of each feature family's shared handler, with slices you can claim. + **ripwire 0.6.5 — TypeScript alias imports resolve, and fixes from Windows testers.** Imports through a tsconfig alias, `baseUrl` or a workspace package are now real edges, so `--deps`, `--arch`, `--impact` and the call graph see them, and a config ripwire could not read is disclosed rather than guessed (thanks @srinchow). On Windows, `--doctor` searches PATH as diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index b4d03761b..b4217969e 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-08.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-11.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. @@ -38,13 +38,13 @@ Two limits apply to nearly everything here and are not repeated in every section ## Contents -**understand a codebase cold** — [`--top-k`](#--top-kn) · [`--max-tokens`](#--max-tokensn) · [`--token-budget`](#--token-budgetnkmg) · [`--help-task`](#--help-tasktask) · [`--for`](#--fortask) · [`--sections`](#--sectionslegocompose) · [`--signatures-only`](#--signatures-only) · [`--auto-bodies`](#--auto-bodies) · [`--no-route`](#--no-route) · [`--adaptive`](#--adaptive) · [`--no-mention-boost`](#--no-mention-boost) · [`--no-doc-mention`](#--no-doc-mention) · [`--lego`](#--legotype) · [`--exemplar`](#--exemplartaskkind) · [`--recall`](#--recalltask) · [`--tree`](#--tree) · [`--html`](#--htmlfile) · [`--color-by`](#--color-bymode) · [`--order`](#--ordermode) · [`--no-stable`](#--no-stable) +**understand a codebase cold** — [`--top-k`](#--top-kn) · [`--no-inventory`](#--no-inventory) · [`--max-tokens`](#--max-tokensn) · [`--token-budget`](#--token-budgetnkmg) · [`--help-task`](#--help-tasktask) · [`--for`](#--fortask) · [`--sections`](#--sectionslegocompose) · [`--signatures-only`](#--signatures-only) · [`--auto-bodies`](#--auto-bodies) · [`--no-route`](#--no-route) · [`--adaptive`](#--adaptive) · [`--no-mention-boost`](#--no-mention-boost) · [`--no-doc-mention`](#--no-doc-mention) · [`--lego`](#--legotype) · [`--exemplar`](#--exemplartaskkind) · [`--recall`](#--recalltask) · [`--tree`](#--tree) · [`--html`](#--htmlfile) · [`--color-by`](#--color-bymode) · [`--order`](#--ordermode) · [`--no-stable`](#--no-stable) **navigate / answer a question** — [`--around`](#--aroundsym) · [`--callers`](#--callerssym) · [`--callees`](#--calleessym) · [`--uses`](#--usessym) · [`--graph-query`](#--graph-queryexpr) · [`--external-surface`](#--external-surface) · [`--path`](#--pathsrcdst) · [`--connect`](#--connectabc) · [`--impact`](#--impactsym) · [`--verify`](#--verifyclaim) · [`--mentions`](#--mentionssym) · [`--affected`](#--affectedf1f2sym) · [`--exercises`](#--exercisestestfile) · [`--situ`](#--situf1f2) · [`--handoff`](#--handoff) · [`--test-gate`](#--test-gatef1f2) · [`--grep`](#--grepstr----regexpat) · [`--grep-context`](#--grep-contextn----grep-beforen----grep-aftern) · [`--and`](#--andstr) · [`--not`](#--notstr) · [`--grep-scope`](#--grep-scopelinefile) · [`--grep-in`](#--grep-incodeany) · [`--handles`](#--handles) · [`--match`](#--matchquery) · [`--pattern`](#--patternpat) · [`--query`](#--queryterms) **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) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--whereis-listing`](#--whereis-listingwhich) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) +**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--lint-max-per-rule`](#--lint-max-per-rulen) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--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) **self-diagnosis** — [`--doctor`](#--doctor) · [`--agent`](#--agentcodexclaude) · [`--skipped`](#--skipped) @@ -68,21 +68,21 @@ _Same map, capped to the 5 highest-ranked symbols._ ``` $ ./build/ripwire . --top-k=5 - + - - - - + + + + - - - + + + + - - -... [11 more line(s); run it to see the whole thing] + +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--token-budget`, `--for`, `--recall`, `--graph-query`, `--pack-signatures`, `--expand`, `--from-trace`, `--run-trace` @@ -92,6 +92,40 @@ $ ./build/ripwire . --top-k=5 - --for's OWN signature/lego/compose bundle self-limits via --pack-top-n instead — --top-k is INERT there (documented, not fixed — a real fix is a behavior change); - to WIDEN a --for answer use --limit=N, the file-grain page (one row per file), not --top-k. +### `--no-inventory` + +**Answers:** drop the default map's tier, the files and entries its ranked rows leave out. + +The default map appends after its ranked rows whenever they left out a file with code: rows (program entries by each language's own entry convention) and rows naming or counting every unshown file by directory. This flag drops it (the rows are unchanged either way). The tier never rides a map fitted to a token budget, nor a payload verb's ride-along map. + +**Try it** + +_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=5 --no-inventory + + + + + + + + + + + + + + +... [7 more line(s); run it to see the whole thing] +``` + +**Caveats (stated by the binary):** + +- The default map appends after its ranked rows whenever they left out a file with code: rows (program entries by each language's own entry convention) and rows naming or counting every unshown file by directory. +- The tier never rides a map fitted to a token budget, nor a payload verb's ride-along map. + ### `--max-tokens=N` **Answers:** budget the map to ~N tokens (binary-search top-K) — SHAPES the map to fit. @@ -106,16 +140,16 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ $ ./build/ripwire . --max-tokens=1500 - - - - + + + + ``` -**Shaped by:** `--token-budget`, `--recall`, `--detail`, `--pr-context`, `--from-trace`, `--run-trace`, `--limit` +**Shaped by:** `--token-budget`, `--recall`, `--detail`, `--pr-context`, `--from-trace`, `--run-trace` **Caveats (stated by the binary):** @@ -136,10 +170,10 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + ``` -**Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--recall`, `--handoff`, `--pr-context`, `--from-trace`, `--run-trace` +**Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--sections`, `--recall`, `--handoff`, `--pr-context`, `--from-trace` **Caveats (stated by the binary):** @@ -173,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. --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. +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. **Try it** @@ -181,19 +215,19 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### 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] ``` @@ -210,7 +244,7 @@ $ ./build/ripwire . --for="rankGraphTeleport" **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. +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. **Try it** @@ -218,14 +252,14 @@ _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] @@ -245,14 +279,14 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -277,17 +311,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] ``` @@ -310,16 +344,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] ``` @@ -342,14 +376,14 @@ _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] @@ -373,10 +407,10 @@ _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 ) @@ -404,15 +438,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] - + + + + +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 ) - + ... [22 more line(s); run it to see the whole thing] ``` @@ -424,7 +458,7 @@ $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-ment **Answers:** show one interface and every class implementing it — the contract, then the existing impls the interface->impls view for ONE named interface/base: its signature, method contract, and every implementor (own-language only). -file:name disambiguates a same-named type. No contract for a language this surface cannot read soundly: methods=0 caveat=… says so. +file:name disambiguates a same-named type. No contract for a language this surface cannot read soundly: methods=0 caveat=… says so. Implementors BELOW a direct one follow as via= depth= rows (transitive=N), 200 a page, walked through PROVEN extends clauses only (base_by=name: bound by spelling); --limit/--offset page them. **Try it** @@ -434,21 +468,22 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + - - + + ``` -**Shaped by:** `--callers`, `--expand`, `--layout` +**Shaped by:** `--callers`, `--expand`, `--layout`, `--limit` **Caveats (stated by the binary):** - file:name disambiguates a same-named type. - No contract for a language this surface cannot read soundly: methods=0 caveat=… says so. +- --limit/--offset page them. ### `--exemplar=TASK|KIND` @@ -463,8 +498,8 @@ _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 ) @@ -493,9 +528,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 216 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 +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 -━━ README.md (relevance 8.038) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 214142 B; lines="2710-2722,2723-2730"; dropped_by_budget=23] +━━ 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] #### 6.2 Exit codes This is the part to wire into a script. @@ -558,13 +593,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 1416688-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_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._ ``` $ ./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.145) ━━ [sections: 1 of 159 selected (188 in doc), section-granular; whole doc 551924 B; lines="3754-3758"; dropped_by_budget=158] [truncated: 1865 of 7739 bytes] +━━ 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] ### `--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. @@ -587,19 +622,19 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - - - - - + + + - - - - + + + + - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -658,21 +693,21 @@ _Stable (path/id) emit order — provider KV-cache hits across re-runs._ ``` $ ./build/ripwire . --order=stable --top-k=5 - + - - - + + + + - - - + + - - - - -... [11 more line(s); run it to see the whole thing] + + + + +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--no-stable` @@ -689,20 +724,21 @@ _--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] ``` --- @@ -724,17 +760,17 @@ $ ./build/ripwire . --around=rankGraphTeleport - - + + - + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -744,7 +780,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 +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=) **Try it** @@ -774,16 +810,13 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - - + + + - - - - + ``` @@ -806,16 +839,16 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + - - + + - - - - - + + + + + ``` @@ -840,19 +873,19 @@ _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] ``` @@ -877,19 +910,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] ``` @@ -912,12 +945,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - - + + + + + + ``` @@ -934,10 +967,10 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - + @@ -951,7 +984,7 @@ $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex **Answers:** show everything that reaches SYM — the transitive blast radius before a change — the indexed symbols that reach SYM (a floor, see counts_floor). -file:name disambiguates like --callers rows run nearest first: d= is the hop depth (1 = a direct caller; printed where it changes), PageRank order within a depth; by_depth=k:n on the root counts reaches= per depth, so a cut drops the deepest first importers= is a SECOND, weaker reach beside it: the files that directly include/import a file defining SYM, emitted as rows (format=columnar carries the count only; --limit sizes it). NEVER added to reaches= — files and symbols are different units, and an importer may use a different symbol from that file, or none at all. lazy="1": every one of that importer's edges is written inside a closure — a TS/JS require()/import() inside a function body, a Ruby constant receiver or argument inside a method/lambda/block, a Ruby autoload or rescue class — not at load time: still a real dependency, weaker than a top-level one +file:name disambiguates like --callers rows run nearest first: d= is the hop depth (1 = a direct caller; printed where it changes), PageRank order within a depth; by_depth=k:n on the root counts reaches= per depth, so a cut drops the deepest first importers= is a SECOND, weaker reach beside it: the files that directly include/import a file defining SYM, emitted as rows (format=columnar carries the count only; --limit sizes it). NEVER added to reaches= — files and symbols are different units, and an importer may use a different symbol from that file, or none at all. lazy="1": every one of that importer's edges is written inside a closure — a TS/JS require()/import() inside a function body, a Ruby constant receiver or argument inside a method/lambda/block, a Ruby autoload or rescue class — not at load time: still a real dependency, weaker than a top-level one a CUT symbol window opens with : the WHOLE reach set by file (rf syms= d=, 20 by default, --limit sizes it); an umbrella header's importers are a count (importers_umbrella="1") **Try it** @@ -959,20 +992,20 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - - - - - - - - - - - - - + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1043,21 +1076,20 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - - in evidence order (order= partners=); seeded_by= the reading taken. counts_floor=1: every count is a FLOOR, never a total. graph_ambiguous=/graph_unresolved=: resolver gauge. graph_unindexed=N: N files no grammar could read (the map header's unindexed=); their calls raise neither gauge. run_unknown=1: no runner derivable (a guess would be worse). root=: p= relative to it. seeds=N: symbols the argument matched; only those outside test files seed the caller walk. seed_test_files=N: matched files that are TESTS; listed to run (seed_kind=test), never walk seeds. tests=N: test files listed to run (the rows). reached=N: symbols the transitive caller walk reached from the seeds (seeds excluded). script_gates_unmodelled=N: test/*.sh runners; their subprocess reach is unmodelled, never in tests=/reached=. run_first=N: the first N test files have the most direct evidence (changed/partner/hops=1, else nearest hops=); not a skip list. changed=: the files/symbols argument as given. --> + - - - - - - - - + + -... [5 more line(s); run it to see the whole thing] + + + + + + ``` **Shaped by:** `--exercises`, `--test-gate`, `--edit-target-file`, `--legend` @@ -1081,7 +1113,7 @@ _Which symbols a TEST FILE exercises — the reverse direction of --affected._ ``` $ ./build/ripwire . --exercises=test/regression.sh - ``` @@ -1106,7 +1138,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: f1e5c2e76 +at: 50dfd49c3 (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1124,19 +1156,19 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - - + + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1160,7 +1192,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1185,15 +1217,15 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - - + + - + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1487,10 +1519,10 @@ _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 @@ -1514,24 +1546,24 @@ test/showcasecapturecheck.sh (C) re-derives this range from the SAME repo every **Try it** -_Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart._ +_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._ ``` $ ./build/ripwire . --pack-signatures --top-k=10 - - - - - - - - - + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1553,7 +1585,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)" ); @@ -1572,7 +1604,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 **Answers:** print these symbols' full bodies, plus the signatures of what they call full bodies of A,B,... -Selector grammar per item (the tail after the LAST ':' decides; a tail STARTING WITH A DIGIT is a range, anything else is a name): NAME every def of that name | FILE:NAME that file's def NAME:START-END body slice | FILE:NAME:START-END selector + slice FILE:LINE:NAME paste a row's p="path:line" straight from --callers/--lint/--grep (NOT --hotspots: its p= is a BARE path — build FILE:LINE:NAME from its own p=/top_l=/top= instead, since top= is just the worst function's name) path::scope::name the id composed from a row's p= sc= n= (the map, --for, --pack-task) Scope::name the sym= spelling edit-check and grep's in= rows print — matches the name under any scope whose ::-boundary SUFFIX is Scope (Box::lid, deep::Box::lid); a wrong scope refuses, it never falls back to the bare-name union Scope.name|Scope#name the same match, dotted (Python/JS/Java) or Ruby-style; tried only when no other spelling matched (START-END is 1-based within the def's OWN body — lines="lo-hi/total" marks the slice partial; out-of-range clamps. FILE matches any path substring, like --callers/--lego.) EXACT-NAME DEFAULT (one token, one unambiguous match, no explicit --top-k): the ranked map defaults to top-k=0 — you already named the exact symbol, so the ~200-row orientation map is pure overhead in front of the one body it exists to summarize. Disclosed on the root as topk_default="0" (self-describing: the change is visible without reading source). A MULTI-match name (an ambiguous bare name) or a multi-token --expand keeps the map — there IS something to disambiguate. An EXPLICIT --top-k=N (0 included) always overrides this default. Each body also carries sibs="a,b,..." sibs_total="N" [sibs_capped="1"] (the file's OTHER symbols, names only, capped at 40) and inc="x.h,..." inc_total="N" [inc_capped="1"] (the file's own #include/import targets, capped at 24) — both absent when the count is 0 (a documented zero, not a degrade), so a body no longer needs a second --outline call just to learn what else lives in its file. CHEAPEST-COMPLETE-ANSWER SERVING (no explicit --top-k, no range slice): the verb ALSO measures the (possibly map-less) bundle against the requested symbols' whole FILE(s) and emits the SMALLER, disclosed on the root as mode="bundle|whole-file" reason="the two byte counts" — on a small file the old bundle was 5.65x the file itself; on a big file the bundle saves ~26x. The whole-file form is with the file CDATA-wrapped (redacted as usual) and every requested symbol's line anchor kept. An EXPLICIT --top-k=N (including 0) opts out of BOTH the exact-name default and mode= auto-selection and keeps the classic undecorated shape; a SYM:START-END slice opts out of mode= auto-selection only (serving the whole file would invert an explicit narrowing) but still gets the exact-name top-k=0 default when it applies. When the bundle wins AND no explicit --top-k was given, the requested bodies are served BEFORE the ranked map (not buried after it), and the map's escape hatch also rides the root as note="...--top-k=0 for the payload alone..." (not stderr-only): a caller reading only stdout sees both the answer and how to drop the map on the next call. Absent whenever the bundle carries no map at all (top-k=0, exact-name default) or loses to whole-file, and whenever --top-k was explicit — the classic shape above carries no note= either. +Selector grammar per item (the tail after the LAST ':' decides; a tail STARTING WITH A DIGIT is a range, anything else is a name): NAME every def of that name | FILE:NAME that file's def NAME:START-END body slice | FILE:NAME:START-END selector + slice FILE:LINE:NAME paste a row's p="path:line" straight from --callers/--lint/--grep (NOT --hotspots: its p= is a BARE path — build FILE:LINE:NAME from its own p=/top_l=/top= instead, since top= is just the worst function's name) path::scope::name the id composed from a row's p= sc= n= (the map, --for, --pack-task) Scope::name the sym= spelling edit-check and grep's in= rows print — matches the name under any scope whose ::-boundary SUFFIX is Scope (Box::lid, deep::Box::lid); a wrong scope refuses, it never falls back to the bare-name union Scope.name|Scope#name the same match, dotted (Python/JS/Java) or Ruby-style; tried only when no other spelling matched (START-END is 1-based within the def's OWN body — lines="lo-hi/total" marks the slice partial; out-of-range clamps. FILE matches any path substring, like --callers/--lego.) EXACT-NAME DEFAULT (one token, one unambiguous match, no explicit --top-k): the ranked map defaults to top-k=0 — you already named the exact symbol, so the ~200-row orientation map is pure overhead in front of the one body it exists to summarize. Disclosed on the root as topk_default="0" (self-describing: the change is visible without reading source). The same default applies when --expand names SEVERAL definitions (an ambiguous bare name, or several tokens) and EVERY one is served: the bodies carry p= l= t= for each, the root adds map_next= (the shell call that prints the map you did not get — orient once per session, then look up lean). If a byte budget cut some, the root adds unserved_total=N (the true count) and unserved_next="--expand=FILE:LINE:NAME,…" (the call serving them; past 16 it serves the first 16 and unserved_listed="16" says so — a larger --pack-budget-bytes serves the rest) — never the generic map. map_next= and unserved_next= are quoted for the shell; map_next= names every root plus the crawl-scope flags (--exclude, --max-file-size, --no-ignore, --ignore-tests). --outline follows the same rule for its one token (several --outline tokens keep the ordinary map). An EXPLICIT --top-k=N (0 included) always overrides this default. Each body also carries sibs="a,b,..." sibs_total="N" [sibs_capped="1"] (the file's OTHER symbols, names only, capped at 40) and inc="x.h,..." inc_total="N" [inc_capped="1"] (the file's own #include/import targets, capped at 24) — both absent when the count is 0 (a documented zero, not a degrade), so a body no longer needs a second --outline call just to learn what else lives in its file. CHEAPEST-COMPLETE-ANSWER SERVING (no explicit --top-k, no range slice): the verb ALSO measures the (possibly map-less) bundle against the requested symbols' whole FILE(s) and emits the SMALLER, disclosed on the root as mode="bundle|whole-file" reason="the two byte counts" — on a small file the old bundle was 5.65x the file itself; on a big file the bundle saves ~26x. The whole-file form is with the file CDATA-wrapped (redacted as usual) and every requested symbol's line anchor kept. An EXPLICIT --top-k=N (including 0) opts out of BOTH the exact-name default and mode= auto-selection and keeps the classic undecorated shape; a SYM:START-END slice opts out of mode= auto-selection only (serving the whole file would invert an explicit narrowing) but still gets the exact-name top-k=0 default when it applies. When the bundle wins AND no explicit --top-k was given, the requested bodies are served BEFORE the ranked map (not buried after it), and the map's escape hatch also rides the root as note="...--top-k=0 for the payload alone..." (not stderr-only): a caller reading only stdout sees both the answer and how to drop the map on the next call. Absent whenever the bundle carries no map at all (top-k=0, exact-name default) or loses to whole-file, and whenever --top-k was explicit — the classic shape above carries no note= either. **Try it** @@ -1580,11 +1612,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + - - + + , with a BRANCH-FREE size(). Both, not one or the other. -// -// ── THE DESIGN, AND WHY IT IS THIS ONE ─────────────────────────────────────────────────────────────── -// The shape the host tree leans on hardest is `Map>` — many tiny id-lists (byName / -// canonByName / shard maps, and ~100 more structures after the conversion wave): WRITE-ONCE during the -// parse/merge, then READ-HOT during resolve. -// +// graph.h — resolve references → graph, build the in-edge CSR for PageRank, and the +// resolved out-edges for serialization. Ranking lives in pagerank.cpp. + +#include "model.h" +#include "elixir_resolve.h" // lexical module/name/arity resolution; reuses cached Binding records +#include "filter.h" // isTestPath — for the Q2 tested= post-pass +#include "pageview.h" // LB-H: kImportReachRowCap — the import tier's display cap lives with the rest of the truncation vocabulary +#include "nextverb.h" // cut-fix E: nextFlag / nextAttrXml — the import tier's importers_next= ... [17 more line(s); run it to see the whole thing] ``` @@ -1685,14 +1717,14 @@ _--no-redact: emit bodies verbatim (credential redaction is on by default)._ ``` $ ./build/ripwire . --expand=readAckRecords --top-k=0 --no-redact - + + - - - - - - + + + + - - - - + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1755,20 +1787,20 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - - - - - + + + + + + + + - + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1793,18 +1825,18 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - - - - - - + + + + + + + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1821,17 +1853,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1852,15 +1884,15 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - - - - - + + + + + + - + ``` @@ -1885,7 +1917,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1920,18 +1952,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] ``` @@ -1951,18 +1983,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] ``` @@ -1988,13 +2020,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] ``` @@ -2019,9 +2051,9 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - - - + + + @@ -2030,7 +2062,7 @@ $ ./build/ripwire . --naming-calibration

-

+

... [14 more line(s); run it to see the whole thing] ``` @@ -2054,18 +2086,18 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - + + + + - + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2091,19 +2123,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] ``` @@ -2126,11 +2158,11 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + - + @@ -2159,16 +2191,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - - + + - - - - + + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2191,16 +2223,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] ``` @@ -2217,19 +2249,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] ``` @@ -2246,19 +2278,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] ``` @@ -2284,7 +2316,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2314,7 +2346,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2340,7 +2372,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2367,19 +2399,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] ``` @@ -2476,19 +2508,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] ``` @@ -2590,19 +2622,19 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - - - - - - + + + + + + - - - - - + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2621,8 +2653,8 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - + ``` @@ -2646,19 +2678,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] ``` @@ -2683,16 +2715,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2688 files · 25558 symbols · 40392 edges · 2494 modules (11469 call-graph isolated) +2849 files · 27441 symbols · 26516 edges · 3248 modules (12310 call-graph isolated) Root: `.` -Call-graph isolate provenance: 3087 declaration, 1678 header, 2119 source, 4585 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3292 declaration, 1982 header, 2365 source, 4671 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 2494) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1685 symbols -- **src::append@elixir_resolve.h:111:4875 [add,resolve,compute]** — 1021 symbols -- **src::emplace@infra/svector.h:408:22477 [resolve,scan,find]** — 605 symbols +## 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 ... [17 more line(s); run it to see the whole thing] ``` @@ -2708,20 +2740,20 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - - - - - + + + + + + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2740,17 +2772,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
7731"] + n101["src
8097"] n102["src/infra
1280"] end subgraph sg1 ["test"] - n103["test
5057"] + 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"] - n535["test/rubyattrsfix
85"] - n411["test/massfix
77"] - n492["test/receiverevidencefix/js/lib
74"] + n608["test/rubyschemafix
99"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2769,19 +2801,19 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - - - + + + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2800,7 +2832,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2852,20 +2884,20 @@ $ ./build/ripwire . --quality-baseline --allow-dirty **Shaped by:** `--quality-baseline` -### `--quality-delta` +### `--show-stale` -**Answers:** before a PR: report ONLY what your change made worse, across 12 kinds — pair with --test-gate. +**Answers:** (with --quality-delta) print the stale-ack rows and the full header a clean answer collapses — a regression-free answer keeps only stale= and stale_by_kind= (its rows collapsed), and on a tree that IS its HEAD only the verdict; -Each kind is measured against the baseline (complexity/verbosity/nesting/params/dup/dead/api-surface + error-masking/short-horizon-churn/new-clone-of-reused-helper + placeholder + defect-shape); every finding is classified by ORIGIN: a symbol that EXISTED at the baseline and got worse (preexisting-worse="N", no attribute on the row) vs one that exists only because the code is NEW (new-symbol="N", origin="new-symbol" on the row). A small numeric delta is additionally sev="minor". EXIT 2 ONLY on a major AND unacked row that is preexisting-worse or defect-shape format-arity (any origin) — the gating="N" header count. New-symbol rows are still PRINTED (they are the debt you are adding — read them); new-symbol rows never gate, except defect-shape format-arity; exit 0 means "nothing that already existed got worse", not "clean". Clone kinds classify by member set (new-symbol only if EVERY member is new); short-horizon-churn is preexisting by construction. LIMIT: origin is canonId (path::scope::name) identity, so a RENAMED/MOVED symbol reads as new and a regression carried in with the move will not gate. error-masking = a NEW empty/pass/comment-only handler, or log-only (a broad handler whose body only logs and never names the error) or rethrow-only (the sole handler re-throws it unchanged); those two gate only where their precision was measured (Python) and are sev="minor" in every other language. placeholder = a stub the change ADDED (todo!()/unimplemented!(), Kotlin TODO(), NotImplementedException, a bare raise NotImplementedError as a free function's body, a throw/raise/panic/assert saying "not implemented") or a comment line opening with TODO/FIXME that names no issue (#12, ABC-12, a URL); new-symbol by construction, so it never gates. Counted per enclosing symbol, like error-masking: a file-level TODO outside every definition is not counted. defect-shape = a known defect shape the change ADDED, named by defect=: format-arity (a literal std::format/print/format_to, fmt::, rw::emitTo/formatTo or Python "literal".format whose fields do not match its arguments — GATES on any origin, a defect not debt), utf8-cut and dedup-first (C++) and vacuous-assert (Bash test scripts) — report-only, always sev="minor". A site is new only when its text occurs more often than in the baseline: a moved one is not. Test-fixture dirs + doc sections are exempt from dead-code/churn; churn needs COMMITTED thrash evidence (rewritten across recent commits AND again by this diff), never the current edit alone WHICH FLOOR IT COMPARES AGAINST, and a side effect: the sidecar is honored only when the sha it was pinned at EQUALS the current git HEAD (strict equality — an ancestor commit describes a DIFFERENT tree, so everything committed since would read as your regression). A sidecar pinned anywhere else is STALE: this verb then DELETES it from your working tree (self-heal, so the next run does not rediscover the dead pin) and auto-compares the working tree vs git HEAD instead. Re-pin with --quality-baseline. The read-only MCP quality_delta verb applies the SAME staleness test but never deletes. A sidecar at the current HEAD that ANOTHER ripwire build pinned (its producer stamp names other sources — a dead set depends on how calls were resolved) is FOREIGN: both arms ignore it, never delete it, and auto-compare vs git HEAD. Which floor was actually used is on every report as baseline=: sidecar | git-HEAD | git-HEAD (stale sidecar removed) | git-HEAD (stale sidecar ignored) | git-HEAD (foreign sidecar ignored) — the stale two say a stale sidecar existed, and 'removed' means the file is gone. A non-git root has no HEAD to fall back to, so its sidecar is honored whenever this build pinned it; without one there, or with another build's, the verb exits 1. +either fold only when it saves a quarter of the bytes. Refused alone. **Try it** -_On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below._ +_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._ ``` -$ ./build/ripwire . --quality-delta - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. declined-call-excluded=N: symbols kept out of dead-code only because a declined call may mean them; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + @@ -2881,7 +2913,24 @@ $ ./build/ripwire . --quality-delta ... [17 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--dmm`, `--quality-ack`, `--scope`, `--json` +### `--quality-delta` + +**Answers:** before a PR: report ONLY what your change made worse, across 12 kinds — pair with --test-gate. + +Each kind is measured against the baseline (complexity/verbosity/nesting/params/dup/dead/api-surface + error-masking/short-horizon-churn/new-clone-of-reused-helper + placeholder + defect-shape); every finding is classified by ORIGIN: a symbol that EXISTED at the baseline and got worse (preexisting-worse="N", no attribute on the row) vs one that exists only because the code is NEW (new-symbol="N", origin="new-symbol" on the row). A small numeric delta is additionally sev="minor". EXIT 2 ONLY on a major AND unacked row that is preexisting-worse or defect-shape format-arity (any origin) — the gating="N" header count. New-symbol rows are still PRINTED (they are the debt you are adding — read them); new-symbol rows never gate, except defect-shape format-arity; exit 0 means "nothing that already existed got worse", not "clean". Clone kinds classify by member set (new-symbol only if EVERY member is new); short-horizon-churn is preexisting by construction. LIMIT: origin is canonId (path::scope::name) identity, so a RENAMED/MOVED symbol reads as new and a regression carried in with the move will not gate. error-masking = a NEW empty/pass/comment-only handler, or log-only (a broad handler whose body only logs and never names the error) or rethrow-only (the sole handler re-throws it unchanged); those two gate only where their precision was measured (Python) and are sev="minor" in every other language. placeholder = a stub the change ADDED (todo!()/unimplemented!(), Kotlin TODO(), NotImplementedException, a bare raise NotImplementedError as a free function's body, a throw/raise/panic/assert saying "not implemented") or a comment line opening with TODO/FIXME that names no issue (#12, ABC-12, a URL); new-symbol by construction, so it never gates. Counted per enclosing symbol, like error-masking: a file-level TODO outside every definition is not counted. defect-shape = a known defect shape the change ADDED, named by defect=: format-arity (a literal std::format/print/format_to, fmt::, rw::emitTo/formatTo or Python "literal".format whose fields do not match its arguments — GATES on any origin, a defect not debt), utf8-cut and dedup-first (C++) and vacuous-assert (Bash test scripts) — report-only, always sev="minor". A site is new only when its text occurs more often than in the baseline: a moved one is not. Test-fixture dirs + doc sections are exempt from dead-code/churn; churn needs COMMITTED thrash evidence (rewritten across recent commits AND again by this diff), never the current edit alone WHICH FLOOR IT COMPARES AGAINST, and a side effect: the sidecar is honored only when the sha it was pinned at EQUALS the current git HEAD (strict equality — an ancestor commit describes a DIFFERENT tree, so everything committed since would read as your regression). A sidecar pinned anywhere else is STALE: this verb then DELETES it from your working tree (self-heal, so the next run does not rediscover the dead pin) and auto-compares the working tree vs git HEAD instead. Re-pin with --quality-baseline. The read-only MCP quality_delta verb applies the SAME staleness test but never deletes. A sidecar at the current HEAD that ANOTHER ripwire build pinned (its producer stamp names other sources — a dead set depends on how calls were resolved) is FOREIGN: both arms ignore it, never delete it, and auto-compare vs git HEAD. Which floor was actually used is on every report as baseline=: sidecar | git-HEAD | git-HEAD (stale sidecar removed) | git-HEAD (stale sidecar ignored) | git-HEAD (foreign sidecar ignored) — the stale two say a stale sidecar existed, and 'removed' means the file is gone. A non-git root has no HEAD to fall back to, so its sidecar is honored whenever this build pinned it; without one there, or with another build's, the verb exits 1. + +**Try it** + +_On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below._ + +``` +$ ./build/ripwire . --quality-delta + + +``` + +**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--dmm`, `--quality-ack`, `--scope`, `--json` **Caveats (stated by the binary):** @@ -2901,24 +2950,12 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - - - - - - - - - - - - - -... [17 more line(s); run it to see the whole thing] + + ``` -**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--dmm`, `--quality-ack`, `--scope`, `--json` +**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--dmm`, `--quality-ack`, `--scope`, `--json` **Caveats (stated by the binary):** @@ -2939,7 +2976,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

@@ -3008,8 +3045,8 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. declined-call-excluded=N: symbols kept out of dead-code only because a declined call may mean them; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. r surface=: the api-surface tier, new-symbol or contract-change. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major). r origin=new-symbol: a finding on NEW code (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -3030,7 +3067,7 @@ $ ./build/ripwire . --quality-delta --scope=src/graph.h **Answers:** after an edit: did SYM's contract change, and which callers no longer fit? fast per-symbol post-edit contract check: SYM's param count + publicness NOW vs git HEAD (unchanged/new-symbol/contract-change with was/now), plus its 1-hop callers with any call-site provably incompatible with the NEW arity flagged. -A contract is PER DEFINITION, so a SYM matching several definition sites REFUSES (exit 1) and lists the file:name spellings that pick one — unlike --callers/--uses, this verb may not union overloads and disclose defs=. A .ripwire_notes entry targeting SYM (or its file) rides along as a child, the same row shape --for/--expand surface. PRE-APPLY PREVIEW — add --edit-payload=FILE|- --dry-run to ask the SAME question about bytes that have NOT been written yet. The payload is spliced over SYM's definition span in memory, exactly as --replace-symbol-body would write it; that one file is re-parsed, the call graph rebuilt over the re-derived tree, and the same document emitted with preview="1" plus an child holding the CURRENT span the apply would replace, as on disk (over 4 KB: the head, with shown=/capped="1"/elided_lines=) — preview then apply, no Read. Nothing is written, and every other file plus the git HEAD baseline stay the real tree's. Refuses, exit 1, on a payload that is unreadable, empty, oversize or NUL-bearing, on one whose splice raises the file's parse errors, on one that does not define SYM, and on a span the file's current bytes no longer fit. Single-root, and it previews a body REPLACEMENT only. +A contract is PER DEFINITION, so a SYM matching several definition sites REFUSES (exit 1) and lists the file:name spellings that pick one — unlike --callers/--uses, this verb may not union overloads and disclose defs=. A .ripwire_notes entry targeting SYM (or its file) rides along as a child, the same row shape --for/--expand surface. An unchanged contract with nothing flagged, on a tree that IS its HEAD (no +dirty), answers with its verdict alone when that saves a quarter of the bytes: callers= counts the caller rows, rows_next= (--limit=N) lists them. A dirty tree lists them. PRE-APPLY PREVIEW — add --edit-payload=FILE|- --dry-run to ask the SAME question about bytes that have NOT been written yet. The payload is spliced over SYM's definition span in memory, exactly as --replace-symbol-body would write it; that one file is re-parsed, the call graph rebuilt over the re-derived tree, and the same document emitted with preview="1" plus an child holding the CURRENT span the apply would replace, as on disk (over 4 KB: the head, with shown=/capped="1"/elided_lines=) — preview then apply, no Read. Nothing is written, and every other file plus the git HEAD baseline stay the real tree's. Refuses, exit 1, on a payload that is unreadable, empty, oversize or NUL-bearing, on one whose splice raises the file's parse errors, on one that does not define SYM, and on a span the file's current bytes no longer fit. Single-root, and it previews a body REPLACEMENT only. **Try it** @@ -3039,13 +3076,13 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - - - - + + + + + ``` @@ -3055,8 +3092,8 @@ $ ./build/ripwire . --edit-check=rankGraphTeleport **Caveats (stated by the binary):** - A contract is PER DEFINITION, so a SYM matching several definition sites REFUSES (exit 1) and lists the file:name spellings that pick one — unlike --callers/--uses, this verb may not union overloads and disclose defs=. +- An unchanged contract with nothing flagged, on a tree that IS its HEAD (no +dirty), answers with its verdict alone when that saves a quarter of the bytes: callers= counts the caller rows, rows_next= (--limit=N) lists them. - that one file is re-parsed, the call graph rebuilt over the re-derived tree, and the same document emitted with preview="1" plus an child holding the CURRENT span the apply would replace, as on disk (over 4 KB: the head, with shown=/capped="1"/elided_lines=) — preview then apply, no Read. -- Refuses, exit 1, on a payload that is unreadable, empty, oversize or NUL-bearing, on one whose splice raises the file's parse errors, on one that does not define SYM, and on a span the file's current bytes no longer fit. ### `--replace-symbol-body=TARGET` @@ -3093,12 +3130,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":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"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"}, {"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":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"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"} ``` ### `--insert-after-symbol=TARGET` @@ -3114,12 +3151,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":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"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"}, {"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":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"bash test/greptiercheck.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"} ``` **Caveats (stated by the binary):** @@ -3165,12 +3202,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":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"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"}, {"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":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"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"} ``` **Shaped by:** `--insert-after-symbol` @@ -3229,11 +3266,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":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 529 more bytes on this line] +{"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] {"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":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true}}]} +"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3247,7 +3284,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_l= is its call-site LINES (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), 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** @@ -3279,19 +3316,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] ``` @@ -3316,17 +3353,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] @@ -3370,10 +3407,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:7926 +$ ./build/ripwire . --at=src/graph.h:9117 - - + + ``` @@ -3397,7 +3434,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3423,11 +3460,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3502,7 +3539,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3536,19 +3573,19 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - + - + + - - + ... [17 more line(s); run it to see the whole thing] ``` @@ -3572,18 +3609,18 @@ _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] ``` @@ -3598,7 +3635,7 @@ $ ./build/ripwire . --stray-content=lane/ --abi **Answers:** find which branch's tree defines or mentions SYM, HEAD first which REF's tree defines or mentions SYM — HEAD first, then every local branch, with on-head="0" naming the case the verb exists for: content that lives only on a branch. -Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed and the kind="ref" rows counted in one element, when that page lists MORE definitions than the --whereis-listing=all page under the row cap, or the same ones in strictly fewer bytes; otherwise every hit is listed. +Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on another ref's row (ref_labels="parsed") it is the same extraction run over that ref's blob, once per blob and path, and only for a blob holding a line a loose lexical test calls worth parsing (it only CHOOSES blobs): a blob without one reads kind="ref". kind="text" is such a line no parse read ( counts them; --detail=1 lifts the parse guard), never a def. head_labels="lexical" ⇒ HEAD fell back to that lexical test (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" (and kind="text") rows are listed and the kind="ref" rows counted in one element, when that page lists MORE definitions than the --whereis-listing=all page under the row cap, or the same ones in strictly fewer bytes; otherwise every hit is listed. On that page a definition line several refs hold (same path, same text) is ONE row with refs="N", and folded= on the root counts the rows it stands for (on the first page, shown + more + folded + refs count = hits); listing=all prints each. PREVIEW: a line over 300 bytes prints t= as its first bytes (cut on a UTF-8 boundary) plus an ellipsis, 200 bytes in all, and the row carries t_bytes= (the whole line's size), when that makes the page shorter than the same page whole; counts them, and its next= (--whereis-full-text) replays that page with every line whole. **Try it** @@ -3606,29 +3643,57 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - - -... [24 more line(s); run it to see the whole thing] + + + + + + + +... [14 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--whereis-listing`, `--with-history`, `--limit` +**Shaped by:** `--whereis-full-text`, `--whereis-listing`, `--with-history`, `--limit` **Caveats (stated by the binary):** -- on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. -- head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). +- --detail=1 lifts the parse guard), never a def. - refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. +- LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. + +### `--whereis-full-text` + +**Answers:** with --whereis: print every t= line whole (no preview, no t_bytes=). + +The listing, window, rows and labels are the page's own, so the of a page that printed a preview names it. --detail lifts the preview too, with the row cap and the parse guard. Refused without --whereis. MCP: full_text=true. + +**Try it** + +_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._ + +``` +$ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text + + + + + + +... [24 more line(s); run it to see the whole thing] +``` + +**Shaped by:** `--whereis` + +**Caveats (stated by the binary):** + +- Refused without --whereis. ### `--whereis-listing=WHICH` **Answers:** with --whereis: which rows to list, defs, refs or all (default: more defs, else shorter). -The default lists defs when that page lists MORE definitions than all under the same row cap (references can fill a capped all page before the branch definitions arrive), whatever its bytes; when both list the same definitions, defs only when strictly shorter in bytes, else all (a tie lists all). defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. +The default lists defs when that page lists MORE definitions than all under the same row cap (references can fill a capped all page before the branch definitions arrive), whatever its bytes; when both list the same definitions, defs only when strictly shorter in bytes, else all (a tie lists all). defs lists every kind="def" (and kind="text") row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. **Try it** @@ -3636,14 +3701,15 @@ _The whole hit list asked for by name: under the same 60-row cap HEAD's referenc ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - - - + + + + - - + @@ -3749,8 +3815,8 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - + @@ -3761,7 +3827,7 @@ $ ./build/ripwire . --field-affinity - + ... [17 more line(s); run it to see the whole thing] ``` @@ -3784,18 +3850,19 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - -... [19 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [18 more line(s); run it to see the whole thing] ``` **Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit` @@ -3819,18 +3886,19 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - -... [19 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [18 more line(s); run it to see the whole thing] ``` **Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit` @@ -3853,18 +3921,18 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - - - - - - - - - - + + + + + + + + + + + + ... [18 more line(s); run it to see the whole thing] ``` @@ -3889,7 +3957,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3924,10 +3992,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:7926 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:7967 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1634 - #3 0x102e01a44 in main src/main.cpp:3999 + #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 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -3954,7 +4022,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4044,17 +4112,17 @@ _ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" - + + (run= when derivable). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : rank N in this ranking, rows in r= order. : cy … [line truncated: 445 more bytes on this line] + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4081,13 +4149,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). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> - - - + 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 ) @@ -4114,10 +4182,10 @@ _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 @@ -4188,14 +4256,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + @@ -4222,19 +4290,20 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + -... [2 more line(s); run it to see the whole thing] + +... [1 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -4257,7 +4326,7 @@ $ ./build/ripwire . --skipped - @@ -4343,21 +4412,21 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + - - - - - + + + + + + - - - + + - - -... [9 more line(s); run it to see the whole thing] + + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--since`, `--in` @@ -4378,22 +4447,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~4 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - - - - - -... [11 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [12 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4454,8 +4523,8 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - - + @@ -4483,7 +4552,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=593769ab691623d8 entries=806 +ripwire legend dictionary ripwire.dict/v1 dictv=80b7e589253a8e76 entries=855 : 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=) @@ -4526,7 +4595,7 @@ $ ./build/ripwire . --hotspots --json **Answers:** paginate a high-cardinality verb paginate a high-cardinality verb. -HONORED by: --deps --callers --callees --tree --lint --hotspots --clones --cochange --owners --communities --community --doc-drift --whereis --grep/--regex --match --pattern --impact --uses --exercises --seams --zoom --external-surface --dead-code --mentions --graph-query --stray-content --test-gate --biggest-first --ensemble --quality-panel --context-ratio --nonlocal-state --comment-coherence --naming-consistency --safe-delete --pr-context --edit-check --flags --situ --for. Emit at most N rows, skipping the first M; N overrides the verb's own display cap (40 hotspot files, 30 co-change pairs, 60 whereis hits, 100 grep/match hits, 40 impact rows, 20 seam pairs, 40 readability rows, 40 ensemble symbol rows, 40 context-ratio symbol rows, 40 nonlocal-state rows, 200 graph-query rows / --top-k, 40 unflagged --edit-check caller rows, 8 --flags read sites per gate, 25 --flip context rows per listing, 8 --situ blast-radius files and 8 co-change partners). A verb NEVER pages the rows that ARE its answer: --edit-check's flagged callers, --flip's and --situ's tests-to-run rows and --flags' gate rows ride every page in full, and every verdict/count attribute is computed over the full set first. With --offset alone (no --limit) the verb's own default page size applies and the root discloses limit="0" — on OUTPUT that 0 means 'no explicit --limit', never a zero-row page (the flag itself refuses --limit=0). A BARE run whose default cap cut rows (capped="1") carries the same limit="0" and the whole paging block below, so you can page from the first answer without guessing. Deterministic seams (rows are already sorted) so --offset=N is the exact continuation of the previous --limit=N page. On --for, --limit=N/--offset=M do not window the bundle: they select its FILE-GRAIN WIDENING PAGE instead (one row per positive-score file, ranked file-first; see --for), the answer to "the head missed it, show me more files". The root element then carries shown= capped= total= has_more= next_offset= offset= limit= — loop until has_more="0". capped= compares the PAGE to the total (1 ⇔ shown < total), so a page past the end reads shown="0" capped="1" has_more="0": nothing was cut, the offset skipped everything — EXCEPT the verbs with TWO INDEPENDENT listings, which carry the noun-prefixed form instead (one shown= could only describe one): --test-gate shown_tests=/tests_capped= + shown_untested=/untested_capped=, --communities shown_modules=/modules_capped= + shown_bridges=/bridges_capped=, --ensemble and --context-ratio shown_syms=/syms_capped= + shown_files=/files_capped=; the window takes the PRIMARY listing (--test-gate's rows; its rows repeat on every page, complete). --edit-check is the same shape for a different reason: its rows split into the ANSWER (callers flagged incompatible="1", with their complete sites_l=) and the CONTEXT (unflagged callers). Only the context pages — shown_unflagged=/unflagged_capped=, with total= the unflagged count — while the flagged rows and the overload census ride every page in full and status=/defs=/callers=/incompatible= are computed over the FULL caller set before any window, so a page can never make the verdict say less than it knows. Any verb NOT in that list REFUSES both flags (exit 1) rather than accepting and ignoring them: budget/top-k verbs (--recall/--pack-task/--from-trace/ --expand/--outline/--pack-signatures/--format=candidates) are shaped by --top-k/--max-tokens/--token-budget, not a page (--for's bare bundle is shaped by --token-budget the same way, and takes --limit/--offset only as its file page, where the budget flags are refused in turn); the rest (--path/--connect/ --around/--exemplar/--report/--mermaid/--map-diff/--metrics and the default map) answer with a single fixed-shape result that has no row list to window at all. +HONORED by: --deps --callers --callees --tree --lint --hotspots --clones --cochange --owners --communities --community --doc-drift --whereis --grep/--regex --match --pattern --impact --uses --exercises --seams --zoom --external-surface --dead-code --mentions --graph-query --stray-content --test-gate --biggest-first --ensemble --quality-panel --context-ratio --nonlocal-state --comment-coherence --naming-consistency --safe-delete --pr-context --edit-check --flags --situ --for --lego. Emit at most N rows, skipping the first M; N overrides the verb's own display cap (40 hotspot files, 30 co-change pairs, 60 whereis hits, 100 grep/match hits, 40 impact rows, 20 seam pairs, 40 readability rows, 40 ensemble symbol rows, 40 context-ratio symbol rows, 40 nonlocal-state rows, 200 graph-query rows / --top-k, 40 unflagged --edit-check caller rows, 8 --flags read sites per gate, 25 --flip context rows per listing, 8 --situ blast-radius files and 8 co-change partners). A verb NEVER pages the rows that ARE its answer: --edit-check's flagged callers, --flip's and --situ's tests-to-run rows and --flags' gate rows ride every page in full, and every verdict/count attribute is computed over the full set first. With --offset alone (no --limit) the verb's own default page size applies and the root discloses limit="0" — on OUTPUT that 0 means 'no explicit --limit', never a zero-row page (the flag itself refuses --limit=0). A BARE run whose default cap cut rows (capped="1") carries the same limit="0" and the whole paging block below, so you can page from the first answer without guessing. Deterministic seams (rows are already sorted) so --offset=N is the exact continuation of the previous --limit=N page. On --for, --limit=N/--offset=M do not window the bundle: they select its FILE-GRAIN WIDENING PAGE instead (one row per positive-score file, ranked file-first; see --for), the answer to "the head missed it, show me more files". The root element then carries shown= capped= total= has_more= next_offset= offset= limit= — loop until has_more="0". capped= compares the PAGE to the total (1 ⇔ shown < total), so a page past the end reads shown="0" capped="1" has_more="0": nothing was cut, the offset skipped everything — EXCEPT the verbs with TWO INDEPENDENT listings, which carry the noun-prefixed form instead (one shown= could only describe one): --test-gate shown_tests=/tests_capped= + shown_untested=/untested_capped=, --communities shown_modules=/modules_capped= + shown_bridges=/bridges_capped=, --ensemble and --context-ratio shown_syms=/syms_capped= + shown_files=/files_capped=; the window takes the PRIMARY listing (--test-gate's rows; its rows repeat on every page, complete). --edit-check is the same shape for a different reason: its rows split into the ANSWER (callers flagged incompatible="1", with their complete sites_l=) and the CONTEXT (unflagged callers). Only the context pages — shown_unflagged=/unflagged_capped=, with total= the unflagged count — while the flagged rows and the overload census ride every page in full and status=/defs=/callers=/incompatible= are computed over the FULL caller set before any window, so a page can never make the verdict say less than it knows. Any verb NOT in that list REFUSES both flags (exit 1) rather than accepting and ignoring them: budget/top-k verbs (--recall/--pack-task/--from-trace/ --expand/--outline/--pack-signatures/--format=candidates) are shaped by --token-budget the same way; --for takes --limit/--offset as its FILE page, where the budget flags are refused in turn (bare --pack-task --limit refuses: it has no page without a budget), and --for/--pack-task beside --token-budget take the window as the BUDGETED-BUNDLE CANDIDATE PAGE — one window over the ranked candidates, signatures only (bodies come via next= or --expand), their resumable continuation; the rest (--path/--connect/ --around/--exemplar/--report/--mermaid/--map-diff/--metrics and the default map) answer with a single fixed-shape result that has no row list to window at all. **Try it** @@ -4535,23 +4604,23 @@ _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] ``` -**Shaped by:** `--top-k`, `--for`, `--tree`, `--graph-query`, `--external-surface`, `--impact`, `--exercises`, `--community` +**Shaped by:** `--top-k`, `--for`, `--lego`, `--tree`, `--graph-query`, `--external-surface`, `--impact`, `--exercises` **Caveats (stated by the binary):** @@ -4569,24 +4638,24 @@ _Drop matching paths (repeatable) before ranking._ ``` $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + - - - - + + + + - - - + + + + - - -... [11 more line(s); run it to see the whole thing] + +... [17 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--skipped`, `--in`, `--index-out` +**Shaped by:** `--expand`, `--skipped`, `--in`, `--index-out` ### `--map-diff` @@ -4600,21 +4669,21 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch ``` $ ./build/ripwire . --map-diff --top-k=5 - + - - - - + + + + - - - + + + + - - -... [11 more line(s); run it to see the whole thing] + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--json`, `--limit` @@ -4634,20 +4703,21 @@ _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] ``` **Shaped by:** `--doctor`, `--index-out` @@ -4683,20 +4753,21 @@ _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] ``` **Shaped by:** `--doctor` @@ -4713,22 +4784,25 @@ _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] ``` +**Shaped by:** `--expand` + **Caveats (stated by the binary):** - by default they are skipped crawl paths the repository's own .gitignore covers. @@ -4746,24 +4820,24 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea ``` $ ./build/ripwire . --max-file-size=8K --top-k=3 - + - - - - + + + + - - + + - - -... [3 more line(s); run it to see the whole thing] + + +... [17 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--skipped` +**Shaped by:** `--expand`, `--skipped` **Caveats (stated by the binary):** @@ -4826,20 +4900,21 @@ _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] ``` ### `--mcp` @@ -4881,10 +4956,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` | 683 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) @@ -1160,7 +1160,7 @@ rows are hash-only — and no number resembling it will be reported from it. **Window 1 (registered 2026-09-02): VOID for the verdict.** Until issue #381 an unconfigured install resolved every session to `treatment`, so this window had no control arm, and the registered treatment − control difference cannot be computed from it. No verdict is read from it. Its four- and six-week dates (2026-09-30, 2026-10-14) are not readout dates. **Window 2: the readout clock restarts at the release that ships `arm=auto` as the default.** The rule is unchanged: one readout at four weeks, at least 40 recommended prompts per arm, and one extension to six weeks if the readout is underpowered. -**Window start:** PENDING (the release that ships `arm=auto` writes its release date here as YYYY-MM-DD) +**Window start:** 2026-10-11 `bench/routing_ab_report.py` reads the `Window start` line: a date gives the readout date (start + 28 days) and the one-time extension date (start + 42 days); `PENDING` gives no date. @@ -5861,7 +5861,7 @@ four `--quality-delta` shapes, against 2–6 before: this lane closed `sa@key`, shapes by absolute legend size and reports the fraction as INFO. A `legend<=payload` arm is unsatisfiable on the case these verbs exist to handle well — a clean tree's payload is near-zero by construction — so at 40% of a 572 B payload the whole legend would have to fit in 381 B, shorter than -the list of the ten measured kinds (eleven since 0.6.5). **A ≤40% relative ceiling for `--quality-delta` and ≤50% for +the list of the ten measured kinds (twelve kinds now, with placeholder and defect-shape). **A ≤40% relative ceiling for `--quality-delta` and ≤50% for `--safe-delete`/`--test-gate` were considered and are recorded as UNREACHABLE rather than as missed work**: post-fix fractions are 87.0% / 91.4% / 78.8% on the clean cases and 81.4–86.7% wherever the payload is real. The same reasoning `test/testgatelegendbudgetcheck.sh` recorded in 2026-08-28. @@ -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 **678 gate scripts**, all of which exist on disk. +naming **683 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`. @@ -6196,7 +6196,7 @@ These are the exact `kind=` strings the binary emits, from `src/quality.h`: `complexity` · `verbosity` · `nesting` · `params` · `duplication` · `dead-code` · `api-surface` · `error-masking` · `short-horizon-churn` · `new-clone-of-reused-helper` · `placeholder` · `defect-shape` -The first ten each target a failure mode measured in the literature; `placeholder` (added 0.6.5) is an +The first ten each target a failure mode measured in the literature; `placeholder` (added 0.6.7) is an honesty check on "done" — a stub or TODO the change added — and never gates. `defect-shape` carries four facets in `defect=` (`format-arity`, `utf8-cut`, `dedup-first`, `vacuous-assert`), shapes this project's own code review kept finding; only `format-arity` gates, and on any origin (new-symbol rows never gate, except @@ -6220,9 +6220,9 @@ output. The verb reports only what a change made *worse*, against git HEAD. `--quality-ack` records a reviewed exception; `--ack-only=KIND` scopes it. -### error-masking widened: log-only and rethrow-only (0.6.5) — hand-labelled precision, and which shapes gate +### error-masking widened: log-only and rethrow-only (0.6.7) — hand-labelled precision, and which shapes gate -`error-masking` counted only an EMPTY handler (empty braces, `pass`, `...`, a comment-only body). 0.6.5 adds +`error-masking` counted only an EMPTY handler (empty braces, `pass`, `...`, a comment-only body). 0.6.7 adds two shapes from `src/handlershape.h`, walked over the same parse the query rows use: - **log-only** — a BROAD handler (catches everything, or the root error type: `Exception`, `Throwable`, @@ -6274,7 +6274,7 @@ each, 24 gating across the 40). This repository has no Python/JS handler of eith added in that window, so on upgrade its own reports are unchanged; the replay shows the widening costs no noise here, not that it finds anything here. The rows the shapes produce were measured on the corpus above. -### placeholder (0.6.5) — what the eleventh kind counts, and a labelled sample +### placeholder (0.6.7) — what the eleventh kind counts, and a labelled sample A stub or TODO the change ADDED, counted per enclosing symbol and compared with the baseline like error-masking. Every row is `origin="new-symbol"` by construction and never gates. It counts: Rust @@ -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 678. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 683. 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 40041f310..b83ac804a 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 | | --- | --- | --- | --- | -| 240 | 96 | 140 | **100** | +| 253 | 98 | 149 | **104** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 240 + 7 is the 247 constants this generator parses out of `src/`. +are not counted as caps, and 253 + 7 is the 260 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. **132 of 240 caps are classified -(47 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 108 render `—`, which means NOT YET +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 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 96 files that declare a cap — the 240 caps counted above, and no parameter. +One table for each of the 98 files that declare a cap — the 253 caps counted above, and no parameter. ### `src/abicheck.h` @@ -190,6 +190,9 @@ Discloses: **none** | `kMaxRenameProbeFiles` | `32` | INDEXING | HEAD copies one rename probe reads; past it the probe stops (a hint, never a claim) | | `kMaxWorktreePaths` | `8192` | INDEXING | — | | `kWhereisHits` | `60` | OUTPUT | — | +| `kWhereisParseMaxBlobs` | `256` | — | — | +| `kWhereisTextPreviewCapBytes` | `200` | OUTPUT | the printed preview, the ellipsis included | +| `kWhereisTextWholeMaxBytes` | `300` | OUTPUT | a line up to this prints whole, so a cut drops 100+ bytes | ### `src/darkflags.h` @@ -323,7 +326,7 @@ Discloses: `capped`, `hosts_capped` ### `src/forpage.h` -Discloses: **none** +Discloses: `capped` | constant | value | class | note | | --- | --- | --- | --- | @@ -359,6 +362,7 @@ Discloses: `importers_capped` | constant | value | class | note | | --- | --- | --- | --- | | `kChaConeCap` | `4096` | INDEXING | per-walk discovery cap, unchanged from the per-call walk | +| `kImpactFileRollupCap` | `20` | — | — | | `kMaxEdges` | `256` | — | total emitted edge cap | | `kMaxNodes` | `96` | — | total emitted node cap (§3 size caps) | | `kMaxRadius` | `12` | — | — | @@ -475,6 +479,7 @@ Discloses: **none** | constant | value | class | note | | --- | --- | --- | --- | +| `kExtendChainCap` | `16` | INDEXING | factory-of-a-factory links the receiver rule follows (nunjucks: 2); past it, no class | | `kMaxQualifierHops` | `32` | INDEXING | `a::b::c::…` past 32 segments is not written C++ | ### `src/ingest_parsepool.h` @@ -492,6 +497,7 @@ Discloses: **none** | constant | value | class | note | | --- | --- | --- | --- | +| `kBaseScopeScanCap` | `4096` | INDEXING | statements the unindexed scope scans read (not the index) | | `kMaxImportContainerDepth` | `256` | INDEXING | — | ### `src/ingest_sidecap.h` @@ -583,8 +589,18 @@ Discloses: **none** | constant | value | class | note | | --- | --- | --- | --- | +| `kMaxUnservedSelectors` | `16` | — | — | | `kRecentRows` | `40` | — | F3: ~45 B a row; the file-level answer, not the file list | +### `src/mapinventory.h` + +Discloses: `entries_capped`, `names_capped` + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kInventoryEntryCap` | `256` | OUTPUT | — | +| `kInventoryNameCeiling` | `2000` | OUTPUT | — | + ### `src/mcp.h` Discloses: `hits_capped` @@ -630,7 +646,6 @@ Discloses: `capped`, `blast_radius_capped`, `calledBy_capped`, `coboost_commits_ | `kMcpPageValueMax` | `1000000000` | — | == cli.h's kPageValueMax | | `kMcpRecallTopKMax` | `1000` | — | — | | `kOtherDefCap` | `4` | OUTPUT | disclosure, not a listing — cap the tail | -| `kRowCap` | `100` | — | — | ### `src/mention.h` @@ -708,6 +723,7 @@ Discloses: `capped`, `count_capped`, `findings_capped`, `hits_capped`, `importer | constant | value | class | note | | --- | --- | --- | --- | | `kCallHierarchyRowCap` | `40` | — | — | +| `kCallSitesPerRowCap` | `16` | — | — | | `kCochangePartnerCap` | `30` | — | — | | `kExternalSurfaceRowCap` | `100` | — | names, by ref count (≈ 5.2 KB on this repo) | | `kImportReachRowCap` | `40` | — | — | @@ -851,6 +867,7 @@ Discloses: `hits_capped` | constant | value | class | note | | --- | --- | --- | --- | | `kGrepCollectionBudget` | `4000000` | — | — | +| `kGrepLiftRowCap` | `10` | — | a RANKED code+string answer's default window (CLI and MCP alike) | | `kGrepMatchedLineMaxBytes` | `512` | — | — | | `kGrepTierFileBudget` | `128` | — | hit files classified per call | | `kMaxAffixSet` | `8` | — | cap on prefix/suffix set sizes | @@ -867,7 +884,7 @@ Discloses: **none** ### `src/serialize.h` -Discloses: `capped`, `calls_capped`, `inc_capped`, `sibs_capped` +Discloses: `capped`, `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` | constant | value | class | note | | --- | --- | --- | --- | @@ -879,6 +896,7 @@ Discloses: `capped`, `calls_capped`, `inc_capped`, `sibs_capped` | `kForFileTailShownCap` | `24` | — | — | | `kForLensDefaultTopN` | `40` | — | — | | `kForPayloadBudgetBytes` | `7500` | — | — | +| `kLegoClosureRowCap` | `200` | OUTPUT | — | | `kMaxExpandIncludes` | `24` | — | inc= cap | | `kMaxExpandSibs` | `100` | — | sibs= cap — a BLOW-UP GUARD, set above the tail, not a trim of the | | `kMaxSig` | `240` | OUTPUT | — | @@ -984,8 +1002,17 @@ Discloses: `capped`, `coboost_commits_capped`, `terms_capped` | constant | value | class | note | | --- | --- | --- | --- | +| `kForOwnerHopRows` | `3` | — | — | | `kJsonEnvelopeDigitsMax` | `10` | — | — | +### `src/verbs_grep.h` + +Discloses: `capped`, `hits_capped`, `importers_capped`, `unindexed_candidates_capped` + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kGrepHitRefsMax` | `4` | — | — | + ### `src/verbs_lint.h` Discloses: `capped`, `count_capped`, `ellipsis_capped`, `findings_capped`, `hits_capped`, `rows_capped` diff --git a/docs/LINEAGE.md b/docs/LINEAGE.md index 8bece8054..af4102e52 100644 --- a/docs/LINEAGE.md +++ b/docs/LINEAGE.md @@ -81,7 +81,7 @@ left to look like a paper. | ONTO — [arXiv:2604.17512](https://arxiv.org/abs/2604.17512) | Genuinely tabular rows re-encode far cheaper as parallel arrays than as repeated per-row markup. | `--format=columnar` (`src/columnar.h`) | | Controlled serialization study — [arXiv:2603.03306](https://arxiv.org/abs/2603.03306) | The same compact re-encoding collapses on *nested* data, and the headline savings are measured against pretty-printed JSON — a baseline minified XML already beats. | `--format=columnar` refuses every non-tabular verb with exit 1 instead of degrading quietly; the nested map is never re-encoded | | Tokenizer and formatting cost — [arXiv:2508.13666](https://arxiv.org/abs/2508.13666) | A single characters-per-token divisor is 20–35% wrong on code, and the spread is dominated by language, not by the tokenizer. | The per-language calibration behind `est_tokens` — and the decision *not* to vendor a byte-pair table, which would buy exactness for the wrong tokenizer | -| AI-generated code smells — [arXiv:2605.02741](https://arxiv.org/abs/2605.02741) | Bloat tracks architectural decay almost perfectly, and the named failure modes are god-class growth, inline re-implementation of existing interfaces, and file separation mistaken for cohesion. | Three of `--quality-delta`'s eleven kinds: verbosity, duplication, and new-clone-of-reused-helper (`src/quality.h`) | +| AI-generated code smells — [arXiv:2605.02741](https://arxiv.org/abs/2605.02741) | Bloat tracks architectural decay almost perfectly, and the named failure modes are god-class growth, inline re-implementation of existing interfaces, and file separation mistaken for cohesion. | Three of `--quality-delta`'s twelve kinds: verbosity, duplication, and new-clone-of-reused-helper (`src/quality.h`) | | Agent-code maintainability — [arXiv:2606.21804](https://arxiv.org/abs/2606.21804) | Classical complexity did not predict real agent-code maintenance failures. Contract drift and code growth did. | The api-surface kind in `--quality-delta`, and `--edit-check` as a standalone contract check (`src/editcheck.h`) | | Metric feedback into the loop — [arXiv:2505.23953](https://arxiv.org/abs/2505.23953) | Feeding measured complexity back after a failed attempt raised Pass@1 from 12.5% to 35.7%; the oracle has to be re-readable, not just a verdict. | `--quality-delta` reports only what a change made *worse*, itemised, in a form meant to go back into the next prompt | | ARISE — [arXiv:2605.03117](https://arxiv.org/abs/2605.03117) | Statement-level definition-use edges exposed as a queryable agent primitive lifted Function Recall@1 by 17 points over an unmodified SWE-agent — the graph's granularity floor, not its ranking, was the binding constraint. | `--slice=SYM[:VAR]` and the rung-2 `--slice-flow` (`src/slice.h`) — per-line def/use rows inside one resolved definition, plus the bounded transitive BFS over reaching-definition edges. **Bounded, and the output says so**: name-based, intra-procedural, no alias analysis, no control dependence, disclosed depth truncation; the paper's three-tier graph API remains a design note. The reference implementation is now published ([FARD-Lab/ARISE](https://github.com/FARD-Lab/ARISE), MIT) and was audited line-by-line against this slicer 2026-08-31 — the core reaching-definition rule matches; the divergences (their traversal is seed-variable-filtered and unbounded, ours chains operand variables under a disclosed bound) are recorded in [`EVALS.md`](EVALS.md) | @@ -273,7 +273,7 @@ disclosed; it is not a lesson this tool folded, so it does not get a row above. **Related work, not counted: [NVIDIA SkillSpector](https://github.com/NVIDIA/SkillSpector) (Apache-2.0).** The later hardening of `src/skillscan.h` — the `--scan-skill`/`--scan-skills` scanner and the `wrap` pre-install scan — was -informed by ideas from SkillSpector, surveyed as related work: in 0.6.6, `EXFILTRATE:net-exfil` is graded by whether a +informed by ideas from SkillSpector, surveyed as related work: in 0.6.7, `EXFILTRATE:net-exfil` is graded by whether a credential-shaped source is on the line (issue #353). No SkillSpector code or pattern text is included today. A counted lineage row, with exact per-rule attribution, will be added once the planned port of its code-based checks lands; until then it is not in the table above, and the counts stay at 49 repositories. diff --git a/docs/TUNING.md b/docs/TUNING.md index 0dc297ff9..8836074e1 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,26 +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 | | --- | --- | --- | --- | --- | --- | -| 150 | 149 | 112 | 12 | **37** | 75 | +| 161 | 160 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **150 cap -declarations** under **149 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 **161 cap +declarations** under **160 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 149 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **26 declared since the sweep was -prepared, which no measurement has touched** (`kChurnMergeBombMaxFiles`, -`kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, `kForPageRowsDefault`, -`kForPageUnionSymbolCap`, `kMaxAstQueryNesting`, +20 more). 111 + 12 + 26 = 149, 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 150" would be wrong in both halves at once, which is the shape of error a generated -table exists to prevent. +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 +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. ## 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 150-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 161-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. @@ -61,7 +60,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kForLensDefaultTopN` = `40` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `320` — **12 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `320` — **12 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -117,7 +116,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kForFileTailShownCap` = `24` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `192` — **10 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `192` — **10 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -134,7 +133,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kForPayloadBudgetBytes` = `7500` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `60000` — **10 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `60000` — **10 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -151,7 +150,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kForCapTailSigBytes` = `96` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `768` — **8 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `768` — **8 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -166,7 +165,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kMaxExpandSibs` = `100` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `800` — **5 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `800` — **5 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -219,7 +218,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kForAutoBodyBudgetBytes` = `6000` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `48000` — **2 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `48000` — **2 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -373,7 +372,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kMaxExpandIncludes` = `24` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `192` — **1 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `192` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | @@ -429,7 +428,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kWithGraphNodeCap` = `8` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `64` — **1 verb(s) respond** +`src/serialize.h` — discloses: `calls_capped`, `entries_capped`, `files_capped`, `importers_capped`, `inc_capped`, `names_capped`, `sibs_capped` — probe value `64` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 23dc272e6..ac7f16979 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -3921,29 +3921,29 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 # navigate — seeds, claims, slices, shapes -## `./build/ripwire . --at=src/graph.h:7397` +## `./build/ripwire . --at=src/graph.h:7512` *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:7397` +## `./build/ripwire . --callers=@src/graph.h:7512` *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* ````` - + - - + + diff --git a/docs/captures/COMMANDS_showcase_2026-10-10.md b/docs/captures/COMMANDS_showcase_2026-10-10.md new file mode 100644 index 000000000..306dccace --- /dev/null +++ b/docs/captures/COMMANDS_showcase_2026-10-10.md @@ -0,0 +1,7643 @@ +# ripwire — every verb, run for real + +- **Date:** 2026-10-10 (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.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=65d62fe5c)` +- **Repo:** the ripwire repo @ `6009ca69` — **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 | 1 + + src/infra/sortutil.h | 31 ++++++++++++++++++++++++++++--- + 3 files changed, 31 insertions(+), 3 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` (209 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [815 more display lines; full output is 26837 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8262 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=10806 > 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.31s** + +````` + + + + +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::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& impl … [line truncated: 63 more bytes on this line] +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t> … [line truncated: 25 more bytes on this line] +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 +… [48 more display lines; full output is 10037 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { +… [12 more display lines; full output is 4505 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.* + +````` + + + + + +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 +… [105 more display lines; full output is 16395 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.* + +````` + + + + + +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]).* + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [55 more display lines; full output is 9999 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.* + +````` + + + + +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 +struct LensRanking +inline bool hasIdentifierShape( std::string_view w ) noexcept +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::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +struct RecallSectionPick +inline bool canonicalIdMatches( std::string_view canonAbs, std::string_view spec ) noexcept +… [75 more display lines; full output is 10887 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.* + +````` + + + + +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 +struct LensRanking +inline bool hasIdentifierShape( std::string_view w ) noexcept +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::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +struct RecallSectionPick +inline bool canonicalIdMatches( std::string_view canonAbs, std::string_view spec ) noexcept +… [77 more display lines; full output is 10743 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).* + +````` + + + + +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 218 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5028 + +━━ README.md (relevance 8.080) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 217180 B; lines="2746-2758,2759-2766"; dropped_by_budget=23] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. When `--callers`, `--callees`, `--uses`, `--impact` or `--path` refuse a selector that matches no indexed definition, they also print an answer on stdout: the verb's element with `found="0"` and the name to retry with. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | +| 5 | The memory guard stopped the run: over `--max-memory` (default 65% of the machine's memory) with no partial answer possible, or a verb other than the default map facing a partial index. One stderr line names the limit and the override. | + +#### 6.3 JSON output + +`--json` emits JSON instead of XML, with keys mirroring the XML attribute names one to one. It is an +allow-list: the default map, `--for`, `--pack-task`, `--callers`, `--callees`, `--impact`, +`--quality-delta`, `--test-gate`, `--metrics`, and `--plan-lanes`, which is JSON-native. Every other +verb refuses on stderr with exit 1 rather than silently falling back to XML, so a verb added tomorrow +refuses by default. + + + +━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.429) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] +### Root spelling (maintainer-owned; reproduce it once so you recognise it) + +```bash +… [150 more lines, 12829 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 1392154-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.116) ━━ [sections: 1 of 160 selected (189 in doc), section-granular; whole doc 569309 B; lines="3756-3760"; dropped_by_budget=159] [truncated: 1865 of 7738 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [367 more display lines; full output is 11224 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 1.51s** + +````` +(empty) +````` + +Artifact written: + +````` + 159465 /aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8280 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [198 more display lines; full output is 9131 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [2701 more display lines; full output is 80092 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [20 more display lines; full output is 4221 bytes on 1 raw line(s)] +````` + +## `./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** + +````` + + +````` + +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 3649 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface` + +*Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5589 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 5449 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [50 more display lines; full output is 5372 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).* + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: 6009ca693 + (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.25s** + +````` + + +````` + +## `./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.26s** + +````` + +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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [24 more display lines; full output is 3484 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.* + +````` + + + + + +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 +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / - … [line truncated: 48 more bytes on this line] +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +… [108 more display lines; full output is 14910 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --pack-signatures --top-k=10` + +*Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [132 more display lines; full output is 11518 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.* + +````` + + + + +, with a BRANCH-FREE size(). Both, not one or the other. +// +// ── THE DESIGN, AND WHY IT IS THIS ONE ─────────────────────────────────────────────────────────────── +// The shape the host tree leans on hardest is `Map>` — many tiny id-lists (byName / +// canonByName / shard maps, and ~100 more structures after the conversion wave): WRITE-ONCE during the +// parse/merge, then READ-HOT during resolve. +// +// Three things matter for that shape, and the layout below gets all three: +// • no per-list malloc — the N small lists that would each allocate are inline; +// • a BRANCH-FREE size() — `return sz_`, because the size lives in its own field; +// • 16 BYTES per instance — because `inl_` and `heap_` are never both live, so they share storage. +// +// The union is the whole trick. The previous revision of this file paid 8 extra bytes (24 B) for the +// explicit size field, on the reasoning that a branch-free size() was worth it. That was a false +// choice: union the inline array with the heap pointer and the struct reaches 16 B — ankerl's size — +// while the size stays in its own field and size() stays branch-free. +// +// THE TRADE THAT REMAINS, stated honestly. At 16 bytes you can have: +// • ankerl::svector's 3 inline slots, with a size() that branches on is_direct() (and, once spilled, +// dereferences into the heap block to read the size — a dependent load, not just a branch); or +// • this type's 2 inline slots, with size() branch-free. +// The measurement chose the second. See bench/SVECTORAB.md; the short version is below. +// +… [1086 more display lines; full output is 66262 bytes on 1112 raw line(s)] +````` + +stderr: + +````` +ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchanged behavior for now) +````` + + +--- + +# assess quality / structure + +## `./build/ripwire . --metrics --top-k=10` + +*Fan-in/out + complexity annotations on the map.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [91 more display lines; full output is 10820 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [898 more display lines; full output is 23488 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 1.48s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5749 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 2.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [306 more display lines; full output is 16116 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 1.52s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 1.36s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5089 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [678 more display lines; full output is 99226 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 2.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [695 more display lines; full output is 70057 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:395, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* + +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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [188 more display lines; full output is 17290 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom` + +*Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [171 more display lines; full output is 8794 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [707 more display lines; full output is 28629 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +2787 files · 26903 symbols · 41775 edges · 2640 modules (11994 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 3221 declaration, 1908 header, 2215 source, 4650 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 2640) +- **src::emitTo@infra/emit.h:53:2760 [write,run,resolve]** — 1714 symbols +- **src::ASSUME@infra/Diagnostics.h:386:25510 [resolve,collect,add]** — 934 symbols +- **src::append@elixir_resolve.h:111:4875 [resolve,scan,read]** — 644 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 366 symbols +- **src::escapeXml@serialize.h:155:10267 [run,emit,write]** — 201 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:8950 [pack,allow,insert]** — 179 symbols +- **src/infra::fail@os_win32.cpp:63:2553 [close,open,release]** — 108 symbols +- **test/ownerhopfix/py/pkg::big_handler_001@broadcast.py:3:72** — 106 symbols +- **src::relForHash@model.h:1634:133594 [clone,compute,scan]** — 53 symbols +- **src/infra::DYNMAP_ASSUME@dynamic_map.hpp:90:4411 [erase,find,insert]** — 50 symbols +- **test/ownerhopfix/gain/pkg::target_01@fanout.py:74:1458 [push]** — 41 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,verify,make]** — 39 symbols + +## God files (most depended-on; showing 10 of 489) +- `test/lib/clean-env.sh` — 186 dependents +- `src/model.h` — 89 dependents +- `src/infra/emit.h` — 86 dependents +- `src/infra/Diagnostics.h` — 60 dependents +- `src/serialize.h` — 40 dependents +… [33 more lines, 3676 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [90 more display lines; full output is 12235 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
7915"] + n102["src/infra
1280"] + end + subgraph sg1 ["test"] + n103["test
5126"] + n392["test/legochargefix
546"] + n333["test/fixtures/recallpassage
314"] + n448["test/ownerhopfix/py/pkg
158"] + n280["test/expandmodefix
151"] + n282["test/expandsibsfix
149"] + n589["test/rubyschemafix
99"] + n571["test/rubyattrsfix
85"] + n424["test/massfix
77"] + end + n73["docs
836"] + n2[".github
606"] + n0["(root)
547"] + 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"] +… [20 more lines, 1599 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 1.97s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [921 more display lines; full output is 101244 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: 12.88s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [120 more display lines; full output is 14810 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).* + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +````` + + + +````` + +## `./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: 2.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [258 more display lines; full output is 17245 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* + +**wall time: 11.92s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 5.78s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [218 more display lines; full output is 28936 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=.* + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./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: 37.51s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [38564 more display lines; full output is 5687492 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** + +````` +(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: 18.79s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 4.93s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 5.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14367 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: 11.73s** + +````` + + + + + + + +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [378 more display lines; full output is 23271 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: 1.95s** + +````` +{"v":1,"verb":"plan-lanes","at":"6009ca693","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":2787,"symbols":26903,"edges":417 … [line truncated: 339 more bytes on this line] +"symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","l":2909,"id":"./src/docdrift.h::docdrift::writeDocDriftPage"}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":651,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1675,"id":null}]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/verbs_change.h","n":"runAbiCheck","scope":"","key":"183da9aaf74a4a92","id":null,"id_addr … [line truncated: 113 more bytes on this line] +{"p":"./src/gitmine.h","n":"gitWindowBoundarySha","scope":"rw","key":"18627516699d7062","id":"./src/gitmine.h::rw::gitWindowBoundarySha","id_addressable":true,"id_collides_with":0,"l":1672,"ord":0,"overloads":1,"amb":3,"cx":3,"ccx":2,"churn":71,"tested":0}, +{"p":"./src/docdrift.h","n":"writeGateability","scope":"docdrift","key":"42c456dfb55faee4","id":"./src/docdrift.h::docdrift::writeGateability","id_addressable":true,"id_collides_with":0,"l":2685,"ord":0,"overloads":1,"amb":3,"cx":6,"ccx":6,"churn":36,"tested":0}, +{"p":"./src/cli.h","n":"inHostActive","scope":"rw","key":"4446994deb74226c","id":"./src/cli.h::rw::inHostActive","id_addressable":true,"id_collides_with":0,"l":4589,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":469,"tested":0}, +{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1298,"ord":0,"overloads":1,"amb":2,"cx":8,"ccx":8,"churn":32,"tested":0}, +{"p":"./src/docdrift.h","n":"recordWeakDisclosures","scope":"docdrift","key":"972779210e49c0cf","id":"./src/docdrift.h::docdrift::recordWeakDisclosures","id_addressable":true,"id_collides_with":0,"l":2430,"ord":0,"overloads":1,"amb":2,"cx":2,"ccx":1,"churn":36,"tested":0}, +{"p":"./src/testmap.h","n":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":1344,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":48,"tested":0}, +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":3029,"ord":0,"overloads":1,"amb":33,"cx":8,"ccx":11,"churn":439,"tested":0}, +{"p":"./src/gitmine.h","n":"resolveSinceScope","scope":"rw","key":"9caaa3dbeaa688d0","id":"./src/gitmine.h::rw::resolveSinceScope","id_addressable":true,"id_collides_with":0,"l":331,"ord":0,"overloads":1,"amb":3,"cx":7,"ccx":7,"churn":71,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":689,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":439,"tested":0}, +{"p":"./src/taskroute.h","n":"testCoverageTaskChoice","scope":"rw::taskroute","key":"bf72991aa28355c7","id":"./src/taskroute.h::rw::taskroute::testCoverageTaskChoice","id_addressable":true,"id_collides_with":0,"l":1388,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":41,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5469,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":359,"tested":0}, +{"p":"./src/cli.h","n":"validateModifierGuards","scope":"rw","key":"ec2848a4801493a2","id":"./src/cli.h::rw::validateModifierGuards","id_addressable":true,"id_collides_with":0,"l":4617,"ord":0,"overloads":1,"amb":33,"cx":73,"ccx":66,"churn":469,"tested":0}], +"files":[{"p":"./src/cli.h","symbols":2,"churn":469,"ccx":66,"hotspot_rank":6}, +{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":45}, +{"p":"./src/docdrift.h","symbols":2,"churn":36,"ccx":7,"hotspot_rank":32}, +{"p":"./src/gitmine.h","symbols":2,"churn":71,"ccx":9,"hotspot_rank":17}, +{"p":"./src/main.cpp","symbols":2,"churn":439,"ccx":12,"hotspot_rank":4}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":359,"ccx":2,"hotspot_rank":5}, +{"p":"./src/taskroute.h","symbols":1,"churn":41,"ccx":2,"hotspot_rank":38}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":36}, +{"p":"./src/verbs_change.h","symbols":1,"churn":78,"ccx":9,"hotspot_rank":22}]},"blast_radius":{"reaches":47,"files_total":17,"capped":false,"files":["./src/cli.h","./src/docdrift.h","./src/editplan.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h","./src/mcpedit.h","./src/mcpserver.h","./src/mcpv … [line truncated: 194 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":13,"files":9,"module_span":1,"max_ccx":66,"sum_ccx":119,"ambiguous_calls":85,"blast_reaches":47,"contract_touches":1,"conflicts":0,"untested":47," … [line truncated: 140 more bytes on this line] +… [75 more display lines; full output is 20891 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` + +*NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* + +**wall time: 1.81s** + +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":"6009ca693","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2787,"symbols":26903,"edges":41775,"ambiguous":12805,"unresolved":12845},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] +"symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] +{"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":651,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":359,"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":3255,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":469,"tested":0}, +{"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":12,"cx":14,"ccx":15,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":13,"cx":26,"ccx":38,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1675,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":78,"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":5469,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":359,"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":469,"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":93}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":359,"ccx":2,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":56}, +{"p":"./src/verbs_change.h","symbols":1,"churn":78,"ccx":4,"hotspot_rank":22}], +"symbols_total":3231,"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":28,"blast_reaches":13,"contract_touches":1,"conflicts":1,"untested":13,"t … [line truncated: 139 more bytes on this line] +{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":121,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2878,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":469,"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":3255,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":469,"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":2957,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":469,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":121,"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":43,"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":3022,"ord":0,"overloads":1,"amb":3,"cx":4,"ccx":3,"churn":469,"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}, +… [61 more display lines; full output is 18818 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).* + +````` + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + +… [243 more display lines; full output is 25250 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [269 more display lines; full output is 26449 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [222 more display lines; full output is 25276 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:8051 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:8092 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1751 + #3 0x102e01a44 in main src/main.cpp:4169 + #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 5251 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 10197 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). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> + + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +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 +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / - … [line truncated: 48 more bytes on this line] +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] +… [61 more display lines; full output is 10488 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.71s** + +````` +(empty) +````` + +Artifact written: + +````` + 425433 /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.21s** + +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 constexpr std::array<std::string_view, 8> kCacheRuleNames = +inline std::string oracleCachePath( const std::string& root, const std::string& headSha ) +… [59 more display lines; full output is 42445 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: 3.66s** + +````` + + + + + + + + + + +````` + + +--- + +# 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: 1.39s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~7 --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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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 +1165,8090,8736,171,1423,1462,1749 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +````` + + + +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":12805,"graph_unresolved":12845,"graph_unindexed":256,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165"}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:8090"}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:8736"}, +{"t":"fn","n":"runEval","p":"src/eval.h:171"}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1423"}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1462"}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1749"}]} +````` + +## `./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: 1.89s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8173 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8099 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.16s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 8005 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: 1.90s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 8005 bytes on 1 raw line(s)] +````` + +Artifact written: + +````` + 23740310 /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 4032 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 /.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 16.98s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 0.2% 3.7% 8.4% + BM25 3.7% 5.7% 8.5% + BM25sub 9.4% 13.9% 25.9% + BM25body 6.7% 14.6% 26.6% + fused 7.7% 15.0% 24.6% + anchored 6.7% 14.6% 26.6% + same-dir 0.2% 0.8% 2.3% + random 0.2% 0.4% 0.7% <- floor (random ranking over F=2787 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: 129.75s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=6641 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.683 55.0% 84.6% 90.1% + subtoken doc-phrase 0.911 88.9% 93.3% 94.4% + name-exact name 0.893 81.5% 95.5% 97.0% + name-exact doc-phrase 0.013 0.4% 1.7% 2.7% + anchored name 0.689 56.6% 83.8% 88.4% + anchored doc-phrase 0.907 88.3% 93.3% 94.0% + routed name 0.895 81.8% 95.7% 97.2% + routed doc-phrase 0.910 88.9% 93.0% 94.1% + note: routing chose name-exact on 3986/4000 NAME queries (a NAME query is always identifier-shaped); + the confidence gate routes doc-phrase queries to name-exact ONLY when EVERY content word names a symbol + (or an explicit camel/snake token appears) AND every matched name is specific enough to anchor on — + a common name (many definitions, or a subtoken carried by many symbol names) declines the route — so + conceptual prose falls back to subtoken+body; routed tracks the better ranker on BOTH modes + (routed==name-exact on name, ~=subtoken+body on doc-phrase). +````` + +## `./build/ripwire . --eval-stray=/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: 5.26s** + +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.669) + 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.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=65d62fe5c) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:8051` + +*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:8051` + +*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 11947 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).* + +````` + + + + + + + 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.* + +````` + + + + + + + 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.* + +````` + + + + + + + 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.03s** + +````` + +rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) + +````` + +## `./build/ripwire . --pattern='DISCLOSE(...)'` + +*The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* + +**wall time: 1.01s** + +````` + +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( 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 +DISCLOSE( out, CMakeScan::DisclosureWhy::RootWalkFailed, "flags: cannot walk root for CMake files — cmake gates omitte +… [73 more display lines; full output is 17835 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 +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [55 more display lines; full output is 9884 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).* + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [66 more display lines; full output is 10217 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").* + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [93 more display lines; full output is 13498 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.* + +````` + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + +runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +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] +inline std::string qualityBaselinePath( const std::string& root ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BA … [line truncated: 62 more bytes on this line] +… [52 more display lines; full output is 9778 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).* + +````` + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + +runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +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] +inline std::string qualityBaselinePath( const std::string& root ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BA … [line truncated: 62 more bytes on this line] +… [52 more display lines; full output is 9733 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: 1.70s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 1.70s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [417 more display lines; full output is 44640 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 8005 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 8005 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:8051 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:8092 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1751 + #3 0x102e01a44 in main src/main.cpp:4169 + #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: 524 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.41s** + +````` + + + + +````` + +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.03s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [185 more display lines; full output is 15572 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 11182 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.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 7594 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --ensemble --limit=8` + +*The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* + +**wall time: 2.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 7393 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.07s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [510 more display lines; full output is 45864 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.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 8.91s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [135 more display lines; full output is 14384 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: 1.99s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [643 more display lines; full output is 72478 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 7764 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --legend-dict` + +*The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* + +````` +ripwire legend dictionary ripwire.dict/v1 dictv=74f30d013d02c2a2 entries=829 +: 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, +… [800 more display lines; full output is 84154 bytes on 830 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.54s** + +````` + + + + + + + + + + +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] ] +… [553 more display lines; full output is 66790 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.51s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [727 more display lines; full output is 66126 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.55s** + +````` + + + +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.51s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto done; +goto done; + +````` + +## `./build/ripwire . --lint --lint-select=cach-` + +*An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* + +**exit code: 1** — **wall time: 1.55s** + +````` +(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.57s** + +````` +(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.56s** + +````` +{"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}}, +… [11013 more display lines; full output is 1551625 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 5502 +````` + +## `./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: 2.73s** + +````` + + +

+

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

+

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

+

+ +````` + +## `./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: 1.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [787 more display lines; full output is 51999 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.37s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.53s** + +````` +(empty) +````` + +Artifact written: + +````` + 159470 /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: 3.26s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (23740310 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (49997444 bytes, rich family) +````` + +Artifact written: + +````` + 23740310 /aux/ci_index.lean.ripwirecache + 49997444 /aux/ci_index.rich.ripwirecache + 73737754 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 8005 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 8005 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. + 126155 /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** + +````` + + + + + + + +````` + +## `./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: 3.44s** + +````` + + + + + + + + + + + + + +````` + +## `./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.89s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: 6009ca693+dirty + [1] blast radius: 303 symbols across 72 files transitively depend on these changes (showing 8 of 72 files — shown=8 total=72 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=72) + counts_floor=1 graph_ambiguous=12808 graph_unresolved=12845 graph_unindexed=256 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missin … [line truncated: 46 more bytes on this line] + src/mcpverbs.h (34 dependent symbols) + src/slice.h (30 dependent symbols) + src/crossref.h (13 dependent symbols) + src/verbs_for.h (12 dependent symbols) + src/quality.h (11 dependent symbols) + src/verbs_navigate.h (10 dependent symbols) + src/resolve.h (9 dependent symbols) + src/serialize.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=746 — 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="4365": + (none — 4365 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.85s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 20.42s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [129 more display lines; full output is 16690 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: cache : over its 2048 MiB budget — evicted 266 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) +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: 10.79s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [129 more display lines; full output is 21568 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: 10.84s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":147,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"6009ca693+dirty","renames":60,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] +{"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"duplication","members":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","tokens":59,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"nesting","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":6,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"new-clone-of-reused-helper","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","was":0,"now":4,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"params","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","was":0,"now":8,"p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"verbosity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":8,"now":47,"p":"src/infra/sortutil.h:49","sev":"minor"}], +"sa":[{"kind":"api-surface","key":"085e3d408c2c4a35","why":"target-gone"}, +{"kind":"api-surface","key":"155d74d341a49f3c","why":"target-gone"}, +{"kind":"api-surface","key":"1a15386c2d1e47af","why":"target-gone"}, +{"kind":"api-surface","key":"298e798c7f075715","why":"target-gone"}, +{"kind":"api-surface","key":"56acdf9b5c314a14","why":"target-gone"}, +{"kind":"api-surface","key":"5a07390012b46e06","why":"target-gone"}, +{"kind":"api-surface","key":"6eef859578c8c376","why":"target-gone"}, +{"kind":"api-surface","key":"6f2394762f82a855","why":"target-gone"}, +{"kind":"api-surface","key":"7f2c3eefdf6e512e","why":"target-gone"}, +{"kind":"api-surface","key":"802e513731103806","why":"target-gone"}, +{"kind":"api-surface","key":"b6a24afef32a68a8","why":"target-gone"}, +{"kind":"api-surface","key":"c923e661c197b265","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1926b0d9e94541a0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1d3814ba687a4aef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1db76028879244ef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"36e39c7ab0fc1686","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"4bce64bd920de0c3","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"61f0e361ef7dee27","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"995375dfa4e63104","why":"target-gone"}, +… [126 more display lines; full output is 15341 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.15s** + +````` +(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, 217 re-derived, 16 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1315 ineligible (no cu … [line truncated: 200 more bytes on this line] +ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written +````` + +## `./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.07s** + +````` +(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, 217 re-derived, 16 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1315 ineligible (no cu … [line truncated: 200 more bytes on this line] +ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already acked) → ./.ripwire_quality_acks +````` + +## `./build/ripwire . --quality-delta` + +*Re-run after the partial ack: acked=3, the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 10.84s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [128 more display lines; full output is 16393 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.47s** + +````` + + + + + + +````` + +## `./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.46s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 3.97s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [89 more display lines; full output is 8590 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*The map re-ranked with a teleport toward the changed file (changed=1 here, not 0).* + +**wall time: 2.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 1.95s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [306 more display lines; full output is 16116 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zz-orphan` + +*CHANGED: a ref with NO merge base with HEAD now reports v="unknown" ok="0" in its own bucket — the absence of an answer, never a claim it is merged. (The sandbox carries a deliberately parentless branch built with `git commit-tree`; a shallow CI clone puts every ref here.)* + +**wall time: 1.61s** + +````` + + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1257, 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: 2.59s** + +````` + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1257, 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.53s** + +````` + + +

+

--callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +**wall time: 1.19s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 7807 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 7733 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.* + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +**wall time: 1.69s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7630 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: 1.66s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7630 bytes on 1 raw line(s)] +````` + +Artifact written: + +````` + 24282880 /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 4114 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: 2.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 15.95s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 1.6% 4.9% 14.7% + BM25 5.5% 8.2% 13.6% + BM25sub 12.8% 18.2% 28.0% + BM25body 13.8% 27.9% 43.5% + fused 9.1% 19.3% 29.4% + anchored 13.8% 27.9% 43.5% + same-dir 0.6% 3.0% 8.3% + random 0.2% 0.4% 0.7% <- floor (random ranking over F=2849 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: 125.13s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=6789 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.679 54.5% 84.5% 89.9% + subtoken doc-phrase 0.912 89.1% 93.3% 94.4% + name-exact name 0.894 81.8% 95.6% 97.0% + name-exact doc-phrase 0.013 0.6% 1.7% 2.7% + anchored name 0.683 55.8% 83.6% 88.4% + anchored doc-phrase 0.908 88.4% 93.3% 94.0% + routed name 0.896 82.0% 95.8% 97.3% + routed doc-phrase 0.911 89.0% 93.1% 94.1% + note: routing chose name-exact on 3986/4000 NAME queries (a NAME query is always identifier-shaped); + the confidence gate routes doc-phrase queries to name-exact ONLY when EVERY content word names a symbol + (or an explicit camel/snake token appears) AND every matched name is specific enough to anchor on — + a common name (many definitions, or a subtoken carried by many symbol names) declines the route — so + conceptual prose falls back to subtoken+body; routed tracks the better ranker on BOTH modes + (routed==name-exact on name, ~=subtoken+body on doc-phrase). +````` + +## `./build/ripwire . --eval-stray=/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: 3.60s** + +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.669) + 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=50dfd49c3) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:9117` + +*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:9117` + +*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 13183 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).* + +````` + + + + + + + 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.* + +````` + + + + + + + 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.* + +````` + + + + + + + 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.04s** + +````` + +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.03s** + +````` + +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 17832 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 +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [53 more display lines; full output is 9905 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).* + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [64 more display lines; full output is 10238 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").* + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [94 more display lines; full output is 13452 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.* + +````` + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + +runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +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] +inline std::string qualityBaselinePath( const std::string& root ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BA … [line truncated: 62 more bytes on this line] +… [52 more display lines; full output is 9679 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).* + +````` + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + +runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +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] +inline std::string qualityBaselinePath( const std::string& root ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BA … [line truncated: 62 more bytes on this line] +… [53 more display lines; full output is 9878 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: 1.37s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 1.28s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --skipped` + +*WHY a file is not in the index (oversize / excluded / unsupported-ext / gitignored) and which indexed files it cannot vouch for (degraded-parse, minified-suspect), plus the per-language census.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [418 more display lines; full output is 44751 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 7630 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 7630 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: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 + #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.40s** + +````` + + + + +````` + +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: 3.72s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [186 more display lines; full output is 15613 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 11191 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.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 7593 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --ensemble --limit=8` + +*The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* + +**wall time: 1.90s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 7406 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.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [518 more display lines; full output is 46544 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.07s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 8.17s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [136 more display lines; full output is 14504 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.05s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [642 more display lines; full output is 72598 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 7764 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --legend-dict` + +*The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* + +````` +ripwire legend dictionary ripwire.dict/v1 dictv=80b7e589253a8e76 entries=855 +: 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, +… [826 more display lines; full output is 87192 bytes on 856 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.60s** + +````` + + + + + + + + + + +vector<std::vector<double>> +nodes[ perm[i] ] +nodes[ perm[ i + 1 ] ] +p = p->next +map +byName[ symN[i] ] +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +vector<std::vector<double>> +m[ in.keys[ i ] ] +map +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +map +m[ g_pool[ g_keyOf[i] ] ] +g_pool[ g_readOf[i] ] +g_pool[ g_readOf[i] ] +m[ g_pool[i] ] +… [599 more display lines; full output is 72017 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.55s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [728 more display lines; full output is 66165 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.60s** + +````` + + + +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.61s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto done; +goto done; + +````` + +## `./build/ripwire . --lint --lint-select=cach-` + +*An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* + +**exit code: 1** — **wall time: 1.59s** + +````` +(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.58s** + +````` +(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.62s** + +````` +{"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}}, +… [11167 more display lines; full output is 1572856 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 5579 +````` + +## `./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: 2.71s** + +````` + + +

+

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

+

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

+

+ +````` + +## `./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: 1.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [803 more display lines; full output is 53087 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.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.17s** + +````` +(empty) +````` + +Artifact written: + +````` + 156663 /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: 3.33s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (24282880 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (50950373 bytes, rich family) +````` + +Artifact written: + +````` + 24282880 /aux/ci_index.lean.ripwirecache + 50950373 /aux/ci_index.rich.ripwirecache + 75233253 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 7630 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 7630 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. + 114603 /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** + +````` + + + + + + + +````` + +## `./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: 3.36s** + +````` + + + + + + + + + + + + + +````` + +## `./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.15s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: 50dfd49c3+dirty + [1] blast radius: 308 symbols across 72 files transitively depend on these changes (showing 8 of 72 files — shown=8 total=72 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=72) + counts_floor=1 graph_ambiguous=2931 graph_unresolved=13056 graph_unindexed=256 (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/crossref.h (14 dependent symbols) + src/verbs_for.h (12 dependent symbols) + src/quality.h (11 dependent symbols) + src/editcheck.h (9 dependent symbols) + src/resolve.h (9 dependent symbols) + src/serialize.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=748 — 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="4487": + (none — 4487 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.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 19.61s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [129 more display lines; full output is 16827 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: 9.89s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [129 more display lines; full output is 21819 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: 10.08s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":147,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"50dfd49c3+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":"36e39c7ab0fc1686","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"4bce64bd920de0c3","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"61f0e361ef7dee27","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"995375dfa4e63104","why":"target-gone"}, +… [126 more display lines; full output is 15369 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: 6.24s** + +````` +(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, 216 re-derived, 17 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: 6.13s** + +````` +(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, 217 re-derived, 17 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: 9.97s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [128 more display lines; full output is 16530 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.27s** + +````` + + + + + + +````` + +## `./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.26s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 3.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [89 more display lines; full output is 8594 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.94s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 1.90s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [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.38s** + +````` + + + + + +````` + +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: 2.16s** + +````` + + + + +````` + +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.40s** + +````` + + +

+

+ +````` + +## `./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: 9.92s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [131 more display lines; full output is 16797 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.19s** + +````` +(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.63s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-11 keep this branch-free — it sits inside the PageRank sort comparator 50dfd49c34e19922bfb4958127487f061c61709a integration/train-27 +````` + +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-11 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 50dfd49c34e19922bfb4958127487f061c61709a integration/train-27 +````` + +## `./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.09s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] +"graph_ambiguous":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"}, +{"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"} +````` + +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.39s** + +````` +{"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.37s** + +````` +{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], +"graph_ambiguous":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"}, +{"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"} +````` + +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.39s** + +````` +(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.38s** + +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":2931,"graph_unresolved":13056,"graph_unindexed":256,"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: 3.06s** + +````` +{"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] +{"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}}]} +````` + +## `./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.03s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [125 more display lines; full output is 15894 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: 6.27s** + +````` +(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: 9.11s** + +````` +(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 26590 per-symbol rows, 27443 indexed symbols total) +````` + +Artifact written: + +````` + 4509729 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer e5db0955eb66fde6a2ea6cfa40ae10f98ca534f8d16f4f725b30137600a13f52 +head 50dfd49c34e19922bfb4958127487f061c61709a +dirty 1 +absorbed 2 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699bdb 0 +ccx 7d8862be4e +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 8.95s** + +````` + + +````` + + +--- + +# the MCP dialect — the same verbs over stdio JSON-RPC (one-shot exchange, not a persistent server) + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp` + +*initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 945 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). Hits carry text; enclosing rows a fetch_body handle. pattern = the literal; in=any lifts the span tiering (default: tightest non-empty tier; rest rides as suppressed_co … [line truncated: 837 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional ceiling (the CLI --for --token-budget). The header carries route= (which ranker answere … [line truncated: 1053 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 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: 2117 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 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= 47417 (~tokens at 4 bytes/token: 11854 ) +explore desc_bytes= 1322 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= 829 schema_bytes= 559 required=['trace'] +find_symbol desc_bytes= 752 schema_bytes= 590 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +rank_by desc_bytes= 758 schema_bytes= 481 required=[] +for desc_bytes= 445 schema_bytes= 777 required=['task'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symbol'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +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: 2117 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The core manifest, summarised (name / description bytes / required args): + +````` +tools= 8 manifest_bytes= 14166 (~tokens at 4 bytes/token: 3541 ) +explore desc_bytes= 1322 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= 829 schema_bytes= 559 required=['trace'] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +quality_delta desc_bytes= 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.18s** + +````` +{"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: 6527 more bytes on this line] +"_index":"[index: files=2849 symbols=27441 hash=5ae537ea]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":""; +// lane lego-transitive: the full dialect's clause for the deeper implementor rows, emitted as its own comment exactly when the +// targeted interface HAS a depth >= 2 implementor (the kImplementorsFloorLegendComment route above). No double hyphen inside: +// it rides an XML comment. CLI and MCP share it. +inline constexpr const char* kLegoClosureLegendComment = + ""; + +// lane lego-transitive fix 1: the hedge's own clause, present-only beside the one above (no double hyphen: an XML comment). +inline constexpr const char* kLegoClosureHedgeLegendComment = + ""; + +// lane lego-transitive fix 2: the stop below a DIRECT row whose own base is unproven, present-only beside the two above +// (rule 5: the walk keeps the stop, the answer says so). No double hyphen: an XML comment. +inline constexpr const char* kLegoClosureUnwalkedLegendComment = + ""; + +// the clauses the targeted answer's deeper rows need: none without a deeper row or a left-out one, the hedge clause only +// when a hedged row or a left-out count rides, the unwalked clause only when a direct row was not walked +inline std::string legoClosureLegendComment( const std::vector>& graphImplementors, + const std::vector>& implementorBound, NodeId focus ) +{ + const LegoClosure closure = legoImplementorClosure( graphImplementors, implementorBound, focus ); + bool deeper = false; + bool hedged = closure.otherBase > 0; + for( const ImplementorRow& row : closure.rows ) + { + deeper = deeper || row.depth >= 2; + hedged = hedged || row.byName; + } + return std::string( deeper ? kLegoClosureLegendComment : "" ) + ( hedged ? kLegoClosureHedgeLegendComment : "" ) + + ( closure.unwalked.empty() ? "" : kLegoClosureUnwalkedLegendComment ); +} + inline const char* implementorsFloorLegendComment( const IngestResult& ing, const std::vector>& graphImplementors, NodeId focus ) { const NodeId one[1] = { focus }; diff --git a/src/crossref.h b/src/crossref.h index ad0d29b46..aa87c483b 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -103,6 +103,7 @@ #include "filter.h" // §P11.5: rw::pathTierOf — the shared source/test/doc ORDERING tier #include "infra/Diagnostics.h" // ASSUME / DISCLOSE #include "infra/sortutil.h" // svLess — the rename probe orders names in byte order, never via string_view operator< +#include "ingest.h" // definitionLinesInBlobs — the tags path over another ref's blob (labelBranchRowsByParse) #include "btree.hpp" // gtl::btree_map — sorted iteration (house rule: never std::map) @@ -112,7 +113,10 @@ #include #include #include // the worktree overlay: symlink_status / read_symlink / file_size on a changed path +#include // blobsUnreadCause — the one refusal sentence #include +#include // parseCatFileHeader: the size field never overflows +#include // labelBranchRowsByParse holds quality::headSnapshotIngestMutex around the blob parse #include #include #include @@ -142,6 +146,30 @@ constexpr std::uint32_t kMaxRefs = 512; // refusal bound — a swee // bury it under thousands of rows. What is dropped is always COUNTED in a element, never silently. constexpr std::size_t kStrayFilesPerRef = 12; constexpr std::size_t kWhereisHits = 60; +// --whereis: a row's t= is a PREVIEW of a LONG source line: a line over kWhereisTextWholeMaxBytes bytes prints its first +// bytes, cut on a UTF-8 boundary, plus an ellipsis (truncateUtf8WithEllipsis) — kWhereisTextPreviewCapBytes at most, the +// ellipsis included — and the row says the whole line's size (t_bytes=); any shorter line prints whole. Picked from the data: +// over 978 t= values of the 26c command sweep (9 repos, every whereis listing) the median line is 54 B and the 95th +// percentile 108 B, so the cap binds only on the tail; on ripwire's own --whereis=lego 38 of 60 rows were 206 B - 7.3 KB +// (a Python dict, markdown paragraphs, a legend literal) and held 121 KB of the 129 KB answer. The 100-byte gap between the +// two (the MIN DROP) keeps a cut worth its disclosure: a 202-byte line cut to 200 saved 2 bytes and its page paid ~330 B of t_bytes=, and their +// readings (ripwire's --whereis=ripwire, 16,084 -> 16,415 B), so a cut always drops at least 100 bytes of the line. Even so a +// page whose only long lines sit just past the threshold can pay more for its disclosure than its cuts save (review C2: one +// 395-byte line, +158 B), so a page prints its previews only when that page is SHORTER than the same page whole +// (writeWhereisPage measures both). --whereis-full-text (MCP full_text) prints every line whole and lifts nothing else: the +// element names that replay of the same page; --detail lifts it with the row cap and the parse guard. +constexpr std::size_t kWhereisTextPreviewCapBytes = 200; // the printed preview, the ellipsis included +constexpr std::size_t kWhereisTextWholeMaxBytes = 300; // a line up to this prints whole, so a cut drops 100+ bytes +constexpr std::size_t kUtf8EllipsisBytes = 3; // U+2026, as truncateUtf8WithEllipsis appends it +static_assert( kWhereisTextPreviewCapBytes > kUtf8EllipsisBytes && kWhereisTextWholeMaxBytes > kWhereisTextPreviewCapBytes, + "the preview leaves room for its ellipsis and is shorter than every line it previews" ); + +// --whereis: the RUNAWAY GUARD on the tags parse of other refs' blobs (labelBranchRowsByParse; --detail=N lifts it). Only a +// blob holding a parse-worthy row (parseWorthyLine) is parsed, one parse per distinct (blob, path): ripwire's own most-called helper +// across 162 refs needs 104 parses and 43 MB (~1.7 s at load 20 on an M2 Pro); a typical name, a handful. Hitting +// either bound is disclosed on the answer (), and the blobs it leaves confirm no definition. +constexpr std::size_t kWhereisParseMaxBlobs = 256; +constexpr std::size_t kWhereisParseMaxBytes = std::size_t( 64 ) << 20; // WHICH ROWS a --whereis answer lists (--whereis-listing=, MCP `listing`). A where-defined question is answered by // the kind="def" rows; the kind="ref" rows (8-59 per answer on the comparison-table cells, ~8 KB) supplied no gold @@ -519,7 +547,9 @@ inline void streamBlobs( const std::string& root, const std::vector return; } - const std::string listPath = quality::cacheDirLadder() + "/ripwire-crossref-" + std::to_string( os::getpid() ) + ".shas"; + static std::atomic listSeq{ 0 }; // concurrent batches (streamBlobsInSlices) each own their list file + const std::string listPath = quality::cacheDirLadder() + "/ripwire-crossref-" + std::to_string( os::getpid() ) + "-" + + std::to_string( listSeq.fetch_add( 1, std::memory_order_relaxed ) ) + ".shas"; { std::FILE* lf = std::fopen( listPath.c_str(), "wb" ); if( !lf ) @@ -544,6 +574,7 @@ inline void streamBlobs( const std::string& root, const std::vector return; } + PROFILE_SCOPE_DESCRIBE( "crossref: git cat-file --batch stream (every blob, callback included)" ); std::vector body; std::string header; for( std::size_t served = 0; served < shas.size(); ++served ) @@ -677,9 +708,15 @@ class BlobStore } facts_.emplace( sha, std::move( f ) ); ++fetched_; - } ); + }, &stats_ ); } + // K75: a batch that never started (the sha list could not be written under the cache dir, git did not spawn) or died + // before serving every blob leaves the shas it never reached ABSENT from facts_, and get() answers an absent sha with a + // shared empty record — "this side contributed no lines". A caller that then reports verdicts from it prints a measured- + // looking answer made of nothing (a branch with unmerged work reading merged), so it asks this first and refuses. + bool unread() const noexcept { return stats_.startFailed || stats_.endedEarly; } + // The facts for `sha`, or a shared empty (non-text) record for an absent side — so a caller can treat // "this path did not exist on that side" as "it contributed no lines" without a null check. const BlobFacts& get( const std::string& sha ) const @@ -697,7 +734,26 @@ class BlobStore gtl::btree_map facts_; std::vector pending_; std::size_t fetched_ = 0; + StreamBlobStats stats_; +}; + +// K75: the words every host of a blob-reading verb refuses in when the one `git cat-file --batch` behind it did not +// serve every blob. The cause is named WHERE the reader looks (stderr + exit 1, or an MCP error), never as a zero. +// ONE renderer for both blob readers (the prconverge.h one-renderer rule): `batch` names which reader's blobs were not +// read — the cross-branch sweep's (--stray-content, --plan, --abi, --eval-stray) or --whereis's — and every other word is +// shared, so the CLI and MCP hosts of both verbs name the same cause, the cache directory and TMPDIR. +enum class UnreadBlobBatch : std::uint8_t +{ + Branches, // the cross-branch sweep's batch: every ref's side of every path it compares + Trees, // --whereis's batch: the trees it searches for the name }; +inline std::string blobsUnreadCause( UnreadBlobBatch batch ) +{ + const std::string_view unread = batch == UnreadBlobBatch::Trees ? "the trees' blobs" : "the branches' blobs"; + return std::format( "could not read {} (the one `git cat-file --batch` did not start or stopped early; its sha list is written under {} - is TMPDIR " + "a writable directory?)", + unread, quality::cacheDirLadder() ); +} // ── git plumbing (read-only) ───────────────────────────────────────────────────────────────────────────── @@ -942,6 +998,48 @@ inline void parallelIndexed( std::size_t count, Body body, ParallelSweep& sweep } } +// ── one blob stream through several git processes (streamBlobsInSlices) ────────────────────────────────────── +// streamBlobs reads every blob through ONE `git cat-file --batch` pipe. On ripwire's 162 refs that is ~900 MB, and under +// load the pipe's ping-pong, not git's decompression, was the larger cost: 1.9 s for git writing to /dev/null against +// 5.9 s through `| cat` at load 28, and ~9 s of a --whereis at load 20-25 spent waiting in fread. This reads `slices` +// contiguous slices of the sha list through as many processes at once (parallelIndexed's pool). `onBlob( index, sha, +// bytes, isText )` runs on the slice's worker thread, once per served sha, so it may write only slot `index` of its own +// storage. The stats merge as one batch's would: counts add, and a slice that failed, was cut short or was never reached +// withholds every completeness claim. +constexpr std::size_t kWhereisStreamSlices = 4; + +template +inline void streamBlobsInSlices( const std::string& root, const std::vector& shas, std::size_t slices, OnBlob onBlob, + StreamBlobStats& stats ) +{ + slices = std::max( 1, std::min( slices, shas.size() ) ); + std::vector sliceStats( slices ); + ParallelSweep sweep; + parallelIndexed( slices, [ & ]( std::size_t t ) + { + const std::size_t begin = shas.size() * t / slices; + const std::size_t end = shas.size() * ( t + 1 ) / slices; + const std::vector part( shas.begin() + std::ptrdiff_t( begin ), shas.begin() + std::ptrdiff_t( end ) ); + std::size_t next = begin; // streamBlobs serves its list in order, one call per served sha + streamBlobs( root, part, [ & ]( const std::string& sha, std::string_view bytes, bool isText ) { onBlob( next++, sha, bytes, isText ); }, + &sliceStats[ t ] ); + }, sweep ); + for( std::size_t t = 0; t < slices; ++t ) + { + const StreamBlobStats& st = sliceStats[ t ]; + stats.missing += st.missing; + stats.nonBlob += st.nonBlob; + stats.oversized += st.oversized; + stats.binary += st.binary; + stats.endedEarly = stats.endedEarly || st.endedEarly; + stats.startFailed = stats.startFailed || st.startFailed; + if( !sweep.isDone( t ) ) + { + DISCLOSE( stats, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: a blob-stream slice was never read — completeness is withheld" ); + } + } +} + // The DISTINCT (a,b) object-name pairs the sweep needs diffed, and their rows. Same reasoning the BlobStore // rests on: a diff between two IMMUTABLE object names is a pure function of that pair, so asking twice can // only ever spend a process for an answer already held. It pays because the sweep genuinely repeats itself — @@ -1068,6 +1166,9 @@ struct StrayResult // unknown="0" at exit 0 reads as "no branch carries stray work" — the most reassuring possible answer, // from a sweep that never happened. ok=false, so the caller refuses in the --doc-drift/--dead-code words. bool filterMatchedNothing = false; + // K75: the blob batch behind every verdict never started or died part-way (an unwritable cache dir, TMPDIR naming a missing + // directory, git failing): each ref's lines would read as absent and its verdict as merged. ok=false; the caller refuses. + bool blobsUnread = false; std::string filter; // H14/M6: the --stray-content=SUBSTR this sweep was narrowed by ("" = none) std::string headSha; std::string headRef; @@ -1359,6 +1460,12 @@ inline StrayResult computeStrayContent( const std::string& root, std::string_vie BlobStore blobs( root ); registerSweepBlobs( diffs, plumbing, blobs ); + if( blobs.unread() ) + { + result.ok = false; // refused, not answered: the caller names blobsUnreadCause() on stderr / as an MCP error + result.blobsUnread = true; + return result; + } // ── phase 5: the analysis proper — pure, serial, deterministic ────────────────────────────────────── // A ref whose work is entirely on the live line is the boring, common case — in a 30-branch sweep it is @@ -1391,10 +1498,20 @@ struct WhereHit std::string date; std::string path; std::uint32_t line = 0; - bool isDef = false; // LEXICAL definition heuristic — see definitionShaped() + bool isDef = false; // kind="def": HEAD's from the index (or, head_labels="lexical", definitionShaped); another + // ref's from the tags parse of its blob (labelBranchRowsByParse) std::string text; // the trimmed source line (evidence, so the caller can judge) bool fromWorktree = false; // read from the WORKING-TREE copy of a path that differs from HEAD (ref="worktree") bool testLocal = false; // a TEST-LOCAL definition, demoted below the production ones (test_local="1") + bool unconfirmed = false; // kind="text": another ref's PARSE-WORTHY row whose blob was not parsed (the + // runaway guard, or a parse that could not finish) — never kind="def", never a ref + bool parseWorthy = false; // parseWorthyLine(): this line makes its blob worth a tags parse (another ref's rows) + // foldSharedBranchDefs: a kind="def" row outside the checkout whose (path, line text) other refs hold too is ONE + // definition held by N refs. The first row of such a group (lowest ref name) is its representative and carries + // defRefs = N (1 = this ref alone); every later row of the group is foldedDef and the definitions page skips it + // (listing=all still prints it). 0 = not a branch definition row. + std::uint32_t defRefs = 0; + bool foldedDef = false; // The CHECKOUT is HEAD's tree overlaid with the working copy of every path that differs from it: those rows // sort, label and count as one group, ahead of the other refs. Keyed on the flag, never on the ref name, so a @@ -1450,6 +1567,18 @@ inline std::vector whereisIndexDefSites( const IngestResult& ing, // Unlisted git could not list what differs; every HEAD row may be stale — complete= is withheld enum class WorktreeOverlay : std::uint8_t { Clean, Read, Partial, Unlisted }; +// How the OTHER refs' rows got their kind= (labelBranchRowsByParse), counted per distinct (blob, path) — the unit one +// parse answers. `leftByGuard` and `failed` are the pairs that confirm nothing: their parse-worthy rows are the +// `textRows` (kind="text"), and the page discloses all three in one element. +struct BranchLabelCensus +{ + std::size_t parsed = 0; // parsed by the tags path (definitionLinesInBlobs) + std::size_t mirrored = 0; // HEAD's own blob at the same path: HEAD's index labels, line for line + std::size_t leftByGuard = 0; // parse-worthy, but past kWhereisParseMaxBlobs / kWhereisParseMaxBytes + std::size_t failed = 0; // parse-worthy, but the parser could not finish (BlobDefsStatus::Failed) + std::size_t textRows = 0; // rows of those two that read kind="text" +}; + struct WhereResult { bool ok = true; @@ -1458,6 +1587,7 @@ struct WhereResult std::string headSha; bool onHead = false; // SYM occurs in HEAD's tree bool headLabelsFromIndex = false; // §A7: HEAD rows' kind= came from the index, not the shape test + BranchLabelCensus branchLabels; // how the OTHER refs' rows got kind= (labelBranchRowsByParse; on the page) std::size_t refsScanned = 0; std::size_t distinctBlobs = 0; std::string filter; // H14/M6: the --stray-filter SUBSTR this scan was narrowed by ("" = none) @@ -1466,6 +1596,7 @@ struct WhereResult bool headHolds = false; // HEAD's COMMITTED tree holds SYM (on-head= reads the CHECKOUT on a dirty tree) std::string dottedRetry; // a Class.method spelling whose method the INDEX defines: the bare name (caller-set) std::string renamedTo; // the working tree renamed SYM to this definition (caller-set, on a zero) + bool textWhole = false; // every t= prints whole (caller-set: --detail, --whereis-full-text, MCP full_text) // T1 (completeness claims): true iff the scan PROVABLY covered every text blob of every scanned ref's // full tree — no missing/oversized/short-read blob, the batch served every sha, and no ref's tree @@ -1488,6 +1619,10 @@ struct WhereResult // what tells the reader which of the two zeros they are holding. std::string seedSpec; std::string nearMiss; + // The blob stream behind hits= never started or ended before serving every blob (an unwritable batch list, a git + // that did not start or died): no row count, zero included, is a measurement then, so both hosts REFUSE the answer + // (exit 1 / an MCP error naming the cause) instead of printing hits= — the honesty bug of cli-floor §4. + bool blobsUnread = false; // The --with-history lane: a non-owning view of the caller's oracle index (nullptr ⇒ not asked for) plus // THIS symbol's verdict, resolved once at compute time so emission stays a pure print. Views at the seam: @@ -1591,12 +1726,21 @@ inline bool insideArgumentList( std::string_view line, std::size_t at ) noexcept return depth > 0; } -// Is this line SHAPED like a definition of `sym`? Declarative marker table over the languages ripwire indexes -// — deliberately LEXICAL, not AST-parsed: this is the REF-BLOB side, raw text that was never ingested, and -// paying a tree-sitter parse per candidate blob to sharpen a hint would cost more than the hint is worth. HEAD -// rows do NOT come here at all when the caller supplies the index's def sites (see relabelHeadHitsFromIndex): -// the honest split documented in --help is HEAD = parsed, refs = lexical, and this function is the lexical half. -// False positives here cost a `kind="def"` that should have read `kind="ref"`, never a missed hit. +// Is this line SHAPED like a definition of `sym`? A declarative marker table over the languages ripwire indexes, +// read LEXICALLY over raw text. It labels NO row on another ref: there parseWorthyLine, a looser superset of it, only +// CHOOSES which blobs the tags path parses (labelBranchRowsByParse — a blob with no such row is not parsed, and its rows +// read kind="ref"). Its one +// remaining LABELLING role is HEAD's fallback, head_labels="lexical" (no index def of the name, or a working tree that +// drifted from HEAD). It errs in BOTH directions, and each costs something different in each role: +// * a quoted signature in a doc, a call on a line ending in `}` or `)` (`if( x ) { f( a ); }`), `go f(a)`, `@f(1)`, +// `f( a, [&]( int x ) {` read as definition-shaped: a false kind="def" on the HEAD fallback (as a chooser, one +// wasted parse that then labels the row a reference); +// * a definition it does not shape — a signature wrapped before its `)` or ending in `,`, a declarator followed by a +// comment, `fun f(x) = x + 1`, `export const f = (a) =>`, a `name() {` at column 0 — reads as a reference: on the +// HEAD fallback a definition missing from the definitions page (counted among the refs). parseWorthyLine takes the +// cheap ones of these back in for the chooser; the rest stay a disclosed floor there too. +// A stricter reading of the line only trades one error for the other (the round-1 prefix and tail rules lost +// `#define NAME(`, `decltype(auto) NAME(`, `@Test(…) public void NAME()` …): the parser is the fix, not the line. inline bool definitionShaped( std::string_view line, std::string_view sym, std::size_t at ) { static constexpr std::string_view kDeclMarkers[] = { @@ -1634,6 +1778,75 @@ inline bool definitionShaped( std::string_view line, std::string_view sym, std:: return false; } +// Does this line make its blob worth a tags PARSE (labelBranchRowsByParse)? A deliberately LOOSE superset of +// definitionShaped(), because the two errors cost different things here: a wrong "yes" costs one parse, which then +// labels the row a reference; a wrong "no" leaves a definition unparsed when it is the only such line of its blob, and +// that definition reads kind="ref". So the shape test's own misses that are cheap to take in are taken in: +// * a comment after the declarator (`int f( int a ) { // note`, `#define f( a ) ( a ) /* note */`): retried without it; +// * the name opening the line (`f() {` in a shell script, a GNU-style C signature `f (int a)` under its return type); +// * the name bound by `=` to a function, an arrow or a lambda (`export const f = (a) =>`, `f = function (`, `f = lambda`); +// * Kotlin's `fun ` marker (`fun f(x) = x + 1`); +// * a markdown heading that opens with the name (`## f`): the index makes a section symbol of it. +// What is still missed is named in definitionShaped's header (a signature wrapped before its parameter list closes, a +// name alone on a line with only a type above it…) and, beyond it, a definition with no parameter list and no +// declaration keyword before the name (`name: function (` in an object literal, Rust `pub const NAME`, a C# property +// `public int NAME { get; set; }`); those definitions stay counted refs when nothing else in their blob earns it a +// parse. A second floor is the parser's own, not this test's: a parsed blob is labelled as HEAD's index labels the +// same bytes, so a file its grammar cannot parse cleanly (a fragment, a file mid-edit) loses a definition or gains a +// false one exactly as HEAD's index would (the review's siblings probe: 20 definitions lost, one call read as a +// definition, each matching HEAD's index for the same bytes). +// `name = (…`, `name = function`, `name = async`, `name = lambda`: the name bound by `=` to a function, an arrow or a lambda. +inline bool boundToFunctionAfter( std::string_view line, std::size_t after ) noexcept +{ + std::size_t k = after; + while( k < line.size() && std::isspace( (unsigned char)line[ k ] ) ) + { + ++k; + } + if( k + 1 >= line.size() || line[ k ] != '=' || line[ k + 1 ] == '=' || line[ k + 1 ] == '>' ) + { + return false; + } + std::size_t v = k + 1; + while( v < line.size() && std::isspace( (unsigned char)line[ v ] ) ) + { + ++v; + } + const std::string_view rhs = line.substr( v ); + return rhs.starts_with( "(" ) || rhs.starts_with( "function" ) || rhs.starts_with( "async" ) || rhs.starts_with( "lambda" ); +} + +// `#`s, then spaces, then the name at `at`: a markdown heading that opens with it. +inline bool headingOpensWith( std::string_view line, std::size_t at ) noexcept +{ + const std::size_t hashes = line.find_first_not_of( " \t" ); + std::size_t h = hashes; + while( h < at && line[ h ] == '#' ) + { + ++h; + } + return hashes != std::string_view::npos && h > hashes && h < at && line.find_first_not_of( " \t", h ) == at; +} + +inline bool parseWorthyLine( std::string_view line, std::string_view sym, std::size_t at ) +{ + if( at == 0 || definitionShaped( line, sym, at ) ) + { + return true; + } + const std::size_t after = at + sym.size(); + for( const std::string_view opener : { std::string_view( "//" ), std::string_view( "/*" ), std::string_view( " #" ) } ) + { + const std::size_t c = line.find( opener, after ); + if( c != std::string_view::npos && definitionShaped( line.substr( 0, c ), sym, at ) ) + { + return true; // the shape, once the comment after the declarator is set aside + } + } + const std::size_t fun = line.find( "fun " ); + return boundToFunctionAfter( line, after ) || ( fun != std::string_view::npos && fun < at ) || headingOpensWith( line, at ); +} + // Record every whole-word occurrence of `sym` in one blob's bytes, as (line number, trimmed line). // One line's worth of the scan, split out so the line WALK below can stay a plain segmentation with no // duplicated body — the duplication is what let the final-line case drift out of sync in the first place. @@ -1655,8 +1868,9 @@ inline void scanLineForSymbol( std::string_view line, std::string_view sym, cons { --b; } - out.push_back( WhereHit{ ref.name, ref.tip, ref.date, path, lineNo, - definitionShaped( line, sym, at ), std::string( line.substr( a, b - a ) ), false } ); + WhereHit& h = out.emplace_back( WhereHit{ ref.name, ref.tip, ref.date, path, lineNo, + definitionShaped( line, sym, at ), std::string( line.substr( a, b - a ) ), false } ); + h.parseWorthy = parseWorthyLine( line, sym, at ); return; // one hit per line — the line IS the evidence } } @@ -1667,28 +1881,38 @@ inline void scanLineForSymbol( std::string_view line, std::string_view sym, cons // perfectly legal git content, and evaluating only on '\n' silently skipped it. A symbol defined on that // final line then reported hits="0" — which this verb's own help text tells the reader means "this repo // never had the name", the single most misleading answer it can give. +// +// The walk is OCCURRENCE-driven, not line-driven: one search finds the next occurrence of `sym` anywhere in the +// blob, the newlines skipped over are counted (the line number) and only the line holding the occurrence is handed +// to scanLineForSymbol — a line without the symbol is never visited on its own. The line-by-line walk this replaces +// paid two searches per line (the newline, then the symbol) over every line of every blob that held the symbol at +// all, and on a 153-ref repo that was the larger half of a 22 s --whereis. Same hits, same order, one per line. inline void scanBlobForSymbol( std::string_view bytes, std::string_view sym, const RefInfo& ref, const std::string& path, std::vector& out ) { - std::size_t lineStart = 0; + std::size_t lineStart = 0; // the first byte of the line that holds `counted` + std::size_t counted = 0; // every newline before this offset is in lineNo std::uint32_t lineNo = 1; - while( lineStart <= bytes.size() ) + for( std::size_t pos = bytes.find( sym ); pos != std::string_view::npos; pos = bytes.find( sym, lineStart ) ) { - std::size_t end = bytes.find( '\n', lineStart ); - const bool isFinal = ( end == std::string_view::npos ); - if( isFinal ) + lineNo += std::uint32_t( std::count( bytes.begin() + std::ptrdiff_t( counted ), bytes.begin() + std::ptrdiff_t( pos ), '\n' ) ); + if( const std::size_t nl = bytes.rfind( '\n', pos ); nl != std::string_view::npos && nl >= counted ) { - end = bytes.size(); + lineStart = nl + 1; + } + std::size_t end = bytes.find( '\n', pos ); + if( end == std::string_view::npos ) + { + end = bytes.size(); // a final line with no trailing '\n' is legal git content and is scanned like any other } - scanLineForSymbol( bytes.substr( lineStart, end - lineStart ), sym, ref, path, lineNo, out ); - - if( isFinal ) + if( end >= bytes.size() ) { break; } + ++lineNo; // the line is done: one hit per line lineStart = end + 1; - ++lineNo; + counted = lineStart; } } @@ -1701,7 +1925,9 @@ inline std::vector lsTree( const std::string& root, const std::string& r DISCLOSE( "crossref: refusing to list a tree whose revision argument is not a resolved object name" ); return {}; } + PROFILE_SCOPE_DESCRIBE( "crossref/whereis: git ls-tree -r of one tip (process + read + parse)" ); const std::string raw = gitCapture( root, "ls-tree -r " + shSingleQuote( rev ) + " -- 2>/dev/null" ); + PROFILE_SCOPE_DESCRIBE( "crossref/whereis: parse one ls-tree listing" ); std::vector out; for( std::string_view line : splitLines( raw ) ) { @@ -1720,6 +1946,316 @@ inline std::vector lsTree( const std::string& root, const std::string& r return out; } +// ── every ref's full tree from content-addressed tree objects (listTreesOfRefs) ─────────────────────────── +// +// lsTree above spawns one `git ls-tree -r` per ref. Measured on ripwire's 162 local refs at load 25: that phase was +// 16.5 s of a 31 s --whereis (process start-up under load, each process re-listing subtrees most refs share). Trees are +// content-addressed, so a level-order walk reads each DISTINCT tree object ONCE — all of one level through one +// `git cat-file --batch` — and every ref's listing is rebuilt from those objects in ls-tree's own order (a tree's +// stored entry order, recursing in place) and spelling (gitQuotedPath: core.quotepath=false, as gitCapture runs it). +// Anything this walk cannot read exactly — a root that is not the top of the work tree, an object git does not +// serve, a malformed tree — returns nullopt and the caller lists with lsTree instead: the same rows, only slower. +struct GitTreeEntry +{ + std::string name; + std::string sha; + bool isTree = false; // a subtree; otherwise a blob of any mode (file, executable, symlink) — ls-tree's "blob" +}; + +// ls-tree's path spelling under core.quotepath=false: a path holding a control byte, `"` or `\` is written as a C +// string — quoted, with \a \b \t \n \v \f \r \" \\ and three-digit octal for the other control bytes and DEL — and every +// other path (bytes >= 0x80 included) is written as is (git quote.c, cq_lookup). +inline std::string gitQuotedPath( std::string_view path ) +{ + const auto needs = []( unsigned char c ) noexcept { return c < 0x20 || c == '"' || c == '\\' || c == 0x7f; }; + if( std::none_of( path.begin(), path.end(), [ & ]( char c ) { return needs( (unsigned char)c ); } ) ) + { + return std::string( path ); + } + std::string out = "\""; + for( const char ch : path ) + { + const unsigned char c = (unsigned char)ch; + static constexpr std::string_view kLetters = "abtnvfr"; // \a \b \t \n \v \f \r for bytes 7..13 + if( c >= 7 && c <= 13 ) + { + out += '\\'; + out += kLetters[ c - 7 ]; + } + else if( c == '"' || c == '\\' ) + { + out += '\\'; + out += ch; + } + else if( needs( c ) ) + { + const char oct[] = { '\\', char( '0' + ( c >> 6 ) ), char( '0' + ( ( c >> 3 ) & 7 ) ), char( '0' + ( c & 7 ) ) }; + out.append( oct, sizeof( oct ) ); + } + else + { + out += ch; + } + } + out += '"'; + return out; +} + +// Parse one tree object: ` \0` repeated. The raw id is 20 bytes for a SHA-1 repository and +// 32 for SHA-256; `idBytes` comes from the hex length of the tree's own name. False on any malformed entry (external +// input: VALIDATEd, never assumed). Gitlinks (mode 160000, a submodule's commit) are skipped, as ls-tree's blob filter does. +inline bool parseGitTree( std::string_view bytes, std::size_t idBytes, std::vector& out ) +{ + static constexpr std::string_view kHex = "0123456789abcdef"; + std::size_t pos = 0; + while( pos < bytes.size() ) + { + const std::size_t sp = bytes.find( ' ', pos ); + const std::size_t nul = sp == std::string_view::npos ? sp : bytes.find( '\0', sp + 1 ); + if( !VALIDATE( nul != std::string_view::npos && nul + 1 + idBytes <= bytes.size() ) ) // an entry cut short: not a tree we can read + { + return false; + } + const std::string_view mode = bytes.substr( pos, sp - pos ); + GitTreeEntry e; + e.name.assign( bytes.substr( sp + 1, nul - sp - 1 ) ); + e.isTree = mode == "40000"; + e.sha.reserve( idBytes * 2 ); + for( std::size_t k = 0; k < idBytes; ++k ) + { + const unsigned char b = (unsigned char)bytes[ nul + 1 + k ]; + e.sha += kHex[ b >> 4 ]; + e.sha += kHex[ b & 15 ]; + } + pos = nul + 1 + idBytes; + if( mode != "160000" ) + { + out.push_back( std::move( e ) ); + } + } + return true; +} + +// A `git cat-file --batch` header, ` `: the type and the size, or size -1 when it is not the header of +// `expected` (an object git reports missing, an empty header because the pipe ended, an answer for another object). +struct CatFileHeader +{ + std::string_view type; + long long size = -1; +}; +inline CatFileHeader parseCatFileHeader( std::string_view header, std::string_view expected ) noexcept +{ + const std::size_t sp2 = header.rfind( ' ' ); + const std::size_t sp1 = ( sp2 == std::string_view::npos || sp2 == 0 ) ? std::string_view::npos : header.rfind( ' ', sp2 - 1 ); + if( sp1 == std::string_view::npos || header.substr( 0, sp1 ) != expected ) + { + return {}; + } + long long size = 0; + for( std::size_t k = sp2 + 1; k < header.size(); ++k ) + { + if( header[ k ] < '0' || header[ k ] > '9' || size > ( std::numeric_limits::max() - 9 ) / 10 ) + { + return {}; + } + size = size * 10 + ( header[ k ] - '0' ); + } + return CatFileHeader{ header.substr( sp1 + 1, sp2 - sp1 - 1 ), sp2 + 1 < header.size() ? size : -1 }; +} + +// One `git cat-file --batch` over `shas` (in order): onObject( sha, type, bytes ) per object. False when the batch did not +// serve every object in full (a start failure, a missing object, a short read): the caller then cannot trust its walk. +template +inline bool catFileBatch( const std::string& root, const std::vector& shas, OnObject onObject ) +{ + if( shas.empty() ) + { + return true; + } + static std::atomic listSeq{ 0 }; + const std::string listPath = quality::cacheDirLadder() + "/ripwire-crossref-trees-" + std::to_string( os::getpid() ) + "-" + + std::to_string( listSeq.fetch_add( 1, std::memory_order_relaxed ) ) + ".shas"; + { + std::FILE* lf = std::fopen( listPath.c_str(), "wb" ); + if( !lf ) + { + return false; + } + for( const std::string& s : shas ) + { + rw::emitTo( lf, "{}\n", s.c_str() ); + } + std::fclose( lf ); + } + const std::string cmd = gitCmd( " -C " ) + shSingleQuote( root ) + " cat-file --batch < " + shSingleQuote( listPath ) + " 2>/dev/null"; + std::FILE* pipe = os::popen( cmd.c_str(), "r" ); + if( !pipe ) + { + os::unlink( listPath.c_str() ); + return false; + } + bool ok = true; + std::string header; + std::vector body; + for( std::size_t served = 0; served < shas.size() && ok; ++served ) + { + header.clear(); + for( int c = std::fgetc( pipe ); c != EOF && c != '\n'; c = std::fgetc( pipe ) ) + { + header.push_back( char( c ) ); + } + const CatFileHeader h = parseCatFileHeader( header, shas[ served ] ); + if( h.size < 0 ) + { + ok = false; // " missing", an empty header (the pipe ended), or an answer for another object + break; + } + body.resize( std::size_t( h.size ) ); + const std::size_t got = h.size > 0 ? std::fread( body.data(), 1, std::size_t( h.size ), pipe ) : 0; + (void)std::fgetc( pipe ); // the framing LF after the payload + ok = got == std::size_t( h.size ) && onObject( shas[ served ], h.type, std::string_view( body.data(), got ) ); + } + os::pclose( pipe ); + os::unlink( listPath.c_str() ); + return ok; +} + +// The root tree of every distinct commit in `tips` (`tree ` heads a commit object), or nullopt. +inline std::optional> rootTreesOf( const std::string& root, std::vector tips ) +{ + std::sort( tips.begin(), tips.end() ); + tips.erase( std::unique( tips.begin(), tips.end() ), tips.end() ); + gtl::btree_map rootOf; // commit → its tree + const bool read = catFileBatch( root, tips, [ & ]( const std::string& sha, std::string_view type, std::string_view bytes ) + { + const std::size_t nl = bytes.find( '\n' ); + const bool shaped = type == "commit" && bytes.starts_with( "tree " ) && nl != std::string_view::npos; + if( !VALIDATE( shaped ) ) + { + return false; + } + std::string& tree = rootOf[ sha ]; + tree.assign( bytes.substr( 5, nl - 5 ) ); + return isBlobSha( tree ); + } ); + return read ? std::optional{ std::move( rootOf ) } : std::nullopt; +} + +using GitTreeTable = gtl::btree_map>; // tree id → its entries, stored order + +// Every tree object reachable from `level` (the root trees), read a level at a time: each level's DISTINCT ids through one +// `git cat-file --batch`, a tree already read never asked for again. nullopt when a level could not be read exactly. +inline std::optional readTreeLevels( const std::string& root, std::vector level ) +{ + GitTreeTable trees; + while( !level.empty() ) + { + std::sort( level.begin(), level.end() ); + level.erase( std::unique( level.begin(), level.end() ), level.end() ); + std::vector next; + const bool read = catFileBatch( root, level, [ & ]( const std::string& sha, std::string_view type, std::string_view bytes ) + { + std::vector entries; + if( type != "tree" || !parseGitTree( bytes, sha.size() / 2, entries ) ) + { + return false; + } + for( const GitTreeEntry& e : entries ) + { + if( e.isTree ) + { + next.push_back( e.sha ); + } + } + trees.emplace( sha, std::move( entries ) ); + return true; + } ); + if( !read ) + { + return std::nullopt; + } + level.clear(); + for( std::string& id : next ) + { + if( trees.find( id ) == trees.end() ) + { + level.push_back( std::move( id ) ); + } + } + } + return trees; +} + +// One ref's `ls-tree -r` rows from the table: depth-first in stored order, a subtree listed where it stands (ls-tree -r's +// own order), each path spelled as ls-tree spells it. +inline std::vector listTreeRows( const GitTreeTable& trees, const std::string& rootTree ) +{ + struct Frame { const std::vector* entries; std::size_t next; std::size_t prefixLen; }; + std::vector out; + std::string prefix; + std::vector stack{ Frame{ &trees.at( rootTree ), 0, 0 } }; + while( !stack.empty() ) + { + Frame& f = stack.back(); + if( f.next == f.entries->size() ) + { + stack.pop_back(); + continue; + } + const GitTreeEntry& e = ( *f.entries )[ f.next++ ]; + prefix.resize( f.prefixLen ); + prefix += e.name; + if( e.isTree ) + { + const std::size_t childPrefix = prefix.size() + 1; + prefix += '/'; + stack.push_back( Frame{ &trees.at( e.sha ), 0, childPrefix } ); + continue; + } + out.push_back( RawRow{ gitQuotedPath( prefix ), std::string{}, e.sha } ); + } + return out; +} + +// Every ref's `ls-tree -r` rows, by the level-order walk above; nullopt when the walk cannot answer exactly. +inline std::optional>> listTreesOfRefs( const std::string& root, const std::vector& refs ) +{ + // ls-tree lists the CURRENT DIRECTORY's subtree, relative to it: from a root below the top of the work tree the walk + // would have to start one subtree down and re-spell every path. That case keeps lsTree. + if( !gitCapture( root, "rev-parse --show-prefix 2>/dev/null" ).empty() ) + { + return std::nullopt; + } + std::vector tips; + for( const RefInfo& r : refs ) + { + tips.push_back( r.tip ); + } + const std::optional> rootOf = rootTreesOf( root, tips ); + if( !rootOf ) + { + return std::nullopt; + } + std::vector roots; + for( const auto& [ commit, tree ] : *rootOf ) + { + (void)commit; + roots.push_back( tree ); + } + const std::optional trees = readTreeLevels( root, std::move( roots ) ); + if( !trees ) + { + return std::nullopt; + } + std::vector> out( refs.size() ); + for( std::size_t i = 0; i < refs.size(); ++i ) + { + out[ i ] = listTreeRows( *trees, rootOf->at( refs[ i ].tip ) ); + } + ENSURES( out.size() == refs.size(), "one listing per ref, in the refs' order (computeWhereis reads slot i as refs[i])" ); + return out; +} + // A git tree path ("src/graph.h") against an index path relativised to the ingest ROOT ("graph.h" when the // tree was ingested as `ripwire src`): equal, or the git path ends with the index path on a component // boundary. Never a realpath (determinism, same reasoning as arch.h::relForHash). @@ -1796,6 +2332,288 @@ inline bool relabelHeadHitsFromIndex( std::vector& hits, std::span counts them. +// A path in no indexed language, or one the crawl would skip (binary, oversized, a nesting refusal), is labelled as +// the index labels such a file on HEAD: no definition. +struct WhereisParseBudget +{ + std::size_t maxBlobs = kWhereisParseMaxBlobs; + std::size_t maxBytes = kWhereisParseMaxBytes; +}; + +struct BranchParseJob +{ + std::string sha; // the blob (with `path`, the unit one parse answers) + std::string path; // the tree path: its extension picks the grammar + std::size_t size = 0; // the blob's bytes, recorded when it holds a parse-worthy row (the guard's measure) + std::string bytes; // the blob itself, read only once the batch picked this job (labelBranchRowsByParse) + std::vector hitLines; // the blob's hit lines, ascending (one hit per line) + std::vector rows; // every WhereHit this (blob, path) produced on a non-HEAD ref + std::size_t headFirst = 0, headEnd = 0; // HEAD's rows of this same (blob, path), when HEAD holds it + bool shaped = false; // some row is parse-worthy (parseWorthyLine): worth a parse +}; + +// The hit lines a parse's definition lines promote: per definition line, the nearest hit line in its window (the lower +// line on a tie — relabelHeadHitsFromIndex walks HEAD's rows in ascending line order with the same strict `<`). +inline std::vector promotedHitLines( std::span hitLines, std::span defLines ) +{ + EXPECTS( std::is_sorted( hitLines.begin(), hitLines.end() ), "one blob's hit lines, in the scan's ascending order (the tie rule reads them so)" ); + std::vector promoted; + for( const std::uint32_t defLine : defLines ) + { + std::size_t best = hitLines.size(); + std::uint32_t bestDist = 0; + for( std::size_t i = 0; i < hitLines.size(); ++i ) + { + const std::uint32_t h = hitLines[ i ]; + if( h + kHeadDefLineAbove < defLine || h > defLine + kHeadDefLineBelow ) + { + continue; + } + const std::uint32_t dist = ( h > defLine ) ? h - defLine : defLine - h; + if( best == hitLines.size() || dist < bestDist ) { best = i; bestDist = dist; } + } + if( best != hitLines.size() ) + { + promoted.push_back( hitLines[ best ] ); + } + } + std::sort( promoted.begin(), promoted.end() ); + return promoted; +} + +// Label one job's rows: kind="def" exactly on `defLines` (sorted), else kind="ref" — or, when the job confirmed nothing +// (`confirmed` false), kind="text" on its parse-worthy rows (parseWorthyLine), the lines that asked for the parse. +inline void labelJobRows( WhereResult& result, const BranchParseJob& job, std::span defLines, bool confirmed ) +{ + EXPECTS( std::all_of( job.rows.begin(), job.rows.end(), [ & ]( std::size_t r ) { return r < result.hits.size() && !result.hits[ r ].inCheckout(); } ), + "a job's rows are rows of other refs this scan produced (HEAD's are the index's to label)" ); + for( const std::size_t r : job.rows ) + { + WhereHit& h = result.hits[ r ]; + if( !confirmed ) + { + h.unconfirmed = h.parseWorthy; + result.branchLabels.textRows += h.parseWorthy ? 1 : 0; + } + h.isDef = confirmed && std::binary_search( defLines.begin(), defLines.end(), h.line ); + } +} + +// The parse batch: the parse-worthy jobs no mirror answered, most rows first (then path, then blob — a fact of the +// answer, never of thread timing), up to the guard. Returns the jobs it parsed, in batch order. +inline std::vector branchParseBatch( const std::vector& jobs, std::span answered, + const WhereisParseBudget& budget, BranchLabelCensus& census ) +{ + std::vector want; + for( std::size_t j = 0; j < jobs.size(); ++j ) + { + if( answered[ j ] == 0 && jobs[ j ].shaped ) + { + want.push_back( j ); + } + } + std::sort( want.begin(), want.end(), [ & ]( std::size_t a, std::size_t b ) + { + const BranchParseJob& x = jobs[ a ]; + const BranchParseJob& y = jobs[ b ]; + if( x.rows.size() != y.rows.size() ) { return x.rows.size() > y.rows.size(); } + if( x.path != y.path ) { return x.path < y.path; } + return x.sha < y.sha; + } ); + std::vector batch; + std::size_t bytes = 0; + for( const std::size_t j : want ) + { + const std::size_t size = jobs[ j ].size; + if( batch.size() >= budget.maxBlobs || size > budget.maxBytes - bytes ) + { + break; // the guard: this job and every later one confirm nothing (counted below by the caller) + } + batch.push_back( j ); + bytes += size; + } + census.leftByGuard = want.size() - batch.size(); + ENSURES( batch.size() <= budget.maxBlobs && bytes <= budget.maxBytes, "the batch stays inside the runaway guard" ); + return batch; +} + +// The jobs a parse is not needed for, labelled here: HEAD's own blob at the same path (when HEAD's rows came from the index) +// takes HEAD's labels line for line; a job with no parse-worthy row, or in no indexed language, holds no definition the +// index would see. Returns which jobs are answered (1) — the rest are the parse batch's to answer. +inline std::vector answerWithoutParse( WhereResult& result, const std::vector& jobs ) +{ + std::vector answered( jobs.size(), 0 ); + for( std::size_t j = 0; j < jobs.size(); ++j ) + { + const BranchParseJob& job = jobs[ j ]; + if( job.headEnd > job.headFirst && result.headLabelsFromIndex ) + { + std::vector headDefs; // HEAD's rows of one blob, ascending by line + for( std::size_t r = job.headFirst; r < job.headEnd; ++r ) + { + if( result.hits[ r ].isDef ) { headDefs.push_back( result.hits[ r ].line ); } + } + labelJobRows( result, job, headDefs, true ); + ++result.branchLabels.mirrored; + answered[ j ] = 1; + } + else if( !job.shaped || sliceGrammarForFile( job.path ) == nullptr ) + { + labelJobRows( result, job, {}, true ); // nothing parse-worthy, or no indexed language: as the index would say + answered[ j ] = 1; + } + } + return answered; +} + +// One parsed job's labels from what definitionLinesInBlobs found in its blob. +inline void applyBlobDefinitions( WhereResult& result, const BranchParseJob& job, const BlobDefinitions& found ) +{ + switch( found.status ) + { + case BlobDefsStatus::Parsed: + labelJobRows( result, job, promotedHitLines( job.hitLines, found.lines ), true ); + ++result.branchLabels.parsed; + break; + case BlobDefsStatus::NoGrammar: + case BlobDefsStatus::Unread: + labelJobRows( result, job, {}, true ); // the crawl defines nothing in such a file on HEAD either + break; + case BlobDefsStatus::Failed: + labelJobRows( result, job, {}, false ); + ++result.branchLabels.failed; + break; + } +} + +// The bytes of the blobs the batch chose, read back from the object store: the scan kept only their sizes, so peak memory +// holds the batch (at most the guard's bytes by default; every candidate under --detail) and never more. A blob is read once however many of the batch's +// paths hold it. Returns, per batch entry, whether its bytes came back whole (an immutable object of the size the scan saw); +// one that did not — the stream died, the object went missing between the two reads — is answered as unparsed by the caller. +// The DISCLOSE sink of one batch entry's read-back (readBranchParseBatch): `whole` is what labelBranchRowsByParse reads. +struct BranchBlobReadBack +{ + enum class DisclosureWhy : std::uint8_t + { + NotReadBackWhole, // the second read served no text blob, or one of another size than the scan's + }; + bool whole = true; + void disclose( DisclosureWhy ) noexcept { whole = false; } // its one reason: the entry confirms nothing +}; + +inline std::vector readBranchParseBatch( const std::string& root, std::vector& jobs, + std::span batch ) +{ + std::vector shas; // the batch's distinct blobs, in first-use order + std::vector shaOf( batch.size() ); + for( std::size_t k = 0; k < batch.size(); ++k ) + { + const std::string& sha = jobs[ batch[ k ] ].sha; + // A linear find, so this loop and the last-use scan below are O(n^2) in the batch: n is at most the guard's 256 blobs + // by default, but every candidate under --detail (~725 on ripwire's own lego, ~0.5 M string compares, not measurable + // beside the parse; review C4). Index the shas if a --detail batch ever reaches the tens of thousands. + const auto it = std::find( shas.begin(), shas.end(), sha ); + shaOf[ k ] = std::size_t( it - shas.begin() ); + if( it == shas.end() ) { shas.push_back( sha ); } + } + std::vector bytes( shas.size() ); + std::vector served( shas.size(), 0 ); + StreamBlobStats stats; + streamBlobsInSlices( root, shas, kWhereisStreamSlices, [ & ]( std::size_t i, const std::string&, std::string_view b, bool isText ) + { + if( isText ) + { + bytes[ i ].assign( b ); + served[ i ] = 1; + } + }, stats ); + std::vector reads( batch.size() ); + for( std::size_t k = 0; k < batch.size(); ++k ) + { + // The second read is external input: a blob that came back short or not at all confirms nothing. + if( served[ shaOf[ k ] ] == 0 || bytes[ shaOf[ k ] ].size() != jobs[ batch[ k ] ].size ) + { + DISCLOSE( reads[ k ], BranchBlobReadBack::DisclosureWhy::NotReadBackWhole, + "whereis: a blob the parse batch chose did not read back whole; its rows confirm nothing (kind=text, counted in )" ); + } + } + // Hand each chosen job its bytes: the last job of a blob takes them, the others copy (a blob at two paths is rare). + for( std::size_t k = 0; k < batch.size(); ++k ) + { + if( !reads[ k ].whole ) { continue; } + const bool lastUse = std::none_of( batch.begin() + std::ptrdiff_t( k + 1 ), batch.end(), + [ & ]( std::size_t j ) { return jobs[ j ].sha == jobs[ batch[ k ] ].sha; } ); + jobs[ batch[ k ] ].bytes = lastUse ? std::move( bytes[ shaOf[ k ] ] ) : bytes[ shaOf[ k ] ]; + } + return reads; +} + +inline void labelBranchRowsByParse( const std::string& root, WhereResult& result, std::vector& jobs, const WhereisParseBudget& budget ) +{ + std::vector answered = answerWithoutParse( result, jobs ); + const std::vector batch = branchParseBatch( jobs, answered, budget, result.branchLabels ); + const std::vector reads = readBranchParseBatch( root, jobs, batch ); + std::vector blobs; + std::vector parsedJobs; // the batch entries whose bytes read back whole, in batch order + blobs.reserve( batch.size() ); + for( std::size_t k = 0; k < batch.size(); ++k ) + { + if( reads[ k ].whole ) + { + blobs.push_back( BlobText{ jobs[ batch[ k ] ].path, jobs[ batch[ k ] ].bytes } ); + parsedJobs.push_back( batch[ k ] ); + } + } + std::vector parsed; + if( !blobs.empty() ) + { + // the tags queries live in a process-global, single-writer cache: a long-lived server serializes every ingest on this lock + std::lock_guard ingestLk( quality::headSnapshotIngestMutex() ); + parsed = definitionLinesInBlobs( blobs, result.sym ); + } + ENSURES( parsed.size() == parsedJobs.size(), "one answer per (blob, path) handed to the parser" ); + for( std::size_t k = 0; k < parsedJobs.size(); ++k ) + { + applyBlobDefinitions( result, jobs[ parsedJobs[ k ] ], parsed[ k ] ); + answered[ parsedJobs[ k ] ] = 1; + std::string().swap( jobs[ parsedJobs[ k ] ].bytes ); // parsed: release the bytes before the answer is written + } + for( std::size_t k = 0; k < batch.size(); ++k ) + { + if( !reads[ k ].whole ) + { + labelJobRows( result, jobs[ batch[ k ] ], {}, false ); // chosen but not read back: confirms nothing, as a failed parse + ++result.branchLabels.failed; + answered[ batch[ k ] ] = 1; + } + } + for( std::size_t j = 0; j < jobs.size(); ++j ) + { + if( answered[ j ] == 0 ) + { + labelJobRows( result, jobs[ j ], {}, false ); // left by the guard: confirms nothing + } + } +} + // ── the working-tree overlay (2026-10-01 freshness fix) ────────────────────────────────────────────────── // // A git listing whose SUCCESS is known. gitOneLine answers "" both for "nothing to list" and for "git failed", @@ -1956,6 +2774,7 @@ struct WorktreeScan // dropped by the caller, so a deleted or renamed definition stops answering from a commit the checkout left. inline WorktreeScan scanWorktree( const std::string& root, std::string_view sym, const RefInfo& head ) { + PROFILE_SCOPE_DESCRIBE( "crossref/whereis: worktree overlay (diff + others + read changed copies)" ); WorktreeScan scan; const GitListing changed = worktreeChangedPaths( root, head.tip ); if( !changed.ok ) @@ -2194,6 +3013,69 @@ inline bool demoteTestLocalDefs( std::vector& hits ) return false; } +// The 26b sweep's definitions page: a defs="1" symbol listed 2113 "definitions", because every one of 153 refs holds +// its own copy of the same `inline … escapeXml( … )` line and each copy was a row. Identical definition lines across +// the OTHER refs — the same path, the same line text, kind="def", outside the checkout — are ONE definition held by +// N refs. In the sorted order (refs by name) the first row of such a group is its representative: it carries +// defRefs = N and the definitions page prints it once with refs="N"; every later row of the group is foldedDef and +// that page skips it. Nothing is erased: hits= still counts every row and listing=all prints each ref's own row. +// A ref that holds the same text at TWO lines of one path (a macro twin, a copy in a #if branch) is two groups — +// the k-th such line of a ref joins the k-th group — so N is exactly the number of refs holding the row, never more. +// The checkout's rows (HEAD and worktree=) are the parsed answer and never fold; nor does any kind="ref" row. A +// kind="text" row (a parse-worthy line whose blob was not parsed, see labelBranchRowsByParse) folds the same way, +// in groups of its own: the same line on N refs is one unconfirmed row with refs="N", never merged into a definition. +inline void foldSharedBranchDefs( std::vector& hits ) +{ + struct Group + { + std::string lastRef; // the ref whose rows were last seen for this (path, text) + std::size_t ordinalInRef = 0; // how many rows of that ref this key has had so far + std::vector representatives; // one per ordinal: the hit index that stands for it + }; + gtl::btree_map groups; + std::string key; + std::size_t branchDefs = 0, represented = 0; // every branch def row is one representative's count + for( std::size_t i = 0; i < hits.size(); ++i ) + { + WhereHit& h = hits[ i ]; + if( ( !h.isDef && !h.unconfirmed ) || h.inCheckout() ) + { + continue; + } + ++branchDefs; + key.assign( h.unconfirmed ? "text\n" : "def\n" ); // a confirmed and an unconfirmed copy of a line never share a group + key += h.path; + key += '\n'; // a path never holds a newline, and text is one trimmed line: the join is unambiguous + key += h.text; + Group& g = groups[ key ]; + if( g.lastRef != h.ref ) + { + g.lastRef = h.ref; + g.ordinalInRef = 0; + } + else + { + ++g.ordinalInRef; + } + if( g.ordinalInRef < g.representatives.size() ) + { + WhereHit& rep = hits[ g.representatives[ g.ordinalInRef ] ]; + ++rep.defRefs; + h.foldedDef = true; + } + else + { + g.representatives.push_back( i ); + h.defRefs = 1; + } + } + for( const WhereHit& h : hits ) + { + represented += h.foldedDef ? 0 : h.defRefs; + } + ENSURES( represented == branchDefs, "the representatives' refs= counts add up to every branch definition row: nothing is dropped, only counted" ); +} + // The emitted ORDER of --whereis rows (see computeWhereis' header): the checkout (HEAD plus its worktree rows) first // as ONE group — a definition the edit just added sorts among HEAD's definitions, not after them; the two never // share a path, so the group needs no ref order inside it — then the other refs by name; within a group SOURCE @@ -2214,8 +3096,9 @@ inline bool whereHitBefore( const WhereHit& a, const WhereHit& b ) { return at < bt; } - // Production definitions, then test-local ones (test_local="1", see demoteTestLocalDefs), then references. - const auto defRank = []( const WhereHit& h ) { return !h.isDef ? 2 : ( h.testLocal ? 1 : 0 ); }; + // Production definitions, then test-local ones (test_local="1", see demoteTestLocalDefs), then the unconfirmed + // parse-worthy rows (kind="text", labelBranchRowsByParse), then references. + const auto defRank = []( const WhereHit& h ) { return h.isDef ? ( h.testLocal ? 1 : 0 ) : ( h.unconfirmed ? 2 : 3 ); }; if( defRank( a ) != defRank( b ) ) { return defRank( a ) < defRank( b ); @@ -2231,6 +3114,206 @@ inline bool whereHitBefore( const WhereHit& a, const WhereHit& b ) return a.fromWorktree < b.fromWorktree; } +// blob sha → every (ref index, path) that points at it, in a deterministic order. `replaced`: HEAD's blob for a path the +// working tree changed — it prints no row (the overlay answers for that path), but it is still read for ONE fact the fate +// lane needs: whether HEAD's COMMITTED tree holds the name (headHolds; review M8). +struct WhereisSite +{ + std::uint32_t refIndex = 0; + std::string path; + bool replaced = false; +}; +struct WhereisSites +{ + gtl::btree_map> bySha; + bool anyEmptyTree = false; // T1: a zero-row listing could be a FAILED one — it forfeits complete= +}; + +// One ref's listed tree: its rows, and whether the listing is WHOLE (a zero-row listing could be a failed one, and a slot no +// worker reached is unlisted: either forfeits complete=, T1). +using ListedTree = std::pair&, bool>; + +// Every ref's tree listing, each DISTINCT tip listed once (HEAD and its own branch share one; so do branches left at the +// same commit), the listings run across the crossref git pool: a listing is a pure function of an immutable commit, so a +// tip is never listed twice, and the order the listings finish in reaches nothing — each lands in its tip's own slot and +// whereisSitesOf assembles the sites in ref order, exactly as a serial loop would. A listing is WHOLE when its worker +// ran and it holds a row: a zero-row listing could be a failed `git ls-tree`, and either forfeits complete= (T1). +// (cli-perf-068; train 26c: whereis-defs-fix's tree-object walk answers first, and this is its fallback.) +struct TipListings +{ + std::vector distinctTips; // sorted, unique + std::vector> rowsByTip; // parallel to distinctTips + std::vector whole; // parallel to distinctTips + + ListedTree of( const std::string& tip ) const + { + const auto at = std::lower_bound( distinctTips.begin(), distinctTips.end(), tip ); + ASSUME( at != distinctTips.end() && *at == tip, "whereis: every ref's tip was listed by listDistinctTips" ); + const std::size_t tipIndex = std::size_t( at - distinctTips.begin() ); + return { rowsByTip[ tipIndex ], whole[ tipIndex ] != 0 }; + } +}; + +inline TipListings listDistinctTips( const std::string& root, const std::vector& refs ) +{ + TipListings out; + out.distinctTips.reserve( refs.size() ); + for( const RefInfo& ref : refs ) + { + out.distinctTips.push_back( ref.tip ); + } + std::sort( out.distinctTips.begin(), out.distinctTips.end() ); + out.distinctTips.erase( std::unique( out.distinctTips.begin(), out.distinctTips.end() ), out.distinctTips.end() ); + out.rowsByTip.resize( out.distinctTips.size() ); + ParallelSweep sweep; + parallelIndexed( out.distinctTips.size(), [ & ]( std::size_t tipIndex ) { out.rowsByTip[ tipIndex ] = lsTree( root, out.distinctTips[ tipIndex ] ); }, sweep ); + out.whole.resize( out.distinctTips.size() ); + for( std::size_t tipIndex = 0; tipIndex < out.distinctTips.size(); ++tipIndex ) + { + out.whole[ tipIndex ] = sweep.isDone( tipIndex ) && !out.rowsByTip[ tipIndex ].empty() ? 1 : 0; + } + ENSURES( out.rowsByTip.size() == out.distinctTips.size() && out.whole.size() == out.distinctTips.size(), "one listing and one verdict per distinct tip" ); + return out; +} + +// Every ref's tree: from content-addressed tree objects (listTreesOfRefs), or — when that walk cannot answer exactly — +// one `git ls-tree -r` per DISTINCT tip through the stray-content sweep's pool (listDistinctTips: each worker writes only its +// own slot). The fan-out reads the trees in ref order, so the map (and the answer) is the serial one by construction. +inline WhereisSites whereisSitesOf( const std::string& root, const std::vector& refs, const std::vector& replaced ) +{ + const std::optional>> walked = listTreesOfRefs( root, refs ); // every distinct tree object read once + TipListings listings; // the fallback's; empty when the walk answered + if( !walked ) + { + DISCLOSE( Diagnostics::answerUnchanged, "lsTree lists the same rows per ref: only slower", + "crossref: the tree-object walk could not read every tree exactly — listing each distinct tip with git ls-tree" ); + listings = listDistinctTips( root, refs ); + } + WhereisSites out; + for( std::uint32_t i = 0; i < refs.size(); ++i ) + { + const auto [ rows, whole ] = walked ? ListedTree{ ( *walked )[ i ], !( *walked )[ i ].empty() } : listings.of( refs[ i ].tip ); + out.anyEmptyTree = out.anyEmptyTree || !whole; + for( const RawRow& r : rows ) + { + const bool isReplaced = i == 0 && std::binary_search( replaced.begin(), replaced.end(), r.path ); + out.bySha[ r.bSha ].push_back( WhereisSite{ i, r.path, isReplaced } ); + } + } + return out; +} + +// One blob's scan, done on the stream's own worker thread (streamBlobsInSlices): a pure function of the bytes — its hit +// rows (stamped with no ref yet) and, when some row is parse-worthy, the blob's SIZE. The bytes are not kept: holding +// every candidate's bytes until the runaway guard picked its batch cost 0.7-1.3 GB of peak RSS on hot names (the +// whereis-lean lane); the batch reads back only the blobs it chose (readBranchParseBatch). +struct WhereisBlobScan +{ + bool hasSym = false; // a text blob holding the name's bytes (the cheap reject passed) + std::vector rows; // its hits; empty ⇔ no whole-word occurrence (hasWholeWord) + std::size_t parseBytes = 0; // the blob's size, for a parse-worthy blob (the guard's measure); else 0 +}; + +inline std::vector scanWhereisBlobs( const std::string& root, const std::vector& shas, std::string_view sym, + StreamBlobStats& stats ) +{ + std::vector scans( shas.size() ); + streamBlobsInSlices( root, shas, kWhereisStreamSlices, [ & ]( std::size_t i, const std::string&, std::string_view bytes, bool isText ) + { + if( !isText || bytes.find( sym ) == std::string_view::npos ) + { + return; // cheap reject before the line walk + } + WhereisBlobScan& b = scans[ i ]; + b.hasSym = true; + scanBlobForSymbol( bytes, sym, RefInfo{}, std::string(), b.rows ); + if( std::any_of( b.rows.begin(), b.rows.end(), []( const WhereHit& h ) { return h.parseWorthy; } ) ) + { + b.parseBytes = bytes.size(); + } + }, stats ); + return scans; +} + +// HEAD's rows of one blob at one path: the index labels them (relabelHeadHitsFromIndex), and a ref holding the same +// blob at the same path takes those labels (labelBranchRowsByParse's mirror). +struct WhereisHeadRows +{ + std::string_view path; + std::size_t first = 0, end = 0; +}; + +// Rows [firstRow, end of hits) are one non-HEAD site's copy of a blob: add them to the (blob, path) job, opening it +// (with the blob's hit lines, its parse-worthiness, HEAD's rows of the same path and the size a parse would read) +// the first time that path is seen for this blob. +inline void noteBranchRows( std::vector& jobs, std::size_t firstJobOfBlob, const WhereResult& result, std::size_t firstRow, + const std::string& sha, const WhereisSite& site, std::span headRows, const WhereisBlobScan& scan ) +{ + auto job = std::find_if( jobs.begin() + std::ptrdiff_t( firstJobOfBlob ), jobs.end(), [ & ]( const BranchParseJob& j ) { return j.path == site.path; } ); + if( job == jobs.end() ) + { + BranchParseJob fresh; + fresh.sha = sha; + fresh.path = site.path; + for( std::size_t r = firstRow; r < result.hits.size(); ++r ) + { + fresh.hitLines.push_back( result.hits[ r ].line ); + fresh.shaped = fresh.shaped || result.hits[ r ].parseWorthy; // parseWorthyLine(): worth a parse + } + const auto headCopy = std::find_if( headRows.begin(), headRows.end(), [ & ]( const WhereisHeadRows& h ) { return h.path == site.path; } ); + if( headCopy != headRows.end() ) + { + fresh.headFirst = headCopy->first; + fresh.headEnd = headCopy->end; + } + if( fresh.shaped ) + { + fresh.size = scan.parseBytes; // the guard's measure; the bytes are read only if the batch picks this job + } + jobs.push_back( std::move( fresh ) ); + job = jobs.end() - 1; + } + for( std::size_t r = firstRow; r < result.hits.size(); ++r ) + { + job->rows.push_back( r ); + } +} + +// One blob's rows, stamped for every (ref, path) holding it (HEAD first: refs[0]), and the facts HEAD's sites carry. +inline void stampWhereisBlob( WhereResult& result, const std::vector& refs, const std::string& sha, const std::vector& sites, + const WhereisBlobScan& scan, std::vector& jobs ) +{ + const std::size_t firstJobOfBlob = jobs.size(); + std::vector headRows; + for( const WhereisSite& s : sites ) + { + const bool head = refs[ s.refIndex ].name == "HEAD"; // WhereHit::inCheckout's own test, so a job never holds a checkout row + result.headHolds = result.headHolds || ( head && ( !s.replaced || !scan.rows.empty() ) ); + result.onHead = result.onHead || ( head && !s.replaced ); + if( s.replaced ) + { + continue; + } + const std::size_t firstRow = result.hits.size(); + for( const WhereHit& row : scan.rows ) + { + WhereHit& h = result.hits.emplace_back( row ); + h.ref = refs[ s.refIndex ].name; + h.tip = refs[ s.refIndex ].tip; + h.date = refs[ s.refIndex ].date; + h.path = s.path; + } + if( head ) + { + headRows.push_back( WhereisHeadRows{ s.path, firstRow, result.hits.size() } ); + } + else if( firstRow < result.hits.size() ) + { + noteBranchRows( jobs, firstJobOfBlob, result, firstRow, sha, s, headRows, scan ); + } + } +} + // The whole --whereis computation. Every ref's FULL tree is enumerated, but each distinct blob is READ once: // a `(blob sha → the paths/refs that point at it)` fan-out map is built first, then one streaming pass scans // each blob's bytes and attributes its hits to every (ref, path) that shares it. That is the content-addressed @@ -2248,8 +3331,9 @@ inline bool whereHitBefore( const WhereHit& a, const WhereHit& b ) // code. Nothing is dropped and no row's attributes change; a reader who wants the doc evidence still gets // every row of it. inline WhereResult computeWhereis( const std::string& root, std::string_view sym, std::string_view filter, - WhereisEvidence evidence = {} ) + WhereisEvidence evidence = {}, const WhereisParseBudget& budget = {} ) { + PROFILE_SCOPE_DESCRIBE( "crossref/whereis: total (refs, listings, blob scan, overlay, sort)" ); WhereResult result; result.sym = std::string( sym ); result.filter = std::string( filter ); // H14/M6: echoed on the root beside refs_scanned=, which it bounds @@ -2274,50 +3358,37 @@ inline WhereResult computeWhereis( const std::string& root, std::string_view sym result.worktree = worktree.state; result.onHead = worktree.mentions; - // blob sha → every (ref index, path) that points at it, in a deterministic order. `replaced`: HEAD's blob for a - // path the working tree changed — it prints no row (the overlay answers for that path), but it is still read for - // ONE fact the fate lane needs: whether HEAD's COMMITTED tree holds the name (headHolds; review M8). - struct Site { std::uint32_t refIndex; std::string path; bool replaced; }; - bool anyEmptyTree = false; // T1: a zero-row ls-tree could be a FAILED listing — it forfeits complete= - gtl::btree_map> sites; - for( std::uint32_t i = 0; i < refs.size(); ++i ) - { - const std::vector rows = lsTree( root, refs[i].tip ); - if( rows.empty() ) - { - anyEmptyTree = true; - } - for( const RawRow& r : rows ) - { - sites[ r.bSha ].push_back( Site{ i, r.path, i == 0 && std::binary_search( worktree.replaced.begin(), worktree.replaced.end(), r.path ) } ); - } - } + const WhereisSites sites = whereisSitesOf( root, refs, worktree.replaced ); + const bool anyEmptyTree = sites.anyEmptyTree; std::vector shas; - shas.reserve( sites.size() ); - for( const auto& [ sha, s ] : sites ) { (void)s; shas.push_back( sha ); } + shas.reserve( sites.bySha.size() ); + for( const auto& [ sha, s ] : sites.bySha ) { (void)s; shas.push_back( sha ); } result.distinctBlobs = shas.size(); result.refsScanned = refs.size() - 1; // HEAD is not one of the swept refs - StreamBlobStats blobStats; // T1: the degrade census that decides whether this scan may claim complete= - streamBlobs( root, shas, [ & ]( const std::string& sha, std::string_view bytes, bool isText ) - { - if( !isText || bytes.find( sym ) == std::string_view::npos ) { return; // cheap reject before the line walk -} - const auto it = sites.find( sha ); - if( it == sites.end() ) { return; -} - for( const Site& s : it->second ) + StreamBlobStats blobStats; // T1: the degrade census that decides whether this scan may claim complete= + std::vector branchJobs; // one per distinct (blob, path) behind a non-HEAD row: labelBranchRowsByParse + { + const std::vector scans = scanWhereisBlobs( root, shas, sym, blobStats ); + for( std::size_t blobIndex = 0; blobIndex < shas.size(); ++blobIndex ) // sha order, on this thread: the serial answer { - const bool head = refs[ s.refIndex ].name == "HEAD"; - result.headHolds = result.headHolds || ( head && ( !s.replaced || hasWholeWord( bytes, sym ) ) ); - result.onHead = result.onHead || ( head && !s.replaced ); - if( !s.replaced ) + const auto it = sites.bySha.find( shas[ blobIndex ] ); + if( scans[ blobIndex ].hasSym && it != sites.bySha.end() ) { - scanBlobForSymbol( bytes, sym, refs[ s.refIndex ], s.path, result.hits ); + stampWhereisBlob( result, refs, shas[ blobIndex ], it->second, scans[ blobIndex ], branchJobs ); } - } }, &blobStats ); + } + } + // cli-perf-068: a stream that never started, or died before serving every blob, leaves hits= and on-head= unmeasured: a zero + // from it is not "no tree holds the name". The caller refuses on this rather than print that zero (CHECKLIST 13b), so nothing + // below runs for it — the parse batch would only re-read through the same cache directory for an answer nobody is shown. + result.blobsUnread = blobStats.startFailed || blobStats.endedEarly; + if( result.blobsUnread ) + { + return result; + } // T1: exhaustive-over-text iff every sha streamed clean AND no ref's tree listing was suspect. An empty // sha list (every scanned tree empty, or none) trivially streamed clean — anyEmptyTree covers that shape. @@ -2328,11 +3399,15 @@ inline WhereResult computeWhereis( const std::string& root, std::string_view sym // §A7: HEAD's rows are the INDEX's answer, not the shape test's — before the sort, because "definitions // before references" is a sort key and a wrong label re-orders the first screen. result.headLabelsFromIndex = relabelHeadHitsFromIndex( result.hits, evidence.indexDefs ); + // …and the other refs' rows are a PARSER's answer too, after HEAD's: a ref holding HEAD's own blob takes HEAD's labels. + labelBranchRowsByParse( root, result, branchJobs, budget ); demoteTestLocalDefs( result.hits ); // The checkout first, then refs by name; within a group, SOURCE before test before docs (§P11.5, see this // function's header), then definitions before references, then path/line — whereHitBefore states it. + PROFILE_SCOPE_DESCRIBE( "crossref/whereis: sort hit rows" ); std::sort( result.hits.begin(), result.hits.end(), []( const WhereHit& a, const WhereHit& b ) { return whereHitBefore( a, b ); } ); + foldSharedBranchDefs( result.hits ); // after the sort: the representative of a group is its first row in this order return result; } @@ -2359,6 +3434,7 @@ struct EvalReport std::vector badRefs; // labels naming a ref this git repo does not have at all — // refused, never scored (see evalStray below) bool ok = true; + bool blobsUnread = false; // K75: the sweep's blob batch was unread — refused, like a non-git root }; inline bool parseVerdict( std::string_view s, Verdict& out ) @@ -2388,7 +3464,7 @@ inline EvalReport evalStray( const std::string& root, const std::string& labelsP } const StrayResult res = computeStrayContent( root, {} ); - if( !res.ok ) { rep.ok = false; return rep; } + if( !res.ok ) { rep.ok = false; rep.blobsUnread = res.blobsUnread; return rep; } // Reported refs carry their verdict; every ref NOT reported was scanned and found merged (writeStrayContent // omits those), so an absent ref scores as `merged` rather than as a miss. @@ -2670,7 +3746,7 @@ inline bool whereisSpecIsFileQualified( std::string_view spec ) { return false; } - return std::all_of( ext.begin(), ext.end(), []( unsigned char c ) { return std::isalnum( c ) != 0; } ); + return std::all_of( ext.begin(), ext.end(), []( char c ) { return std::isalnum( static_cast( c ) ) != 0; } ); // explicit narrowing: an unsigned char parameter is G1's implicit sign-change on a byte >= 0x80 } // The bare-name half a caller should retype. Only meaningful when whereisSpecIsFileQualified( spec ). @@ -2735,6 +3811,16 @@ inline std::string whereisDottedRetryOf( const IngestResult& ing, std::string_vi // The legend's CONDITIONAL tail: each paragraph rides only an answer that carries what it defines, so a plain // answer pays no bytes for the with_history lane, test-local rows or the worktree overlay. Split out of // writeWhereisPage so the page writer stays a page writer. +// refs= on a hit rides only a definitions page that printed one (foldSharedBranchDefs), and so does its reading: the +// SHARED DEFINITIONS clause of the legend, spliced by writeWhereisListedPage beside the tail below. +inline constexpr char kWhereisSharedDefsLegend[] = + "SHARED DEFINITIONS: refs=\"N\" on a kind=\"def\" row outside the checkout means N scanned refs hold this same " + "definition line (the same path and the same line text; on a kind=\"text\" row, the same unparsed line, never merged " + "with a confirmed one); the row is printed once, for the first of those " + "refs by name, whose ref=, tip=, date= and l= it carries (another of the N may hold the line at a different " + "line number). It appears only under listing=\"defs\" and only when N is at least 2: a definition one ref " + "alone holds carries no refs=. hits= still counts every copy, and listing=all prints each ref's own row. "; + inline void writeWhereisLegendTail( std::FILE* out, const WhereResult& res ) { // §L10b: the with_history lane's own element, previously undefined on this legend — shared @@ -2754,6 +3840,18 @@ inline void writeWhereisLegendTail( std::FILE* out, const WhereResult& res ) "holds a production definition, and those rows are ordered after the production definitions and before " "the references of their tier. Nothing is dropped. " ); } + // The unparsed element's vocabulary (writeWhereisUnparsed), only on an answer that carries the element. No literal + // double dash inside this XML comment: the flag is spelled by name, the next= attribute carries its real spelling. + if( res.branchLabels.leftByGuard + res.branchLabels.failed > 0 ) + { + rw::emitTo( out, "UNPARSED: the unparsed element counts the blobs of other refs that hold a line worth parsing but that " + "no parse read: blobs= of them, either past the runaway guard on the parse batch ({} blobs or {} MB, filled " + "most rows first, then by path and blob) or a parse that could not finish; rows= is the kind=\"text\" rows they " + "gave. Those rows are neither a confirmed definition nor a confirmed reference, and the blobs' other rows read " + "kind=\"ref\". Its next= rides when the guard left some: the same question (the symbol and an explicitly named " + "listing) with the detail flag at 1, which lifts the guard; a ref filter (the MCP kind argument) is not carried, " + "as the CLI has no whereis ref filter. ", kWhereisParseMaxBlobs, kWhereisParseMaxBytes >> 20 ); + } // The overlay's own vocabulary, only on an answer that carries it — a clean checkout pays no bytes for it. if( res.worktree != WorktreeOverlay::Clean ) { @@ -2850,12 +3948,39 @@ struct ListedHits std::vector rows; std::string_view attr; std::size_t refsElided = 0; + std::size_t defsFolded = 0; // def rows of other refs a listed row's refs= stands for (folded="N" on the root) + WhereisListing asked = WhereisListing::ShorterOfDefsAll; // the listing the question named (whereisServedListing) + bool textPreview = false; // a long t= prints as a preview (writeWhereisPage: only when that page is shorter) }; +// kind= of a row: "def" (a parser's definition), "text" (a parse-worthy line on another ref no parse confirmed, +// labelBranchRowsByParse) or "ref". +inline const char* whereisKindOf( const WhereHit& h ) noexcept +{ + return h.isDef ? "def" : ( h.unconfirmed ? "text" : "ref" ); +} + +// The ` refs="N"` a row carries, or "": it rides ONLY the definitions page (foldSharedBranchDefs), where the row stands +// for N refs' copies of the same definition line. listing=all prints each copy as its own row and never carries it; a +// lone copy carries none. The legend's SHARED DEFINITIONS clause rides exactly the pages where this is non-empty. +inline std::string whereisSharedAttr( const ListedHits& listed, const WhereHit& h ) +{ + return ( listed.attr == "defs" && h.defRefs >= 2 ) ? ( " refs=\"" + std::to_string( h.defRefs ) + "\"" ) : std::string(); +} + +// A row the definitions page lists: a definition, or a parse-worthy line on another ref no parse read (kind="text"). +inline bool whereisDefsPageRow( const WhereHit& h ) noexcept +{ + return h.isDef || h.unconfirmed; +} + inline ListedHits listedHits( const WhereResult& res, WhereisListing listing ) { EXPECTS( listing != WhereisListing::ShorterOfDefsAll, "the default is resolved to Defs or All (whereisServedListing) before the rows are picked" ); - const std::size_t defs = std::size_t( std::count_if( res.hits.begin(), res.hits.end(), []( const WhereHit& h ) { return h.isDef; } ) ); + // The definitions page lists the kind="def" rows AND the kind="text" ones (parse-worthy lines on another ref + // that no parse confirmed or refuted, labelBranchRowsByParse): a possible definition is printed, labelled, never + // counted away among the references. listing=refs and are the kind="ref" rows exactly. + const std::size_t defs = std::size_t( std::count_if( res.hits.begin(), res.hits.end(), whereisDefsPageRow ) ); const std::size_t refs = res.hits.size() - defs; ListedHits out; // Defs with nothing to elide (no ref row) IS the whole list; Defs with no def row would list nothing but a count, @@ -2867,14 +3992,21 @@ inline ListedHits listedHits( const WhereResult& res, WhereisListing listing ) for( std::size_t i = 0; i < res.hits.size(); ++i ) { out.rows[ i ] = i; } return out; } - const bool wantDefs = lean; + // The definitions page lists each shared branch definition ONCE (foldSharedBranchDefs): a folded row's line is + // already on the page, on its representative, whose refs= counts it. The whole list (listing=all) keeps every row. + const bool wantDefs = lean; + std::size_t folded = 0; for( std::size_t i = 0; i < res.hits.size(); ++i ) { - if( res.hits[ i ].isDef == wantDefs ) { out.rows.push_back( i ); } + const WhereHit& h = res.hits[ i ]; + if( whereisDefsPageRow( h ) != wantDefs ) { continue; } + if( wantDefs && h.foldedDef ) { ++folded; continue; } + out.rows.push_back( i ); } out.attr = wantDefs ? std::string_view( "defs" ) : std::string_view( "refs" ); out.refsElided = wantDefs ? refs : 0; - ENSURES( out.rows.size() == ( wantDefs ? defs : refs ), "a listing holds exactly the rows of its kind" ); + out.defsFolded = folded; + ENSURES( out.rows.size() + folded == ( wantDefs ? defs : refs ), "a listing holds exactly the rows of its kind, a folded def on its representative" ); return out; } @@ -2963,7 +4095,15 @@ inline void writeWhereisRoot( std::FILE* out, const WhereResult& res, const Wher { anyHoist = anyHoist || whereisRowOnHeadCommit( res.hits[ listed.rows[ k ] ], res.headSha, headDate ); } - const std::string listingAttr = listed.attr.empty() ? std::string() : ( " listing=\"" + std::string( listed.attr ) + "\"" ); + // folded= (foldSharedBranchDefs): the def rows of other refs that a listed row's refs= stands for, so the root's + // arithmetic stays checkable in one place: shown + more + folded + refs count = hits. Only when a row folded, + // so an answer with no shared branch definition is byte-identical to before. + // ref_labels="parsed": the answer holds rows of OTHER refs, whose kind= a parser gave (labelBranchRowsByParse). Only + // then, so an answer with no branch row — a repo with no other ref — is byte-identical to before. + const bool anyBranchRow = std::any_of( res.hits.begin(), res.hits.end(), []( const WhereHit& h ) { return !h.inCheckout(); } ); + const std::string listingAttr = ( listed.attr.empty() ? std::string() : ( " listing=\"" + std::string( listed.attr ) + "\"" ) ) + + ( listed.defsFolded > 0 ? ( " folded=\"" + std::to_string( listed.defsFolded ) + "\"" ) : std::string() ) + + ( anyBranchRow ? std::string( " ref_labels=\"parsed\"" ) : std::string() ); const std::string headDateAttr = anyHoist ? ( " head_date=\"" + ex( headDate ) + "\"" ) : std::string(); rw::emitTo( out, "", ex( res.sym ).c_str(), res.onHead ? 1 : 0, res.refsScanned, res.distinctBlobs, res.hits.size(), @@ -2977,6 +4117,105 @@ inline void writeWhereisRoot( std::FILE* out, const WhereResult& res, const Wher } +// The --detail=1 replay of the SAME question (the ): the symbol, the listing the question named (an explicit +// one; the default stays the default) and the detail flag, which lifts every cap of this verb (the row cap, the parse guard, +// the t= preview). A ref filter (the MCP `kind`) is NOT carried, as does not +// carry it: the CLI has no whereis ref filter (--stray-content= is another verb, and naming it would run that verb instead), +// and the legend's UNPARSED clause says so. +inline std::string whereisDetailNext( const WhereResult& res, WhereisListing asked ) +{ + std::string call = nextFlag( "--whereis=", res.sym ); + switch( asked ) + { + case WhereisListing::Defs: call += " --whereis-listing=defs"; break; + case WhereisListing::Refs: call += " --whereis-listing=refs"; break; + case WhereisListing::All: call += " --whereis-listing=all"; break; + case WhereisListing::ShorterOfDefsAll: break; + } + return nextAttrXml( call + " --detail=1" ); +} + +// : the other refs' (blob, path) pairs that held a parse-worthy row and confirm +// nothing — the runaway guard left them (WhereisParseBudget) or the parser could not finish — and the rows of theirs +// that read kind="text". next= rides only when the guard left some: --detail=1 lifts it. Absent when every +// parse-worthy blob was parsed (or mirrored HEAD's), so an answer the guard never touched is byte-identical. +inline void writeWhereisUnparsed( std::FILE* out, const WhereResult& res, WhereisListing asked ) +{ + const BranchLabelCensus& c = res.branchLabels; + if( c.leftByGuard + c.failed == 0 ) + { + return; + } + const std::string next = c.leftByGuard > 0 ? whereisDetailNext( res, asked ) : std::string(); + rw::emitTo( out, "", c.leftByGuard + c.failed, c.textRows, next.c_str() ); +} + +// Whether a row prints its t= as a preview (and carries t_bytes=): this page prints previews (writeWhereisPage served the +// cut page because it is shorter than the same page whole) and the line is over kWhereisTextWholeMaxBytes, so a cut drops at +// least the 100 bytes between that and kWhereisTextPreviewCapBytes. A page that prints previews cuts EVERY such row. +inline bool whereisTextCut( const ListedHits& listed, const WhereHit& h ) noexcept +{ + if( !listed.textPreview ) + { + return false; // the page prints whole: its cut render was not shorter, or --detail / the full-text flag asked + } + return h.text.size() > kWhereisTextWholeMaxBytes; +} + +// The rows of this page whose t= is a preview (whereisTextCut): the count and the trigger of the legend's +// PREVIEW clause, so both ride exactly the pages that print a cut row and an answer of short lines is byte-identical. +inline std::size_t whereisPageCutRows( const WhereResult& res, const ListedHits& listed, const PageWindow& hitPage ) +{ + std::size_t cut = 0; + for( std::size_t k = hitPage.begin; k < hitPage.end; ++k ) + { + cut += whereisTextCut( listed, res.hits[ listed.rows[ k ] ] ) ? 1 : 0; + } + return cut; +} + +// The : THIS PAGE again with every line whole (review C1, rule 5: a cut is recovered by the page it cut, not by +// a different answer). It names the listing the page SERVED (a default page replays the listing it chose, written out), the +// page's own window (--limit / --offset when it was paged), --with-history when the fate rows rode, and --whereis-full-text, +// which lifts the preview and nothing else: the parse guard and the row cap stay, so every kind= label, the row order and the +// window are the page's own, and the replay is the same rows with whole lines (test/crossrefcheck.sh 5h, the X2 arms). +// --detail=1 is NOT this call: it lifts the guard as well, which relabels a kind="text" call site kind="ref" (it leaves the +// defs page), and it unpages a refs or all listing (25 MB for a hot name). The MCP `kind` filter is not carried +// (whereisDetailNext says why). +inline std::string whereisFullTextNext( const WhereResult& res, const WhereisPageView& v ) +{ + std::string call = nextFlag( "--whereis=", res.sym ) + " --whereis-listing="; + call += v.listed.attr.empty() ? std::string_view( "all" ) : v.listed.attr; + if( v.pageLimit > 0 ) + { + call += " --limit=" + std::to_string( v.pageLimit ); + } + if( v.pageOffset > 0 ) + { + call += " --offset=" + std::to_string( v.pageOffset ); + } + if( res.history != nullptr ) + { + call += " --with-history"; + } + return nextAttrXml( call + " --whereis-full-text" ); +} + +// : rows= rows of this page print a t= preview (each carries t_bytes=, the whole line's bytes), +// and next= replays this page with every line whole (whereisFullTextNext). Absent when no row was cut. On a page the MCP +// `kind` ref filter bounded, next= cannot carry the filter (the CLI has no whereis ref filter), so it replays OTHER rows: +// uncarried= names the dropped filter and the PREVIEW clause names the recovery (MCP whereis with full_text:true), review +// D1, rule 5 (test/crossrefcheck.sh 5h, the D1 arm). An unfiltered page carries no uncarried= and is byte-identical. +inline void writeWhereisTextCut( std::FILE* out, const WhereResult& res, const WhereisPageView& v, std::size_t cutRows, + const XmlEscaper& ex ) +{ + if( cutRows > 0 ) + { + const std::string uncarried = res.filter.empty() ? std::string() : ( " uncarried=\"" + ex( res.filter ) + "\"" ); + rw::emitTo( out, "", cutRows, whereisFullTextNext( res, v ).c_str(), uncarried.c_str() ); + } +} + // The rows of the page, its remainder and the element (writeWhereisPage). inline void writeWhereisRows( std::FILE* out, const WhereResult& res, const ListedHits& listed, const PageWindow& hitPage, const std::string& headDate, const XmlEscaper& ex ) @@ -2992,16 +4231,34 @@ inline void writeWhereisRows( std::FILE* out, const WhereResult& res, const List { const WhereHit& h = res.hits[ listed.rows[ k ] ]; ++shownCount; + // t= is a PREVIEW (kWhereisTextPreviewCapBytes) on a page that prints previews: a longer line prints its first bytes, + // cut on a UTF-8 boundary with the ellipsis, and t_bytes= says the whole line's size. A line that fits prints whole + // and carries no t_bytes=. + std::string preview; + std::string cutAttr; + std::string_view text = h.text; + if( whereisTextCut( listed, h ) ) + { + // The cap counts the ellipsis: the kept prefix is 3 bytes shorter, so a cut t= is never longer than the cap + // (nor than the line it previews, which a cut at the cap plus the ellipsis made of a 201-byte line). + preview = h.text; + truncateUtf8WithEllipsis( preview, kWhereisTextPreviewCapBytes - kUtf8EllipsisBytes ); + ENSURES( preview.size() <= kWhereisTextPreviewCapBytes && preview.size() < h.text.size(), + "a cut t= fits the cap, ellipsis included, and is shorter than the line" ); + cutAttr = " t_bytes=\"" + std::to_string( h.text.size() ) + "\""; + text = preview; + } if( whereisRowOnHeadCommit( h, res.headSha, headDate ) ) { - rw::emitTo( out, "", + rw::emitTo( out, "", ex( h.ref ).c_str(), ex( h.path ).c_str(), - h.line, h.isDef ? "def" : "ref", h.testLocal ? " test_local=\"1\"" : "", ex( h.text ).c_str() ); + h.line, whereisKindOf( h ), h.testLocal ? " test_local=\"1\"" : "", cutAttr.c_str(), ex( text ).c_str() ); continue; } - rw::emitTo( out, "", + rw::emitTo( out, "", ex( h.ref ).c_str(), h.tip.c_str(), ex( h.date ).c_str(), ex( h.path ).c_str(), - h.line, h.isDef ? "def" : "ref", h.testLocal ? " test_local=\"1\"" : "", ex( h.text ).c_str() ); + h.line, whereisKindOf( h ), h.testLocal ? " test_local=\"1\"" : "", whereisSharedAttr( listed, h ).c_str(), + cutAttr.c_str(), ex( text ).c_str() ); } ASSUME( shownCount == hitPage.end - hitPage.begin ); // = the LISTED rows AFTER this page, so shown + more == the listed rows from this page's offset @@ -3020,6 +4277,16 @@ inline void writeWhereisRows( std::FILE* out, const WhereResult& res, const List } } +// The window one --whereis page prints over its listed rows. An explicit --limit overrides maxHits (the caller's 60-hit +// display default, or SIZE_MAX under --detail); --offset skips whole rows and clamps at the end, so offset-past-the-end is +// an empty page rather than an out-of-range read. maxHits can be SIZE_MAX, which pageWindow's int limit cannot carry — +// clamp the "no explicit --limit" arm to the row count instead of overflowing it. +inline PageWindow whereisHitWindow( std::size_t listedCount, std::size_t maxHits, int pageLimit, int pageOffset ) +{ + const int rowCap = pageLimit > 0 ? pageLimit : ( maxHits >= listedCount ? int( listedCount ) : int( maxHits ) ); + return pageWindow( listedCount, rowCap, pageOffset ); +} + // Contract-level defect: this verb said hits="2560" and printed 60, and // --limit/--offset were accepted and ignored, so a paging loop over it never advanced and never ended. // `pageLimit`/`pageOffset` (0 = un-paginated) window the hit list, which is already deterministically @@ -3036,25 +4303,25 @@ inline void writeWhereisListedPage( std::FILE* out, const WhereResult& res, std: std::vector esc; const XmlEscaper ex = [ & ]( std::string_view s ) { return std::string( escapeXml( s, esc ) ); }; - // The emitted window. An explicit --limit overrides maxHits (the caller's 60-hit display default, or - // SIZE_MAX under --detail); --offset skips whole rows and clamps at the end, so offset-past-the-end is - // an empty page rather than an out-of-range read. maxHits can be SIZE_MAX, which pageWindow's int limit - // cannot carry — clamp the "no explicit --limit" arm to the row count instead of overflowing it. - const std::size_t listedCount = listed.rows.size(); - const int rowCap = pageLimit > 0 ? pageLimit - : ( maxHits >= listedCount ? int( listedCount ) : int( maxHits ) ); - const PageWindow hitPage = pageWindow( listedCount, rowCap, pageOffset ); + const PageWindow hitPage = whereisHitWindow( listed.rows.size(), maxHits, pageLimit, pageOffset ); rw::emitRaw( out, "", out ); const std::string headDate = whereisHeadDate( res ); @@ -3138,6 +4434,8 @@ inline void writeWhereisListedPage( std::FILE* out, const WhereResult& res, std: // text hit), which is a claim strong enough to override the line-removal oracle outright. So the fate // row is printed unless the index itself already proved the symbol is defined on HEAD. writeWhereisFate( out, res, ex ); + writeWhereisUnparsed( out, res, listed.asked ); + writeWhereisTextCut( out, res, WhereisPageView{ listed, hitPage, pageLimit, pageOffset }, cutRows, ex ); writeWhereisRows( out, res, listed, hitPage, headDate, ex ); rw::emitRaw( out, "" ); @@ -3157,7 +4455,9 @@ inline ListedHits whereisServedListing( const WhereResult& res, std::size_t maxH { if( listing != WhereisListing::ShorterOfDefsAll ) { - return listedHits( res, listing ); + ListedHits named = listedHits( res, listing ); + named.asked = listing; // an explicit listing rides the (writeWhereisUnparsed) + return named; } ListedHits defs = listedHits( res, WhereisListing::Defs ); if( defs.attr.empty() ) @@ -3170,11 +4470,22 @@ inline ListedHits whereisServedListing( const WhereResult& res, std::size_t maxH // lists are in hit-list order and Defs is the def subsequence, so the definitions the All page shows are a PREFIX // of the Defs page's: comparing the counts compares the sets. More definitions wins whatever its bytes; only equal // sets fall to the bytes rule below. + // Counted as DEFINITION ROWS OF THE WHOLE LIST: on the Defs page a representative row stands for defRefs rows + // (foldSharedBranchDefs), on the All page every row is its own. The prefix argument still holds: each def row + // the All page shows is either a representative (on the Defs page no later than its own def-subsequence + // position, so within the first n) or folds into one that precedes it, and every (representative, ref) pair + // is counted once in defRefs — so the Defs page's weighted count is at least the All page's. const auto defsShown = [ & ]( const ListedHits& l ) { - const std::size_t n = std::min( l.rows.size(), maxHits ); - return std::size_t( std::count_if( l.rows.begin(), l.rows.begin() + std::ptrdiff_t( n ), - [ & ]( std::size_t i ) { return res.hits[ i ].isDef; } ) ); + const std::size_t n = std::min( l.rows.size(), maxHits ); + const bool weighted = l.attr == "defs"; + std::size_t shown = 0; + for( std::size_t k = 0; k < n; ++k ) + { + const WhereHit& h = res.hits[ l.rows[ k ] ]; + shown += !h.isDef ? 0 : ( weighted ? std::max( h.defRefs, 1 ) : 1 ); + } + return shown; }; const std::size_t defsOnDefsPage = defsShown( defs ); const std::size_t defsOnAllPage = defsShown( all ); @@ -3191,13 +4502,37 @@ inline ListedHits whereisServedListing( const WhereResult& res, std::size_t maxH return defsShorter ? std::move( defs ) : std::move( all ); } -// One --whereis answer page: the listing it serves (whereisServedListing), then that page. +// One --whereis answer page: the listing it serves (whereisServedListing), then that page, its long lines cut to previews +// only when that makes THIS page shorter (review C2). The listing is chosen on the WHOLE-line pages (ListedHits::textPreview +// is false there), so a preview never flips the default listing. A page holding a line over kWhereisTextWholeMaxBytes is +// then rendered both ways, and the cut one is served only when it is STRICTLY shorter both as written and as the compact +// dialect delivers it, the rule whereisServedListing applies to its two pages: its , its rows' t_bytes= and the +// PREVIEW clause or the compact readings can cost more than cutting a line just past the threshold saves (a 395-byte line +// saved 92 B and its disclosure cost 250). So a served page is byte-identical to the same page whole, or strictly shorter +// in both postures. A tie, or a render that fails, serves the page whole (the whole answer). inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_t maxHits, int pageLimit, int pageOffset, WhereisListing listing ) { // The LISTED rows (WhereisListing): indices into res.hits, in the hit list's own order, so a listing is a // subsequence of the whole list and its rows print byte-identically to the same rows of listing=all. const ListedHits listed = whereisServedListing( res, maxHits, listing ); + ListedHits previewed = listed; + previewed.textPreview = !res.textWhole; + if( whereisPageCutRows( res, previewed, whereisHitWindow( listed.rows.size(), maxHits, pageLimit, pageOffset ) ) == 0 ) + { + writeWhereisListedPage( out, res, maxHits, pageLimit, pageOffset, listed ); + return; + } + const Rendered cutPage = renderToString( [ & ]( std::FILE* f ) { writeWhereisListedPage( f, res, maxHits, pageLimit, pageOffset, previewed ); } ); + const Rendered wholePage = renderToString( [ & ]( std::FILE* f ) { writeWhereisListedPage( f, res, maxHits, pageLimit, pageOffset, listed ); } ); + const bool cutShorter = cutPage.ok && wholePage.ok && cutPage.text.size() < wholePage.text.size() + && compactDeliveredBytesOrWritten( cutPage.text, "whereis" ) + < compactDeliveredBytesOrWritten( wholePage.text, "whereis" ); + if( cutShorter || wholePage.ok ) + { + rw::emitRaw( out, ( cutShorter ? cutPage.text : wholePage.text ).c_str() ); + return; + } writeWhereisListedPage( out, res, maxHits, pageLimit, pageOffset, listed ); } diff --git a/src/docparse.h b/src/docparse.h index b90ff7a1e..8bd9f5e11 100644 --- a/src/docparse.h +++ b/src/docparse.h @@ -616,6 +616,26 @@ inline std::string shellQuote( const std::string& s ) } // namespace detail +// Could `markitdown ` start at all? The bridge runs through the shell, and a shell that finds no command answers +// rc 127, which runMarkitdown reads as "" — so a machine WITHOUT markitdown paid one /bin/sh start per binary doc on +// every warm run, for an answer known before the fork (cli-floor 2026-10-08: ~25 ms of a warm call on this repository, +// two present/ decks). This asks the same question the shell would, with no process: os::which walks PATH exactly as +// sh does (an empty entry is the current directory; a directory or a non-executable file named markitdown is not a +// program). It is asked per bridge doc, just before the popen it replaces, and never memoized, so installing +// markitdown under a running MCP server is seen on its next ingest. It answers TRUE whenever it cannot rule the shell out — PATH unset +// (sh then searches its own default path), an executable `./markitdown` (a Windows shell searches the current +// directory first), or an exported shell function of that name — because a false "absent" would drop a Section the +// shell would have produced, while a false "present" only costs the popen it always cost. Gated by +// test/docmdcachecheck.sh arms 6-9. +inline bool markitdownMayRun() +{ + if( std::getenv( "PATH" ) == nullptr || std::getenv( "BASH_FUNC_markitdown%%" ) != nullptr || std::getenv( "BASH_FUNC_markitdown()" ) != nullptr ) + { + return true; + } + return !os::which( "markitdown" ).empty() || !os::which( "./markitdown" ).empty(); +} + // Run `markitdown ` and return its stdout (Markdown), or "" if markitdown is not on PATH / it failed. // NOTE: this performs a subprocess call — used only for binary doc formats, only when they are ingested. inline std::string runMarkitdown( const std::string& path ) diff --git a/src/editcheck.h b/src/editcheck.h index 82b990994..077c6c3c7 100644 --- a/src/editcheck.h +++ b/src/editcheck.h @@ -23,13 +23,17 @@ #include "arch.h" // fnv1a64 #include "gitstamp.h" // r26-stamp Task A: gitstamp::atAttr — the at="[+dirty]" root anchor #include "graphlegend.h" // §H4 §3.4: the shared counts_floor= marker + floor/counting-unit legend tail +#include "compactlegend.h" // fix round 1 (ruling 5): foldPays — the verdict line ships only when it pays #include "pageview.h" // LB-G: pageWindow / effectiveRowCap / pagingDisclosure — the ONE paging vocabulary #include "editcheckdecl.h" // the C/C++ declaration/definition identity: editCheckTieDeclaration / editCheckDeclDefaults +#include "valuerefs.h" // CALLSITE-AT: VrRender / vrPath — the one root-relative file spelling a site token shares with rows #include +#include #include #include #include +#include #include #include #include @@ -650,17 +654,20 @@ inline bool editCheckArityAccepts( const Symbol& os, std::uint16_t minArity, std // aliases, imports and receiver, to a definition in the overload set: the same lexical evidence the map used, // asked about the function rather than one arity of it. Built once per document; `scratch` is the resolver's // output buffer, reused across a pass. -struct EditCheckCalleeTest +// +// CALLSITE-AT (2026-10-09): the test is split from the arity data. CallSiteCalleeTest is the name/Elixir half alone, +// which is all a SITE pass needs (the callers verb's sites_at= and safe-delete's sites_at=, callSitesAt below); EditCheckCalleeTest +// adds editCheckDeclDefaults, which reads the C/C++ declarations' source text and is only the arity verdict's input. +struct CallSiteCalleeTest { const IngestResult& ing; const Symbol& focus; std::span overloadNodes; std::optional logical; // engaged for an Elixir focus only std::vector scratch; - EditCheckDeclDefaults declDefaults; // per overload: the fewest arguments a matching C/C++ declaration admits - EditCheckCalleeTest( const IngestResult& input, const Symbol& focusSymbol, std::span overloads ) - : ing( input ), focus( focusSymbol ), overloadNodes( overloads ), declDefaults( editCheckDeclDefaults( input, overloads, {} ) ) + CallSiteCalleeTest( const IngestResult& input, const Symbol& focusSymbol, std::span overloads ) + : ing( input ), focus( focusSymbol ), overloadNodes( overloads ) { if( focus.lang == Lang::Elixir ) { @@ -690,6 +697,16 @@ struct EditCheckCalleeTest } }; +struct EditCheckCalleeTest : CallSiteCalleeTest +{ + EditCheckDeclDefaults declDefaults; // per overload: the fewest arguments a matching C/C++ declaration admits + + EditCheckCalleeTest( const IngestResult& input, const Symbol& focusSymbol, std::span overloads ) + : CallSiteCalleeTest( input, focusSymbol, overloads ), declDefaults( editCheckDeclDefaults( input, overloads, {} ) ) + { + } +}; + // per-node flags for the seen callers whose call-site is incompatible with the CURRENT arity: a call site is // flagged only when its argument count is reliably counted AND NO overload could accept it (every overload // has a FIXED arity (arityExact!=0) that disagrees — a variadic/default-arg/implicit-receiver wildcard can @@ -753,27 +770,193 @@ inline std::vector editCheckIncompatibleFlags( const IngestResult& ing, st // ONE pass over ing.references plus one sort — never a per-row rescan (editCheckIncompatibleFlags' own // rule; a widely-shared name has hundreds of caller rows). Duplicate (node,line) pairs collapse: this is a // set of LINES TO OPEN, so two calls on one line are one site. -inline std::vector> -editCheckCallSites( const IngestResult& ing, EditCheckCalleeTest& callee, std::span callerIncompatible ) +template +inline void forEachCallSite( const IngestResult& ing, CallSiteCalleeTest& callee, std::span callerSelected, OnSite&& onSite ) { - std::vector> sites; for( const Reference& r : ing.references ) { if( r.role != RefRole::Call || r.isCompose || r.isDocLink || r.lang == Lang::Markdown ) { continue; } - if( r.fromSymbol >= callerIncompatible.size() || !callerIncompatible[ r.fromSymbol ] || !callee.reaches( r ) ) + if( r.fromSymbol >= callerSelected.size() || !callerSelected[ r.fromSymbol ] || !callee.reaches( r ) ) { continue; } - sites.emplace_back( r.fromSymbol, r.line ); + onSite( r ); } +} + +inline std::vector> +editCheckCallSites( const IngestResult& ing, CallSiteCalleeTest& callee, std::span callerIncompatible ) +{ + std::vector> sites; + forEachCallSite( ing, callee, callerIncompatible, [ & ]( const Reference& r ) { sites.emplace_back( r.fromSymbol, r.line ); } ); std::sort( sites.begin(), sites.end() ); sites.erase( std::unique( sites.begin(), sites.end() ), sites.end() ); return sites; } +// CALLSITE-AT (2026-10-09, idea #1 of the 26b ideas pass): the SAME pass as editCheckCallSites, keeping each site's own +// file, for the rows that print a site as a pasteable `file:line` token (the callers verb's sites_at=, safe-delete's sites_at=). +// A row's p= is where the CALLER is defined; until this, "every call site that must change" needed the uses verb as a +// second call (tmux-19: 15 caller rows in the first answer, the 23 sites only in the follow-up). The file is the +// reference's own fileId, never inferred from the caller row. Sorted (caller, file, line), (caller, file, line) unique: +// two calls on one line are one site to open. POD with no member initializers (EditCheckContract's reason above). +struct CallSiteAt +{ + NodeId caller; + std::uint32_t fileId; + std::uint32_t line; + + friend auto operator<=>( const CallSiteAt&, const CallSiteAt& ) = default; +}; + +// The call sites of `defs` inside the caller rows `pageCallers` (one page of a callers listing). `defs` is the resolved +// definition set of ONE selector; a set whose members carry two leaf names (a qualified tier may) gets one pass per +// distinct name, so the second name's sites are never silently dropped. Each pass is one scan of the reference table. +inline std::vector callSitesAt( const IngestResult& ing, std::span defs, std::span pageCallers ) +{ + std::vector sites; + if( defs.empty() || pageCallers.empty() ) + { + return sites; + } + std::vector onPage( ing.symbols.size(), 0 ); + for( const NodeId c : pageCallers ) + { + EXPECTS( c < onPage.size(), "a caller row is an indexed symbol" ); + onPage[c] = 1; + } + std::vector sameName; + for( std::size_t i = 0; i < defs.size(); ++i ) + { + const std::string_view name = ing.symbols[ defs[i] ].name; + const auto nameAt = [ & ]( NodeId d ) { return ing.symbols[d].name == name; }; + if( std::any_of( defs.begin(), defs.begin() + std::ptrdiff_t( i ), nameAt ) ) + { + continue; // this name's pass already ran + } + sameName.clear(); + std::copy_if( defs.begin() + std::ptrdiff_t( i ), defs.end(), std::back_inserter( sameName ), nameAt ); + CallSiteCalleeTest callee( ing, ing.symbols[ defs[i] ], sameName ); + forEachCallSite( ing, callee, onPage, [ & ]( const Reference& r ) { sites.push_back( CallSiteAt{ r.fromSymbol, r.fileId, r.line } ); } ); + } + std::sort( sites.begin(), sites.end() ); + sites.erase( std::unique( sites.begin(), sites.end() ), sites.end() ); + return sites; +} + +// One caller's slice of callSitesAt's sorted result (empty when the pass found none for it). +inline std::span callSitesOf( std::span sites, NodeId caller ) +{ + const auto lo = std::lower_bound( sites.begin(), sites.end(), caller, []( const CallSiteAt& s, NodeId c ) { return s.caller < c; } ); + const auto hi = std::upper_bound( lo, sites.end(), caller, []( NodeId c, const CallSiteAt& s ) { return c < s.caller; } ); + return { lo, hi }; +} + +// One caller's sites as pasteable `file:line` tokens, ascending, at most kCallSitesPerRowCap of them (pageview.h: a +// runaway guard; the cut is disclosed by sites_total= and the uses verb lists the rest). `pathOf( fileId )` spells the file +// exactly as the row's own p= does (valuerefs.h vrPath: root-relative on a single root). Unescaped: each dialect escapes its own way. +static_assert( kCallSitesPerRowCap == 16, "compactlegend.h's sites_total= readings spell this cap as 16: change both together" ); +inline std::vector callSiteTokens( const IngestResult& ing, std::span mine, const VrRender& rr ) +{ + std::vector tokens; + const std::size_t shown = std::min( mine.size(), kCallSitesPerRowCap ); + tokens.reserve( shown ); + for( std::size_t i = 0; i < shown; ++i ) + { + tokens.push_back( vrPath( ing, mine[i].fileId, rr ) + ":" + std::to_string( mine[i].line ) ); + } + ENSURES( tokens.size() <= kCallSitesPerRowCap, "the per-row site cap holds" ); + return tokens; +} + +// A page of caller rows with their site tokens, computed ONCE and rendered by every dialect (XML, columnar, JSON, MCP): +// tokens[i]/totals[i] belong to rows[i]; `any` = some row has a site (the legend clause rides exactly then), `anyCut` = +// some row's list was cut (sites_total= rides). POD-style, value-initialized (EditCheckContract's reason above). +struct RowCallSites +{ + std::vector> tokens; + std::vector totals; + bool any; + bool anyCut; +}; + +inline RowCallSites rowCallSites( const IngestResult& ing, std::span defs, std::span rows, const VrRender& rr ) +{ + RowCallSites out{}; + const std::vector sites = callSitesAt( ing, defs, rows ); + out.tokens.reserve( rows.size() ); + out.totals.reserve( rows.size() ); + for( const NodeId r : rows ) + { + const std::span mine = callSitesOf( sites, r ); + out.tokens.push_back( callSiteTokens( ing, mine, rr ) ); + out.totals.push_back( mine.size() ); + out.any = out.any || !mine.empty(); + out.anyCut = out.anyCut || mine.size() > out.tokens.back().size(); + } + ENSURES( out.tokens.size() == rows.size() && out.totals.size() == rows.size(), "one entry per row, in row order" ); + return out; +} + +// The XML row attributes for one caller: ` sites_at="f:l f:l"` (space-separated, XML-escaped) plus ` sites_total="N"` only when +// the list was cut. "" when the pass found no site in this caller (the attribute is never printed empty). +inline std::string callSitesAttrXml( const std::vector& tokens, std::size_t total ) +{ + if( tokens.empty() ) + { + return {}; + } + std::string joined; + for( const std::string& t : tokens ) + { + if( !joined.empty() ) { joined += ' '; } + joined += t; + } + std::vector esc; + std::string out = " sites_at=\"" + std::string( escapeXml( joined, esc ) ) + "\""; + if( total > tokens.size() ) + { + out += " sites_total=\"" + std::to_string( total ) + "\""; + } + return out; +} + +// The JSON twin (CLI --json rows and the MCP calledBy rows): `,"sites_at":["f:l",..]` plus `,"sites_total":N` only when cut. +inline std::string callSitesKeyJson( const std::vector& tokens, std::size_t total ) +{ + if( tokens.empty() ) + { + return {}; + } + std::string out = ",\"sites_at\":["; + for( std::size_t i = 0; i < tokens.size(); ++i ) + { + out += i == 0 ? "\"" : ",\""; + out += jsonStr( tokens[i] ); + out += "\""; + } + out += "]"; + if( total > tokens.size() ) + { + out += ",\"sites_total\":" + std::to_string( total ); + } + return out; +} + +// One JSON row object with its sites_at/sites_total keys spliced in before the closing brace — only on the callers side and +// only when the page has sites (row k of `sites`); every other row comes back unchanged. +inline std::string withRowSites( std::string rowJson, const RowCallSites& sites, std::size_t k, bool callersSide ) +{ + if( callersSide && sites.any && k < sites.tokens.size() && !rowJson.empty() ) + { + rowJson.insert( rowJson.size() - 1, callSitesKeyJson( sites.tokens[k], sites.totals[k] ) ); + } + return rowJson; +} + // The comma-joined, ascending site list for ONE caller, or "" when the pass above found none (a flagged // caller always has at least one, so "" only happens on a degraded reference table — and an empty // attribute is never printed; see the emitter). @@ -1049,10 +1232,111 @@ inline bool editCheckShowsModuleScope( const IngestResult& ing, const std::vecto // definitions a file:name spelling found and could not tie to the file it named. callers=/incompatible= are read from // `focus` alone, so without it a declaration whose dropped definition carries the broken caller answered incompatible="0" // with nothing beside it. Defaults to 0 — absent attribute, absent clause — for a caller that resolved no file:name. +// `page` (2026-09-10, folded into one value by lane lean-trio): --limit/--offset, and — idea #11 — the CLI's clean-answer +// posture: `verdictOnly` makes an unchanged contract with nothing flagged print the verdict and the call that lists the +// callers instead of 40 caller rows, under every legend posture; `selector` is the CLI's own spelling of SYM, replayed in +// rows_next=. The MCP verb and the preview keep the full document (verdictOnly false). +struct EditCheckPage +{ + int limit = 0; + int offset = 0; + bool verdictOnly = false; + std::string_view selector; +}; + +// idea #11: the facts the VERDICT-ONLY answer is decided on, gathered by editCheckBundleText once every verdict number is +// fixed. The answer is taken only when there ARE caller rows and every fact the full document could add beside them is +// absent — one definition, nothing flagged, no unproven or declined definition, no notes, no explicit window, not the +// preview — so the rows are the ONLY thing left out, and rows_next= is the call that prints them (an explicit --limit is the full document). +// Fix round 1 (rv-lean-trio item 1, owner: a fold must never hide an answer-bearing row): ALSO only when the tree IS its git +// HEAD — the at= stamp carries no +dirty (tracked changes or untracked files). On a dirty tree the contract check has floors it +// cannot see past (a TS `export` dropped from SYM is status="unchanged"; a C caller edited to the wrong arity beside an unchanged +// prototype is never flagged), so a caller row may be the one the edit breaks: every row rides. A non-git root has no HEAD to +// be at, so it prints the full document too. +struct EditCheckVerdictFacts +{ + EditCheckPage page; + bool preview = false; + std::string_view status; + std::size_t incompatible = 0; + std::size_t defs = 0; + std::size_t unprovenDefs = 0; + std::size_t declinedCalls = 0; + bool defsUnmeasured = false; + bool defaultsFromDecl = false; + bool notesDegraded = false; + bool noNotes = true; + std::size_t callers = 0; +}; + +inline bool editCheckVerdictOnlyTaken( const EditCheckVerdictFacts& f ) noexcept +{ + // callers > 0: with no caller there is no row to leave out, so the full document is owed (its payload is the same line) + const bool cleanContract = f.status == "unchanged" && f.incompatible == 0 && f.defs == 1 && !f.defsUnmeasured && f.callers > 0; + const bool nothingBeside = f.unprovenDefs == 0 && f.declinedCalls == 0 && !f.defaultsFromDecl && !f.notesDegraded && f.noNotes; + return f.page.verdictOnly && !f.preview && f.page.limit == 0 && f.page.offset == 0 && cleanContract && nothingBeside; +} + +// The verdict line, or "" when the full document is owed (editCheckVerdictOnlyTaken). Its legend states what the full +// document's would about THESE attributes — the arity hedge (incompatible= is a fact about the tree, never a proof of +// binding), the floor tail every graph-count verb splices — and nothing about rows it does not carry. It is the full +// dialect; the compact layer rewrites it like any other (L1: the payload is the same under both postures). +inline std::string editCheckVerdictOnlyText( const EditCheckVerdictFacts& f, const Graph& g, const std::string& root, const Symbol& fsym, + std::string_view relPath, bool singleRoot ) +{ + if( !editCheckVerdictOnlyTaken( f ) ) + { + return {}; + } + // ruling 1: the tree IS its HEAD — a resolvable commit and no "+dirty" ("+shallow" may ride: it is about history, not the + // working tree); an empty stamp (no git, no HEAD) or a dirty one owes the full document. The subprocess runs only once + // every in-memory fact already chose the verdict, and its value IS the at= below. + const std::string atStamp = gitstamp::stampAt( root ); + if( atStamp.empty() || atStamp.find( "+dirty" ) != std::string::npos ) + { + return {}; + } + // fix round 2 (rv-lean-trio2 nit b): a tracked path marked skip-worktree or assume-unchanged can differ from HEAD with + // `git status` silent, so the stamp above reads clean over an edited caller. The SAME probe --quality-delta's identity + // basis refuses on (quality.h indexHidesTrackedContent; a git failure reads as "hides"): any such path owes the rows. + if( quality::indexHidesTrackedContent( root ) ) + { + return {}; + } + std::vector esc; + const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; + const std::string rowsNext = nextFlag( "--edit-check=", f.page.selector.empty() ? std::string_view( fsym.name ) : f.page.selector ) + + " --limit=" + std::to_string( f.callers ); + std::string out = ""; + out += ""; + editCheckPriceRoot( out ); // M11: unconditional, like the full document's + return out; +} + inline std::string editCheckBundleText( const IngestResult& ing, const Graph& g, const std::string& root, std::size_t maxFileBytes, const std::vector& excludes, NodeId focus, const notes::NoteIndex* ni = nullptr, bool preview = false, - int pageLimit = 0, int pageOffset = 0, std::size_t unprovenDefs = 0, + EditCheckPage page = {}, std::size_t unprovenDefs = 0, // L3 follow-up (CodeRabbit 4053600616): read BEFORE the caller nulls `ni` // for emptiness (main.cpp's MainDispatch::notesDegraded), so a sidecar // that left EVERY line unparsed still reaches this root. Defaults false, @@ -1061,6 +1345,8 @@ inline std::string editCheckBundleText( const IngestResult& ing, const Graph& g, // the pre-apply preview's spliced file: its spans index these bytes, not the disk const EditCheckSpliced& spliced = {} ) { + const int pageLimit = page.limit; + const int pageOffset = page.offset; const Symbol& fsym = ing.symbols[ focus ]; // R-E (2026-08-17 harvest): same single-root condition every other verb's root= uses (sarif.h) — the ONE // shared assembler for CLI --edit-check and the MCP edit_check verb, so the two dialects cannot diverge. @@ -1107,6 +1393,16 @@ inline std::string editCheckBundleText( const IngestResult& ing, const Graph& g, std::vector esc; const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; + // idea #11: the VERDICT-ONLY answer (editCheckVerdictOnlyText). Placed after every verdict number is fixed, like the + // window above: it cannot move a count. Fix round 1 (ruling 5): it is a CANDIDATE — the full document is still built, and + // the verdict line ships only when it pays against it (foldPays, at the end of this function). + const EditCheckVerdictFacts verdictFacts{ page, preview, verdict.status, incompatibleCount, overloadNodes.size(), unprovenDefs, declinedCalls, + contract.defsUnmeasured, callee.declDefaults.fromDecl, notesDegraded, + renderNoteChildren( ni, symbolNoteTarget( ni, ing, fsym ), esc ).empty() + && renderNoteChildren( ni, fileNoteTarget( ni, ing.files[ fsym.fileId ] ), esc ).empty(), + callerIds.size() }; + const std::string verdictLine = editCheckVerdictOnlyText( verdictFacts, g, root, fsym, ecPathRel( fsym.fileId ), ecSingleRoot ); + // card A1 — the preview sentence, FIRST, because every number below it is about bytes that are not on // disk and a reader who meets that fact at the end has already read the document as a fact about the tree. std::string out = ""; + + // the honest degrade page: the emitter READS the sink — the full quintet with zero rows and + // reason= naming what was lost, built by the SAME lambdas so it cannot drift (#362 review's ask). + if( !rendered ) + { + const std::string reason = " reason=\"" + std::string( forPageRender.why == ForPageRender::DisclosureWhy::ChargeStreamTorn + ? "sigs-charge-stream-torn" : "sigs-charge-stream-refused" ) + "\""; + const std::string h = buildHead( 0, win.end, reason ); + const auto [ t, est ] = buildTail( h, legend, "", /*overCeiling=*/false, win.end ); + return h + t + legend + ""; + } + // rows: packSignatures' block minus its own open tag (kept verbatim with its close). + const std::size_t openBeg = sigsDoc.find( "' ); + const std::size_t keepFrom = ( openBeg != std::string::npos && openEnd != std::string::npos ) ? openEnd + 1 : 0; + std::string rowsNow = sigsDoc.substr( keepFrom ); + + // THE TRUE RESUME (#362 review, item 2): the last PRINTED row's candidate index + 1 — NOT + // win.begin + a served count: a pseudo-symbol (a candidate slot packSignatures serves without + // printing a row) skews the arithmetic one short, and the next page re-serves the last row + // (the duplicate seam). shownIds is packSignatures' own emitted-ids report, in rank order. + std::size_t kept = shownIds.size(); + std::size_t nextNow = ( kept > 0 ) ? std::size_t( candIdxOf[ shownIds.back() ] ) + 1 : win.end; + // the forward guarantee (#362 review, item 3): next_offset ALWAYS moves past this page's + // window start when anything was printed — a next= that points back at the page it came + // from is a loop with no exit. + if( kept > 0 && nextNow <= win.begin ) { nextNow = win.begin + 1; } + + // THE BUDGET TRIM (#362 review, item 1): the page IS an answer under the budget. The H1 + // ladder already trimmed within the block; if the assembled page still exceeds, drop + // trailing WHOLE rows — cutting at the START of the last row (a cut at its close leaves + // the row open and the document malformed) — never below ONE row while the window is + // non-empty (item 3: a budget that cannot fit a row serves the top row with over_ceiling="1" + // rather than an empty page whose next= points back at itself), and re-derive the page's own + // arithmetic (shown/next_offset/has_more/next=) from what shipped. + std::string h = buildHead( kept, nextNow, {} ); + auto [ t, est ] = buildTail( h, legend, rowsNow, /*overCeiling=*/false, nextNow ); + if( tokenBudget > 0 && est > tokenBudget ) + { + const std::size_t rowFloor = ( win.end > win.begin ) ? std::size_t( 1 ) : std::size_t( 0 ); + while( kept > rowFloor && est > tokenBudget ) + { + const std::size_t lastClose = rowsNow.rfind( "" ); + const std::size_t rowStart = rowsNow.rfind( ""; + --kept; + nextNow = ( kept > 0 ) ? std::size_t( candIdxOf[ shownIds[ kept - 1 ] ] ) + 1 : win.end; + h = buildHead( kept, nextNow, {} ); + std::tie( t, est ) = buildTail( h, legend, rowsNow, /*overCeiling=*/false, nextNow ); + if( est <= tokenBudget ) { break; } + } + if( est > tokenBudget ) + { + std::tie( t, est ) = buildTail( h, legend, rowsNow, /*overCeiling=*/true, nextNow ); + } + } + std::string pageOut = h + t + legend + rowsNow; + // (#362 review, item 4) the dialect actually emitted: compact first (the central layer — it + // reprices est_tokens= and settles over_ceiling= on the bytes it writes), so the page's own + // numbers describe what ships; the outer layers see schema= and pass it through untouched. + if( compactLegend ) + { + applyCompactDialect( pageOut, "candidate-page" ); + } + // the stderr notice reads the FINAL page: est_tokens straight off the emitted root, so the + // line can never contradict the document (the full-dialect trim numbers used to). + if( tokenBudget > 0 ) + { + const std::size_t finalEst = rootUnsignedAttr( std::string_view( pageOut ).substr( pageOut.find( '<' ), pageOut.find( '>' ) + 1 ), "est_tokens" ); + if( finalEst > tokenBudget && pageOut.find( " over_ceiling=\"1\"" ) != std::string::npos ) + { + rw::emitTo( stderr, "ripwire: for-page est_tokens={} exceeds the stated budget={} — the window cannot fit; " + "the top row is served with over_ceiling=1\n", finalEst, tokenBudget ); + } + } + return pageOut; +} + + struct ForFileRow { std::uint32_t fileId = 0; diff --git a/src/gitmine.h b/src/gitmine.h index cee6a5b88..b11fb4073 100644 --- a/src/gitmine.h +++ b/src/gitmine.h @@ -19,6 +19,7 @@ // gitmine.h no longer carries its own copy; see jsonesc.h for the dedup rationale #include +#include // popenTrimmed's 64 KB read block #include // the join's once-per-process disclosure flags #include // std::popcount — the recurrence mask's set-bit count (Clio sub-windows) #include @@ -238,23 +239,27 @@ inline std::string sinceUnresolvedRefusal( std::string_view value, std::string_v // popen-trim shape in the tool: the quality.h git one-liners, the doctor probes, crossref.h and binstale.h all // reach it as quality::popenTrimmed (a using-declaration of this). It lives HERE — the lower header, which // quality.h includes — because resolveSinceScope needed the same reader (N4, 2026-09-04) and a second copy in -// this file was a --quality-delta new-clone-of-reused-helper row. Built on readByteSafeLine (infra/stdinline.h), -// the G3 byte-safe line reader every git pipe in this header uses: no fixed buffer to split a long path on -// (churnjoincheck's G3 arm forbids fgets here), a high byte never sign-changed. Lines are re-joined with the -// '\n' the reader consumed, so multi-line callers (crossref.h, the rename map) read exactly what fgets gave them. +// this file was a --quality-delta new-clone-of-reused-helper row. It reads the WHOLE stream in blocks and splits +// nothing: the answer is every byte the command wrote, then the trailing whitespace trim. That is byte-for-byte +// what the line reader it replaces produced (each line re-joined with the '\n' it consumed, an unterminated last +// line given one, which the trim then removes), with no fixed buffer to split a long path on (churnjoincheck's +// G3 arm) and no high byte ever sign-changed — but one fread per 64 KB instead of one locked fgetc per byte. The +// per-byte read was 55% of a --whereis on a 137-branch checkout (one `git ls-tree -r` per branch, cli-floor +// 2026-10-08), and every git one-liner in the tool reads through here. inline std::string popenTrimmed( const std::string& cmd ) { + PROFILE_SCOPE_DESCRIBE( "gitmine: popenTrimmed (one shell + git child, block read)" ); std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) { return {}; } - std::string out; - std::string line; - while( readByteSafeLine( pipe, line ) ) + std::string out; + std::array block; + for( std::size_t got = std::fread( block.data(), 1, block.size(), pipe ); got > 0; got = std::fread( block.data(), 1, block.size(), pipe ) ) { - out += line; - out += '\n'; + ASSUME( got <= block.size(), "fread returns at most the count it was asked for" ); + out.append( block.data(), got ); } os::pclose( pipe ); while( !out.empty() && ( out.back() == '\n' || out.back() == '\r' || out.back() == ' ' ) ) @@ -669,8 +674,24 @@ struct GitCommandLines std::vector lines; bool isStarted = false; int status = 0; + bool abandoned = false; // the read stopped early (abandonGitWalks): lines is a prefix, never an answer }; +// A git walk this run no longer needs: --for / --metrics / --exemplar start the 18-month history walk beside the ingest +// (main.cpp startHistoryWalks), and a memory-guard refusal used to join that walk's future before exiting — the refusal +// line was on stderr, the exit code waited for the end of the log (CodeRabbit on #383, main.cpp:3721). Set once, +// process-wide, only on an exit path: the walk's reader (gitLogNameOnlyRaw, the `--name-only` stream that IS the 431 ms +// walk) and the shared line reader (gitCommandLines) stop at their next line and pclose, git gets SIGPIPE on its next +// write, and the join ends there. A block reader (popenTrimmed: the walk's sha-list freshness probe, milliseconds on a +// real repository) runs to its end. A line reader that stopped early says so (`abandoned`), and the stream cache +// (quality.h gitRawCommitStreamCached) never writes a prefix. test/memguardcheck.sh (B21). +inline std::atomic gGitWalksAbandoned{ false }; +inline void abandonGitWalks() noexcept { gGitWalksAbandoned.store( true, std::memory_order_relaxed ); } +inline bool gitWalksAbandoned() noexcept { return gGitWalksAbandoned.load( std::memory_order_relaxed ); } +// the walk readers' line read: false at the end of the stream, or as soon as the walks are abandoned (the lines read so +// far are then a prefix no caller serves or caches — both are guarded by the same flag) +inline bool readWalkLine( std::FILE* pipe, std::string& line ) { return !gitWalksAbandoned() && readByteSafeLine( pipe, line ); } + inline GitCommandLines gitCommandLines( const std::string& cmd ) { GitCommandLines out; @@ -696,7 +717,7 @@ inline GitCommandLines gitCommandLines( const std::string& cmd ) // is a silently DROPPED commit rather than a fabricated one — still a number the reader would trust. // It leaves a trailing '\r' in place by contract, which is why the strip below still runs. std::string line; - while( readByteSafeLine( pipe, line ) ) + while( readWalkLine( pipe, line ) ) { while( !line.empty() && ( line.back() == '\n' || line.back() == '\r' ) ) { @@ -707,7 +728,8 @@ inline GitCommandLines gitCommandLines( const std::string& cmd ) out.lines.push_back( line ); } } - out.status = os::pclose( pipe ); + out.abandoned = gitWalksAbandoned(); // the rest of the stream was never read; pclose ends the child at its next write + out.status = os::pclose( pipe ); return out; } @@ -1560,8 +1582,41 @@ struct RawCommitStream { struct Commit { std::int64_t epoch = 0; std::vector paths; }; std::vector commits; + // K51: the walk COULD NOT BE READ (git would not start, or exited non-zero part-way) — as against "read, and the + // window holds no commit". `commits` is then empty or a prefix, never a measurement, and the memoizing caller + // must not store it: an empty stream cached under the (repo, HEAD, window) key silenced churn=/amp= until HEAD moved. + bool unread = false; + // Why, when `unread`: git never ran (no popen, or the shell could not find/execute it: exit 127/126) — `commits` is + // empty — as against git ran and stopped part-way, `commits` the newest-first prefix it printed before it died. + bool notRun = false; + // git's decoded exit code (WEXITSTATUS), -1 when it did not exit normally or never started; 0 on a clean read. + int exitCode = 0; }; +// K51: mark a walk unread. `exitCode` is git's decoded exit (-1: it never started, or did not exit normally); the shell's 127 +// (not on PATH) and 126 (not executable) mean git never ran either, so `commits` is empty, not a prefix. +inline void markWalkUnread( RawCommitStream& out, int exitCode, bool neverStarted ) noexcept +{ + out.unread = true; + out.exitCode = exitCode; + out.notRun = neverStarted || exitCode == 127 || exitCode == 126; +} + +// A pclose()/wait status as an exit code: WEXITSTATUS on a normal exit, -1 for a signal or an unreadable status. +inline int decodedExitCode( int status ) noexcept +{ + return WIFEXITED( status ) ? WEXITSTATUS( status ) : -1; +} + +// K51: fold the walk's pclose() status into `out` — 0 is a clean read and changes nothing. +inline void noteWalkExit( RawCommitStream& out, int status ) noexcept +{ + if( status != 0 ) + { + markWalkUnread( out, decodedExitCode( status ), false ); + } +} + inline RawCommitStream gitLogNameOnlyRaw( const std::string& root, const std::string& coSince ) { PROFILE_SCOPE_DESCRIBE( "gitmine: gitLogNameOnlyRaw (git log --name-only popen)" ); @@ -1572,11 +1627,12 @@ inline RawCommitStream gitLogNameOnlyRaw( const std::string& root, const std::st std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) { + markWalkUnread( out, -1, true ); return out; } std::string s; - while( readByteSafeLine( pipe, s ) ) // F6: THE line reader, not a char[4096] a long path can be split across + while( readWalkLine( pipe, s ) ) // F6: THE line reader, not a char[4096] a long path can be split across; stops when abandoned { while( !s.empty() && ( s.back() == '\n' || s.back() == '\r' ) ) { @@ -1598,7 +1654,9 @@ inline RawCommitStream gitLogNameOnlyRaw( const std::string& root, const std::st } out.commits.back().paths.push_back( std::move( s ) ); // readByteSafeLine clear()s its buffer first, so moving out of it is safe } - os::pclose( pipe ); + // K51: git died (a missing or corrupt object, a bad ref) — the lines above are a prefix, not the window — or the shell + // never ran it (127: not on PATH, 126: not executable), and there are no lines at all. + noteWalkExit( out, os::pclose( pipe ) ); return out; } diff --git a/src/graph.h b/src/graph.h index e8eb7b1bc..a191f5bd7 100644 --- a/src/graph.h +++ b/src/graph.h @@ -9,14 +9,14 @@ #include "pageview.h" // LB-H: kImportReachRowCap — the import tier's display cap lives with the rest of the truncation vocabulary #include "nextverb.h" // cut-fix E: nextFlag / nextAttrXml — the import tier's importers_next= #include "graphlegend.h" // M15: graphGaugeAttrXml/Json + kGraphCountFloorAttrXml/Json — graphCountFloorAttrXml( g ) below -#include "lintrules.h" // §P9.4: langOfPath / dependencyCapable — the file-language classification +#include "lintrules.h" // langOfPath / dependencyCapable — the file-language classification // restrictDependencyHealth() needs (owns the extension table, kept in sync // by hand with ingest.cpp's kLangTable per its own header comment) #include "infra/sparseCsr.h" // first-party infra math (src/infra/) #include "infra/csrverify.h" // structural gate, ASSUME'd after every production CSR build #include "infra/hashutil.h" // fnv1aAbsorb — internDeclinedList buckets a declined call's candidate list by its bytes #include "pagerank.h" // double-precision PageRank kernel over float CSR storage -#include "prconverge.h" // W2-F: RankDisclosure — the power iteration's own account, carried with its result +#include "prconverge.h" // RankDisclosure — the power iteration's own account, carried with its result #include "smallvec.h" // rw::SmallVec — THE ONE ALIAS (src/smallvec.h picks the implementation) #include "resolve.h" // P2-D one-hop type narrowing (Rule 1: class membership) — applied before the name-based fallback #include "receiverevidence.h" // FE-B: typed receivers RESOLVE; a call nothing proves keeps its rows marked via="name" @@ -69,6 +69,9 @@ struct Graph std::size_t scipDocsSeen = 0; // # SCIP documents consumed (0 unless --scip); honesty summary std::size_t scipEdgesPinned = 0; // # (from,to) edges the SCIP index pinned; honesty summary std::vector> implementors; // base-class id → derived class ids (inheritance/Lego view) + // lane lego-transitive fix 1: parallel to implementors — how each base→derived edge was BOUND (BaseBound). The direct rows + // and every other reader keep reading implementors alone; only the --lego closure (legoImplementorClosure) walks this. + std::vector> implementorBound; std::vector> mentions; // code symbol id → markdown file-nodes that name it in a `backtick` (doc↔code) std::vector ambOut; // per-symbol: # outgoing calls that STILL resolved to MULTIPLE in-repo // defs AFTER canonical/narrow/locality resolution (the resolver could @@ -142,6 +145,12 @@ struct Graph // that call bound to it by name. std::vector gateDeclinedTarget; std::size_t gateDeclinedCalls = 0; // of the header's declined=, the calls the builtin-method name gate declined + // Of the header's declined=, the C++ calls the standard-member gate declined (StdMemberGate below): its own legend clause + // rides where this is >0, so a tree with none keeps its bytes. stdMemberDeclines is one entry per DECLINED C++ call named + // like a standard member on a receiver no rule tied to an in-tree class — by that gate or by the ladder's own tier-3 decline — as + // ( caller, kCppStdMemberNames index ), sorted after the resolve loop: the callees answer's line. + std::size_t stdMemberDeclinedCalls = 0; + std::vector> stdMemberDeclines; // Every call reference's disposition (pincensus.h CallDisposition), one bucket per reference. Read by buildGraph's // unaccounted alert and copied into pinCensus when a census is armed; its external/unresolved/declined buckets // equal those header gauges by construction. @@ -177,6 +186,168 @@ struct Graph // emits nothing — the map is byte-identical either way. }; +// One row of an interface's implementor CLOSURE (lane lego-transitive): a type `depth` extends/implements hops below the +// walk's base, reached FIRST through `via` (the base itself at depth 1). `byName` (fix 1): the edge from `via` was bound by +// the base NAME alone — no import, qualifier or enclosing scope proved which same-named type the clause meant. +struct ImplementorRow +{ + NodeId id = kNoNode; + NodeId via = kNoNode; + std::uint32_t depth = 0; + bool byName = false; +}; + +// lane lego-transitive fix 1: how an extends/implements edge base→derived was bound (Graph::implementorBound). +// Proven — the language's own lookup names THIS base: a same-file declaration the clause's enclosing scopes declare +// (ingest_relations.h localBaseBinding), the file an import binds the name (or its qualifier) to, an included +// C-family header, the package directory (Java/Kotlin/Go), Ruby's constant scope (RubyBaseScope). +// ByName — no such evidence, or it named two candidates: the edge rests on the spelling alone. +// Other — the evidence names a DIFFERENT same-named type: this edge is the name collision, not a subtype. +// The values order the merge of two references drawing the same edge: the smaller wins (a proven site proves the edge). +enum class BaseBound : std::uint8_t { Proven = 0, ByName = 1, Other = 2 }; + +// Breadth-first walk DOWN the implementors map from `base`: every type that extends/implements it directly (depth 1) +// or through another such type (depth 2+), each ONCE at its shallowest depth. Order is depth ascending, then discovery +// order — each parent's list is id-sorted by buildGraph — so the walk is deterministic. A cycle or a diamond visits a +// type once and `base` never re-enters. Language-neutral: the map holds every language's edges (class bases, Rust +// `impl Trait for T`, Ruby mixins, the JS `Base.extend('Name')` factory), so the closure is one rule for all of them. +inline std::vector implementorClosure( const std::vector>& implementors, NodeId base ) +{ + std::vector rows; + HashMap seen; + seen.reserve( 32 ); + seen.emplace( base, char( 1 ) ); + const auto expand = [ & ]( NodeId parent, std::uint32_t depth ) + { + if( parent >= implementors.size() ) + { + return; + } + for( NodeId child : implementors[ parent ] ) + { + if( seen.emplace( child, char( 1 ) ).second ) + { + rows.push_back( ImplementorRow{ child, parent, depth } ); + } + } + }; + expand( base, 1 ); + for( std::size_t i = 0; i < rows.size(); ++i ) // rows grows inside the loop: index, never iterate by reference + { + expand( rows[i].id, rows[i].depth + 1 ); + } + ENSURES( rows.empty() || rows.back().depth >= rows.front().depth ); // breadth-first: depth never falls + return rows; +} + +// lane lego-transitive fix 1: the --lego closure, walked ONLY through edges the index proves (BaseBound::Proven). The plain +// walk above follows every implementors edge, and buildGraph binds a base NAME to every same-named class — so one name +// collision (b.py's `Leaf(Mid)` under a.py's unrelated `Mid`) became an unhedged depth-2 subtype on every ancestor's +// answer. Here the depth-1 rows are the direct implementors exactly as the map lists them; below them: +// • a Proven edge adds its row and is walked further; +// • a ByName edge adds its row HEDGED (byName) and is not walked: nothing proves the type below it is in this closure; +// • an Other edge — the evidence names a different same-named type — adds no row; `otherBase` counts those types (each +// once, and never one the walk lists anyway). They are listed on the --lego answer of the type they do extend. +// A direct row whose own edge is not Proven is listed (the map's contract) but not walked; when types sit below it, +// `unwalked` names it (fix 2: the stop is disclosed, never silent — the row's own --lego answer walks them). A type first +// reached hedged and then proven at the same depth is upgraded and walked. Order and determinism are the plain walk's. +struct LegoClosure +{ + std::vector rows; + std::size_t otherBase = 0; + std::vector unwalked; // direct rows with implementors of their own whose edge is not proven, in row order +}; + +// The walk's state, one closure: the rows so far, which of them are walked further, the types each row's node maps to, +// and the types reached only through an Other edge. +class LegoClosureWalk +{ +public: + LegoClosureWalk( const std::vector>& implementors, const std::vector>& bound, NodeId base ) + : implementors_( implementors ), bound_( bound ) + { + seen_.reserve( 32 ); + seen_.emplace( base, std::numeric_limits::max() ); + expand( base, 1 ); + for( std::size_t i = 0; i < out_.rows.size(); ++i ) // rows grow inside the loop: index, never iterate by reference + { + if( walk_[ i ] != 0 ) + { + expand( out_.rows[ i ].id, out_.rows[ i ].depth + 1 ); + } + } + for( const auto& [ id, unused ] : other_ ) + { + ( void )unused; + out_.otherBase += seen_.find( id ) == seen_.end() ? 1u : 0u; + } + for( std::size_t i = 0; i < out_.rows.size() && out_.rows[ i ].depth == 1; ++i ) // breadth-first: the direct rows lead + { + const NodeId id = out_.rows[ i ].id; + if( walk_[ i ] == 0 && id < implementors_.size() && !implementors_[ id ].empty() ) + { + out_.unwalked.push_back( id ); + } + } + ENSURES( out_.rows.size() == walk_.size() ); + } + LegoClosure take() { return std::move( out_ ); } + +private: + BaseBound boundOf( NodeId parent, std::size_t k ) const noexcept + { + return bound_.empty() ? BaseBound::ByName : BaseBound( bound_[ parent ][ k ] ); // no bound map: nothing is proven + } + void add( NodeId child, NodeId parent, std::uint32_t depth, BaseBound b ) + { + const auto [ it, fresh ] = seen_.try_emplace( child, std::uint32_t( out_.rows.size() ) ); + if( fresh ) + { + out_.rows.push_back( ImplementorRow{ child, parent, depth, depth >= 2 && b == BaseBound::ByName } ); + walk_.push_back( b == BaseBound::Proven ? char( 1 ) : char( 0 ) ); + return; + } + const std::uint32_t at = it->second; + if( b == BaseBound::Proven && at < out_.rows.size() && out_.rows[ at ].depth == depth && walk_[ at ] == 0 ) + { + out_.rows[ at ].via = parent; // the same depth, now through a proven edge: upgrade, and walk it + out_.rows[ at ].byName = false; + walk_[ at ] = 1; + } + } + void expand( NodeId parent, std::uint32_t depth ) + { + const std::vector* kids = parent < implementors_.size() ? &implementors_[ parent ] : nullptr; + for( std::size_t k = 0; kids != nullptr && k < kids->size(); ++k ) + { + const BaseBound b = boundOf( parent, k ); + if( depth >= 2 && b == BaseBound::Other ) + { + other_.try_emplace( ( *kids )[ k ], char( 1 ) ); + } + else + { + add( ( *kids )[ k ], parent, depth, b ); + } + } + } + + const std::vector>& implementors_; + const std::vector>& bound_; + LegoClosure out_; + std::vector walk_; // parallel to rows: 1 = the edge that added the row is proven, so it is walked + HashMap seen_; // node → its row index (base → UINT32_MAX) + HashMap other_; // types reached only through an Other edge +}; + +inline LegoClosure legoImplementorClosure( const std::vector>& implementors, const std::vector>& bound, NodeId base ) +{ + EXPECTS( bound.empty() || bound.size() == implementors.size(), "buildGraph sizes the bound map with the implementors map" ); + LegoClosure out = LegoClosureWalk( implementors, bound, base ).take(); + ENSURES( out.rows.empty() || out.rows.back().depth >= out.rows.front().depth ); // breadth-first: depth never falls + return out; +} + // FE-B: is the call edge from → to NAME-ONLY, over the raw out-CSR and its parallel hedge bits — bound by name alone at // every site that bound it, so every surface renders it via="name". false for an edge that does not exist and for empty // hedge bits (a graph with no name-only edge at all). edgeNameOnly( Graph ) below and serialize.h's block share it. @@ -2457,9 +2628,8 @@ inline JsImportTables buildJsImportTables( const IngestResult& ing, const WsIncl // message send has no receiver shape either — so a gate would decline typed true edges it cannot tell from the // false ones; probed: `void f( Pool p ) { p.get( k ); }` and `Pool *p; [p addObject:x]` bind by name alone); // Go (its builtin types have no methods; a stdlib-type receiver such as `sync.Pool.Get` does bind a lone in-repo -// `Get` by name, and needs the declared-type evidence Go's extractor does not record either); C and C++ (they -// carry declared-type evidence, so the fix there is an evidence-AGAINST rule for a receiver of a std or builtin -// type, not a name list). +// `Get` by name, and needs the declared-type evidence Go's extractor does not record either); C (no member +// functions); C++ has its own rule, StdMemberGate below, because its receiver evidence differs (Rule 1/2/2b type it). // THE SAME-FILE ANNOTATION RULE (BuiltinMethodGate::fileNames, byReferenceOnly). Python records no annotation as a binding // or reference, so `def f( p: Pool )`, `x: Pool = …`, `"Pool"`, `Optional[Pool]`, `isinstance( o, Pool )` and `-> Pool` // leave fileRefClasses empty in the file that DEFINES Pool. A class the file defines still counts there when its name @@ -2849,6 +3019,199 @@ struct BuiltinMethodGate } }; +// The class template argument `n` of `written` names, its template-id's `<` at `open`: the argument's final segment, `const` +// / `volatile` / `struct` / `class` and a leading `::` stripped, `Gen` naming Gen. "" when there is no such argument, it is +// written in std, or it is no class name (a pointer, an array, a function type; a builtin keyword reads as itself and names +// no class). +inline std::string_view stdTemplateArgClass( std::string_view written, std::size_t open, int n ) noexcept +{ + EXPECTS( open < written.size() && written[ open ] == '<' && n >= 0, "a template-id's `<` and an argument index" ); + std::size_t begin = open + 1; + std::size_t end = begin; + int depth = 0; + int arg = 0; + for( ; end < written.size(); ++end ) + { + const char c = written[ end ]; + if( c == '<' || c == '(' ) + { + ++depth; + } + else if( ( c == '>' || c == ')' ) && depth > 0 ) + { + --depth; + } + else if( depth == 0 && c == ',' && arg < n ) + { + ++arg; + begin = end + 1; + } + else if( depth == 0 && ( c == '>' || c == ',' ) ) + { + break; + } + } + if( end >= written.size() || arg != n ) + { + return {}; // no closing `>`, or fewer arguments than `n + 1` + } + std::string_view argText = trimWs( written.substr( begin, end - begin ) ); + for( const std::string_view cv : { std::string_view( "const " ), std::string_view( "volatile " ), std::string_view( "struct " ), std::string_view( "class " ) } ) + { + argText = argText.starts_with( cv ) ? trimWs( argText.substr( cv.size() ) ) : argText; + } + argText = argText.starts_with( "::" ) ? argText.substr( 2 ) : argText; + argText = trimWs( argText.substr( 0, argText.find( '<' ) ) ); // `Gen` names Gen + if( argText.starts_with( "std::" ) || argText.empty() || argText.find_first_of( "*&[( " ) != std::string_view::npos ) + { + return {}; + } + const std::size_t cut = argText.rfind( "::" ); + const std::string_view name = ( cut == std::string_view::npos ) ? argText : argText.substr( cut + 2 ); + const bool isId = !name.empty() && rw::namesplit::isIdentStart( name.front() ) + && std::all_of( name.begin(), name.end(), []( char c ) { return rw::namesplit::isIdentChar( c ); } ); + ENSURES( !isId || ( name.data() >= written.data() && name.data() + name.size() <= written.data() + written.size() ), + "the class is a view into the recorded text" ); + return isId ? name : std::string_view{}; +} + +// The element type an element access (`c[ k ]`, `c.at( k )`, `c.front()`, `c.back()`) of a container whose recorded type text +// is `written` yields — the final segment of the template argument externalnames.h cppStdElementArgIndex names: +// "std::map" -> "EvalString", "const std::vector" -> "Item". "" for every other text: +// another template, an argument written in std ("std::vector", "std::map>" — one level only), +// an argument that is not a class name. +inline std::string_view stdElementType( std::string_view written ) noexcept +{ + constexpr std::string_view kStd = "std::"; + written = written.starts_with( "const " ) ? trimWs( written.substr( 6 ) ) : written; + const std::size_t open = written.find( '<' ); + if( !written.starts_with( kStd ) || open == std::string_view::npos ) + { + return {}; + } + const int argIndex = externalnames::cppStdElementArgIndex( trimWs( written.substr( kStd.size(), open - kStd.size() ) ) ); + return argIndex < 0 ? std::string_view{} : stdTemplateArgClass( written, open, argIndex ); +} + +// THE C++ STANDARD-MEMBER GATE (test/stdmembergatecheck.sh; the names are data: externalnames.h kCppStdMemberNames). +// A C++ member call whose receiver no rule typed — `out.push_back( c )` on a `std::vector&` parameter, `s.size()`, +// `getV().empty()` — is NAME-ONLY (FE-B): it keeps every same-file, same-directory and included-file definition of the +// name, each hedged via="name". When the name is a standard container or string member, those rows were overwhelmingly +// calls on the standard types: on this repository's own map 14 of the top 20 PageRank rows were such names +// (svector::push_back, WidePath::c_str, ElixirResolver::append, every `empty` and `find` in src/). A standard container +// is a concrete, non-virtual class template, so a receiver of standard type never dispatches into the tree: the standard +// library's own member is always one of the call's candidates, and nothing in the source chose an in-tree one over it. +// Such a call is DECLINED, never retargeted: no edge; the header's declined=; the tier the ladder would have bound is +// interned (declined_calls= on --callers/--impact of those definitions) and marked in gateDeclinedTarget (none of them +// becomes a dead-code candidate for losing a by-name caller); Graph::stdMemberDeclinedCalls for its legend clause; and a +// line on the caller's --callees answer (Graph::stdMemberDeclines, stdMemberCallsMadeBy). +// APPLIES to a C++ Call with no qualifier, written on a receiver (`x.m()`, `p->m()`, `f().m()`), named in the table, that +// the resolve loop left NAME-ONLY (no SCIP pin, canonical tier, receiver rule, FFI binding or identity claim decided it, +// and the language's own lookup proved no candidate — `this->size()` in a class with `size` is proven and kept), and +// whose receiver's recorded WRITTEN type does not name an in-tree class that may define the callee (buildGraph's +// stdReceiverOf / stdReceiverKeeps: a local declared `SmallVec` — an alias the rules do not map — whose cone holds +// svector keeps the ladder's hedge on svector::push_back, since the type may be that container, and so does a field written +// that way, `this->items_` or one hop off a typed root, `b.items_`; a namespace-scope, file-static or extern variable, +// `ns::List g_list; g_list.size()`; a `->` through a standard smart pointer or optional whose argument is the class, +// `std::unique_ptr up; up->size()` (kCppStdArrowTemplates); a `->` through an in-tree class with its own +// `operator->`, `SP& p; p->size()`, or through a class the tree does not define, `QSharedPointer& p` — +// their pointee is unrecorded; a `->` through a typedef or using alias whose recorded target is one of those, `typedef +// std::shared_ptr ListSP; ListSP p; p->size()` (the alias record keeps no template argument, so `typedef +// std::unique_ptr StrUP` keeps the hedge too); `HashMap m; m.find( k )` reaching UnionFind::find by name, +// `std::unique_ptr p; p->size()`, `std::vector* pv; pv->size()` and `IntVec* p; p->size()` through +// `typedef std::vector IntVec` do not keep it); ONE element access of a container whose recorded written type is a std +// sequence container or map (externalnames.h kCppStdElementFirstArg / kCppStdElementSecondArg) — `m[ k ].empty()`, `v.at( i ) +// .size()`, `v.front().size()`, `rule->bindings_[ k ].empty()` through `typedef std::map Bindings;` — +// reads the element type off that text (stdElementType; a local's, a parameter's or a namespace-scope variable's qualified +// text, a std-container field's "elem" record, a type alias's recorded target text), so `std::vector>& vs; vs[ 0 ] +// .push_back()` keeps the hedge and `std::vector& v; v[ 0 ].size()` does not. +// STATED FLOORS (test/stdmembergatecheck.sh arms H and W pin them): (1) by name: an in-tree class reached through a receiver +// the index records no written type for loses its by-name edge to declined_calls= — a call result (`h.list().size()`, +// `v.front().size()`, `makeList().size()`), an iterator (`auto it = v.begin(); it->size()`), `auto` (`auto l = ns::List{}`, +// `auto& v = pool.items()`), `( *p ).push_back()`, a range-for `auto`, a template parameter, a field two hops off its root, an +// element access of an element access (`m[ a ][ b ].m()`, an element written in std: `std::map>` reads one +// level only), an element access whose container is no one-hop name (`h.list()[ 0 ].size()`), an iterator's `->second`, and +// a std-container field inherited from a base class (stdElemFields keys the declaring class alone); (2) a template's arguments are read only where the parser recorded the type's whole text — a +// QUALIFIED type (Binding::importedName) — and only through `->`: `QList l; l.size()` (unqualified) and +// `std::vector v; v.size()` (the container's own member) are declined; a member field's template arguments are +// read for std::unique_ptr / std::shared_ptr alone (ingest_relations.h stdSmartPointee), so `std::optional o_; +// o_->size()` is declined, and an alias declared inside a function body records no target (captureTypeAlias), so a local +// `typedef std::shared_ptr L; L p; p->size()` is declined; (3) a written type is matched by its +// cone's class NAMES, as CHA-lite matches it, and a namespace-scope variable by its name — those declared in the caller's +// file or a file it includes, else every one in the tree; (4) generic names and iostream/filesystem/atomic/thread members are +// not in the table (externalnames.h says which and why), so their calls keep the ladder. +// LANGUAGE SCOPE (each probed): C has no member functions. Rust receivers carry no declared type (`let s = Stack{..}; +// s.push()` splits by name today), so a gate would decline typed true edges with the false ones. Go, Java, Kotlin, C# and +// Swift name their standard members after interfaces in-tree types implement on purpose (Len/Write/String/Close, add/size, +// Add, append), so an unproven receiver may dispatch into the tree. ObjC message sends carry no receiver shape. +// Python/JS/TS/Ruby: BuiltinMethodGate above. +struct StdMemberGate +{ + static constexpr std::uint16_t kNotStd = 0xFFFFu; + // How many in-tree definitions the name must reach before the gate declines. The split it replaces always has the standard + // library's own member as one arm, so ONE in-tree candidate already makes two unrelated definitions: `s.append( x )` on a + // std::string bound to a repository's only `append` (one definition, one by-name edge) is the case this gate exists for. + static constexpr std::size_t kMinInTreeCandidates = 1; + + // The table index of a call the gate is about — C++, a call, unqualified, written on a receiver, a table name — else + // kNotStd. Whether the resolve loop left it name-only, and its receiver's written type, are the caller's to add. + static std::uint16_t tableIndexOf( const Reference& r ) noexcept + { + if( r.lang != Lang::Cpp || r.role != RefRole::Call || !r.qualifier.empty() || !isMemberCallRef( r ) ) + { + return kNotStd; + } + const std::span table( externalnames::kCppStdMemberNames ); // strictly sorted: static_assert + const auto it = std::lower_bound( table.begin(), table.end(), std::string_view( r.calleeName ), rw::sortutil::svLess ); + const std::uint16_t index = ( it != table.end() && *it == r.calleeName ) ? std::uint16_t( it - table.begin() ) : kNotStd; + ENSURES( index == kNotStd || index < table.size(), "a table index, or none" ); + return index; + } + + // What the receiver exception read off a call's receiver (buildGraph's stdReceiverOf): the written class names whose cone + // may hold the callee — the receiver's own type, or the pointee a `->` reaches — and whether the call goes through a `->` + // whose pointee the declaration does not record (an in-tree class with its own `operator->`, a class the tree does not + // define). Views into ing.bindings, the field tables and the name tables, all fixed for the resolve loop; one reused + // buffer per resolve loop (clear() between calls). + struct Receiver + { + std::vector types; + bool opaqueArrow = false; + void clear() noexcept + { + types.clear(); + opaqueArrow = false; + } + }; + + // true ⇒ `name` (a std template's own name, "shared_ptr") is one whose `->` reaches its first template argument + static bool isStdArrowTemplate( std::string_view name ) noexcept + { + const std::span wrappers( externalnames::kCppStdArrowTemplates ); + return std::binary_search( wrappers.begin(), wrappers.end(), name, rw::sortutil::svLess ); + } + // The pointee a `->` reaches through a declaration whose recorded type text (Binding::importedName: the written type WHOLE, + // as the parser spelled it) is a standard smart pointer or optional — the final segment of its first template argument: + // "std::unique_ptr" -> "List", "std::optional>" -> "Gen". "" for every other text: another + // template (kCppStdArrowTemplates), an argument written in std ("std::unique_ptr"), an argument that is not + // a class name (a pointer, an array, a function type, a builtin keyword reads as itself and names no class). + static std::string_view stdArrowPointee( std::string_view written ) noexcept + { + constexpr std::string_view kStd = "std::"; + const std::size_t open = written.find( '<' ); + if( !written.starts_with( kStd ) || open == std::string_view::npos ) + { + return {}; + } + if( !isStdArrowTemplate( trimWs( written.substr( kStd.size(), open - kStd.size() ) ) ) ) + { + return {}; + } + return stdTemplateArgClass( written, open, 0 ); + } + +}; + // The class a call runs against, keyed as canonByName and the inheritance graph key a class (its name). Inside a def it // is the def's scope. A Ruby call in a class or module BODY runs with that class as self — a DSL call there // (`define_section :x`, `scope :y, -> { where( … ) }`) — while the class symbol's own scope names the namespace around it. @@ -5512,6 +5875,289 @@ inline FalseEdgeRules buildFalseEdgeRules( const IngestResult& ing, const HashMa return rules; } +// lane lego-transitive fix 1: the evidence buildGraph's implementors builder reads to tell WHICH same-named class a base +// clause meant (BaseBound). Every table is one the call resolver already built; this holds references, owns nothing. +struct BaseEvidenceTables +{ + const IngestResult& ing; + const ExternalVetoTables& extVeto; // Python `from m import X` / `import m as q` → m's one file + const JsImportTables& jsImports; // JS/TS named import → the definition it pins + const std::vector>& fileIncludes; // C-family: the transitive, path-precise include set (sorted) + const std::vector>& includeAdj; // every language: the files a file's imports resolve to + const std::vector& fileDir; // file → interned directory id + const ReceiverEvidence* recvEv; // JS/TS: a CommonJS require alias's module (moduleNames) +}; + +// is `path` the module `name` inside the package directory `pkgDir` (`pkg/name.py` or `pkg/name/__init__.py`)? +inline bool pythonSubmoduleOf( std::string_view path, std::string_view pkgDir, std::string_view name ) noexcept +{ + const std::string_view dir = includerDir( path ); + const std::string_view file = dir.size() < path.size() ? path.substr( dir.empty() ? 0 : dir.size() + 1 ) : path; + if( dir == pkgDir ) + { + return file.size() == name.size() + 3 && file.starts_with( name ) && file.ends_with( ".py" ); + } + return file == "__init__.py" && includerDir( dir ) == pkgDir && dir.ends_with( name ) + && ( dir.size() == name.size() || dir[ dir.size() - name.size() - 1 ] == '/' ); +} + +// the candidates of `cand` defined in file `f`, appended to `out` once each +inline void appendCandidatesIn( const IngestResult& ing, const std::vector& cand, std::uint32_t f, std::vector& out ) +{ + for( NodeId c : cand ) + { + if( ing.symbols[ c ].fileId == f && std::find( out.begin(), out.end(), c ) == out.end() ) + { + out.push_back( c ); + } + } +} + +// the file a Python import table binds `name` to in file `f` ("#name"), or kNoFile +inline std::uint32_t pythonImportFileOf( const HashMap& table, std::uint32_t f, std::string_view name, std::string& key ) +{ + key.clear(); Narrower::appendUint( key, f ); key.push_back( '#' ); key.append( name ); + const auto it = table.find( key ); + return it == table.end() ? kNoFile : it->second; +} + +inline constexpr int kPythonReexportHops = 4; // member-import hops provenBasesOf follows (a package re-exporting a re-export) + +// a Python `from m import N` names m's file `f`; when m defines no candidate it may RE-EXPORT the name (a package +// __init__.py's `from pkg._mod import N`): follow those member imports of `link`, a few hops, until a file defines one +inline void pythonReexportChain( const BaseEvidenceTables& t, const std::vector& cand, std::uint32_t f, std::string_view link, + std::vector& out ) +{ + std::string key; + for( int hop = 0; hop < kPythonReexportHops && f != kNoFile && out.empty(); ++hop ) + { + appendCandidatesIn( t.ing, cand, f, out ); + f = out.empty() ? pythonImportFileOf( t.extVeto.memberImportFile, f, link, key ) : f; + } +} + +// tier 2, Python: `from m import X` (X bare), `import pkg.mod as q` (q.X), `from pkg import q` (q.X: q is pkg's submodule, +// or a class q that pkg defines or re-exports and X sits in) +inline void pythonImportedBases( const BaseEvidenceTables& t, const Reference& r, const std::vector& cand, std::vector& out ) +{ + const std::string_view qual = r.memberRoot; // the base's written qualifier on an inherit ref ("" bare) + std::string key; + if( qual.empty() ) + { + pythonReexportChain( t, cand, pythonImportFileOf( t.extVeto.memberImportFile, r.fileId, r.calleeName, key ), r.calleeName, out ); + return; + } + if( qual.find( '.' ) != std::string_view::npos ) + { + return; // a dotted qualifier (`a.b.X`): not read + } + if( const std::uint32_t f = pythonImportFileOf( t.extVeto.importBindFile, r.fileId, qual, key ); f != kNoFile ) + { + pythonReexportChain( t, cand, f, r.calleeName, out ); + return; + } + const std::uint32_t pkg = pythonImportFileOf( t.extVeto.memberImportFile, r.fileId, qual, key ); + if( pkg == kNoFile ) + { + return; + } + const std::string_view pkgDir = includerDir( rootRelPath( t.ing, pkg ) ); + for( NodeId c : cand ) + { + if( pythonSubmoduleOf( rootRelPath( t.ing, t.ing.symbols[ c ].fileId ), pkgDir, qual ) && std::find( out.begin(), out.end(), c ) == out.end() ) + { + out.push_back( c ); + } + } + if( out.empty() ) + { + pythonReexportChain( t, cand, pkg, qual, out ); + } +} + +// tier 2, JS/TS: a bare name an ES named import pins, or a CommonJS `const { Node } = require('./a')` (a ModuleAlias, not +// an ES import) whose relative specifier names the candidate's module — the test a `Node.m()` member call is proven by +// (receiverevidence.h moduleNames). A qualified `ns.Base` is not read (a namespace import binds no table here). +inline void jsImportedBases( const BaseEvidenceTables& t, const Reference& r, const std::vector& cand, std::vector& out ) +{ + if( !r.memberRoot.empty() ) + { + return; + } + const auto pin = t.jsImports.targets.find( jsImportKey( r.fileId, r.calleeName ) ); + const bool listed = pin != t.jsImports.targets.end(); + if( listed && pin->second.outcome == JsImportOutcome::Pinned && std::find( cand.begin(), cand.end(), pin->second.node ) != cand.end() ) + { + out.push_back( pin->second.node ); + return; + } + for( NodeId c : cand ) + { + if( t.recvEv != nullptr && ( !listed || pin->second.outcome == JsImportOutcome::Unlisted ) && t.recvEv->moduleNames( r, r.calleeName, c ) ) + { + out.push_back( c ); + } + } +} + +// tier 2, every other language: a file the reference's file includes (C-family: transitively, as `#include` makes names +// visible) or imports (each language's resolved import edges) +inline void includedBases( const BaseEvidenceTables& t, const Reference& r, const std::vector& cand, std::vector& out ) +{ + if( r.lang == Lang::Cpp || r.lang == Lang::C || r.lang == Lang::ObjC ) + { + const std::vector* inc = r.fileId < t.fileIncludes.size() ? &t.fileIncludes[ r.fileId ] : nullptr; + for( NodeId c : cand ) + { + if( inc != nullptr && std::binary_search( inc->begin(), inc->end(), NodeId( t.ing.symbols[ c ].fileId ) ) ) + { + out.push_back( c ); + } + } + return; + } + if( r.fileId < t.includeAdj.size() ) + { + for( std::uint32_t f : t.includeAdj[ r.fileId ] ) + { + appendCandidatesIn( t.ing, cand, f, out ); + } + } +} + +// does directory `dir` end with the package `qual` written dotted (`a.b` → a directory ending `a/b`)? +inline bool dirSpellsPackage( std::string_view dir, std::string_view qual ) +{ + std::string path( qual ); + std::replace( path.begin(), path.end(), '.', '/' ); + return dir.size() >= path.size() && dir.ends_with( path ) && ( dir.size() == path.size() || dir[ dir.size() - path.size() - 1 ] == '/' ); +} + +// tier 3: the package — a Java/Kotlin/Go class in the reference's own directory (the Java/Kotlin layout convention; Go by +// definition), or a Java qualifier `a.b.Mid` spelling the candidate's directory +inline void packageBases( const BaseEvidenceTables& t, const Reference& r, const std::vector& cand, std::vector& out ) +{ + if( r.lang != Lang::Java && r.lang != Lang::Kotlin && r.lang != Lang::Go ) + { + return; + } + const std::string_view qual = r.memberRoot; + for( NodeId c : cand ) + { + const std::uint32_t f = t.ing.symbols[ c ].fileId; + const bool hit = qual.empty() ? t.fileDir[ f ] == t.fileDir[ r.fileId ] + : r.lang == Lang::Java && dirSpellsPackage( includerDir( rootRelPath( t.ing, f ) ), qual ); + if( hit ) + { + out.push_back( c ); + } + } +} + +// tier 1: the innermost candidate definition holding the same-file declaration the clause binds to (RawRef::baseBind) +inline NodeId locallyBoundBase( const IngestResult& ing, const Reference& r, const std::vector& cand ) noexcept +{ + const std::uint32_t at = r.baseBind - 1u; + NodeId best = kNoNode; + for( NodeId c : cand ) + { + const Symbol& s = ing.symbols[ c ]; + if( s.fileId == r.fileId && s.sigStartByte <= at && at < s.endByte && ( best == kNoNode || s.sigStartByte > ing.symbols[ best ].sigStartByte ) ) + { + best = c; + } + } + return best; +} + +// The candidates of `cand` (one inherit reference's class-like bases, already root/lang filtered) that the language's own +// lookup PROVES the clause names, into `out`: empty = nothing proves one (or the evidence named two). Tiers, first hit wins: +// 1. the same-file declaration the clause's enclosing scopes declare (RawRef::baseBind, ingest_relations.h) — when one +// answers, an import cannot outrank it, and a local declaration that is no candidate proves none; +// 2. an import (pythonImportedBases, jsImportedBases) or an include (includedBases); +// 3. the package (packageBases). +// What it does NOT prove: a C++ `using namespace`, a star import, a C# `using`, Swift's module-wide visibility, a PHP +// namespace across files — those edges stay ByName (hedged on --lego), never Proven. +inline void provenBasesOf( const BaseEvidenceTables& t, const Reference& r, const std::vector& cand, std::vector& out, + bool rubyScoped = false ) +{ + out.clear(); + if( rubyScoped ) + { + out = cand; // Ruby's constant lookup placed the base (resolve.h RubyBaseScope): every candidate is that one constant + return; + } + if( r.baseBind == 0xFFFFFFFFu ) + { + return; // a scope too large to read: the local lookup could not rule a shadowing declaration out + } + if( r.baseBind != 0 ) + { + if( const NodeId local = locallyBoundBase( t.ing, r, cand ); local != kNoNode ) + { + out.push_back( local ); + } + return; + } + if( r.lang == Lang::Python ) + { + pythonImportedBases( t, r, cand, out ); + } + else if( r.lang == Lang::JavaScript || r.lang == Lang::TypeScript ) + { + jsImportedBases( t, r, cand, out ); + } + else + { + includedBases( t, r, cand, out ); + } + if( out.empty() ) + { + packageBases( t, r, cand, out ); + } + if( out.size() > 1 ) + { + out.clear(); // the evidence named two same-named types: it proves neither + } +} + +// one implementors edge as drawn, with how its base was bound (merged into Graph::implementorBound after the dedup) +struct ImplementorBoundEdge +{ + NodeId base = kNoNode; + NodeId derived = kNoNode; + BaseBound bound = BaseBound::ByName; +}; + +// a candidate's binding given the reference's proven set: in it → Proven, an empty set → ByName, else Other +inline BaseBound boundOfCandidate( const std::vector& proven, NodeId c ) noexcept +{ + if( std::find( proven.begin(), proven.end(), c ) != proven.end() ) + { + return BaseBound::Proven; + } + return proven.empty() ? BaseBound::ByName : BaseBound::Other; +} + +// Graph::implementorBound, parallel to the deduped implementors lists: two references drawing one edge keep the +// stronger binding (BaseBound's order — a proven site proves the edge) +inline void fillImplementorBound( Graph& g, const std::vector& edges ) +{ + g.implementorBound.assign( g.implementors.size(), {} ); + for( std::size_t b = 0; b < g.implementors.size(); ++b ) + { + g.implementorBound[ b ].assign( g.implementors[ b ].size(), std::uint8_t( BaseBound::Other ) ); + } + for( const ImplementorBoundEdge& e : edges ) + { + const std::vector& kids = g.implementors[ e.base ]; + const auto pos = std::lower_bound( kids.begin(), kids.end(), e.derived ); + ASSUME( pos != kids.end() && *pos == e.derived, "every bound edge was pushed into implementors before the dedup" ); + std::uint8_t& slot = g.implementorBound[ e.base ][ std::size_t( pos - kids.begin() ) ]; + slot = std::min( slot, std::uint8_t( e.bound ) ); + } +} + inline void internDeclinedList( Graph& g, HashMap>& listsByHash, std::span cand ); // defined beside declinedCallsNaming, its one reader inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = nullptr, bool census = false ) { @@ -5603,6 +6249,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null PROFILE_SCOPE_DESCRIBE( "buildGraph/1d: byName (name -> def ids)" ); for( const Symbol& s : ing.symbols ) { + if( s.kind == SymKind::Section && s.lang == Lang::Ruby ) + { + continue; // definitions-only (#339): a Ruby schema column answers --uses/--grep/--whereis as + // a def, but admits NO call edge — the byName table feeds both call binding and the + // common-name damping, so an untyped `response.code` must not become a column's + // caller, and 200 `name` columns must not damp a model's real `def name`. + // Mirrors contextratio.h's isMeasurableKind treatment of Sections. Edges may come + // later behind an evidence rule (receiver proven to be the owning model). + } byName[ s.name ].push_back( s.id ); } } @@ -6052,8 +6707,439 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } return ( ref.lang == Lang::TypeScript || ref.lang == Lang::JavaScript ) && rootRelPath( ing, cs.fileId ).ends_with( ".d.ts" ); }; + // StdMemberGate's receiver exception. A C++ receiver may carry a WRITTEN type the rules above could not map to a class — a + // local or parameter declared `SmallVec` (an alias), a member field Rule 2b has a type for (`items_`, + // `this->items_`), one field hop off a typed root (`b.items_` with `Box& b`), a namespace-scope, file-static or extern + // variable (`ns::List g_list;`), or a `->` through a standard smart pointer or optional whose argument is a class + // (`std::unique_ptr up; up->size()`). When such a type's inheritance cone (its alias targets and bases included, + // chaUp) holds one of the candidates the ladder reached, the call keeps the ladder's hedge: the type may be that in-tree + // class. A `->` through a type whose `->` the declaration does not resolve — an in-tree class with its own `operator->` + // (`SP& p; p->size()`), or a class the tree does not define (`QSharedPointer`, a raw pointer cannot be + // told from such a wrapper by the type's text) — keeps it too. A written type whose cone holds none of them (`HashMap + // m; m.find( k )` reaching UnionFind::find by name, `std::unique_ptr p; p->size()`) is no evidence for them, + // and the gate applies. Every type is one the parser recorded for the declaration (Binding::typeName, and importedName, the + // whole qualified text with its template arguments); a declared local hides a same-named field, and a field hides a + // namespace-scope variable, as C++'s own lookup orders them. + std::string stdRecvKey; // reused "#" buffer + Reference stdFieldProbe; // reused: fieldEntryAt reads only recvVar, the member name it looks up + // the declaration texts the exception reads beyond Rule 2's tables: each C++ definition's QUALIFIED declarations, std ones + // included ("#" -> binding indices; Rule 2's tables keep "" for a std one), and every namespace-scope + // declaration (var -> binding indices). Empty on a tree with no C++ definition. + HashMap> stdQualifiedDecls, stdNamespaceDecls; + const bool anyCppDefinition = std::any_of( ing.symbols.begin(), ing.symbols.end(), []( const Symbol& s ) { return s.lang == Lang::Cpp; } ); + if( anyCppDefinition ) + { + for( std::uint32_t i = 0; i < std::uint32_t( ing.bindings.size() ); ++i ) + { + const Binding& b = ing.bindings[ i ]; + if( ( b.kind != LocalBindKind::Type && b.kind != LocalBindKind::ParamType ) || b.var.empty() || b.typeName.empty() || b.isFromAssignment ) + { + continue; // an assignment declares nothing (resolve.h assignmentNamesNoClass) + } + if( b.fromSymbol == kNoNode ) + { + stdNamespaceDecls[ b.var ].push_back( i ); + } + else if( !b.importedName.empty() && ing.symbols[ b.fromSymbol ].lang == Lang::Cpp ) + { + stdRecvKey.clear(); Narrower::appendUint( stdRecvKey, b.fromSymbol ); stdRecvKey.push_back( '#' ); stdRecvKey.append( b.var ); + stdQualifiedDecls[ stdRecvKey ].push_back( i ); + } + } + } + const rw::SmallVec* const arrowOperators = [ & ]() -> const rw::SmallVec* + { + const auto it = byName.find( std::string( "operator->" ) ); + return it == byName.end() ? nullptr : &it->second; + }(); + const auto fieldTypeIn = [ & ]( const std::string& className, std::string_view member ) -> const FlatRecvType* + { + stdFieldProbe.recvVar.assign( member ); + return narrower.fieldEntryAt( stdFieldProbe, className, { fieldNarrow.fieldTypeByClass, memberFields, chaUp } ); + }; + // every C/C++ type alias record (`typedef std::shared_ptr ListSP;` and `using ListSP2 = std::shared_ptr;` + // alike, ingest_relations.h captureTypeAlias): alias name -> its recorded targets (the reference indices). A typedef is ALSO + // a Struct symbol at its name (so classNames holds it) and a `using` is not; arrowIsOpaque reads both through these records. + HashMap> typeAliasTargets; + if( anyCppDefinition ) + { + for( std::uint32_t i = 0; i < std::uint32_t( ing.references.size() ); ++i ) + { + if( const Reference& r = ing.references[ i ]; isTypeAliasRecord( r ) && !r.recvVar.empty() && !r.calleeName.empty() ) + { + typeAliasTargets[ r.recvVar ].push_back( i ); + } + } + } + // a `->` through a written non-std type: may it reach a class the declaration does not name? An ALIAS answers by its + // recorded target (one rule for typedef and using): a std target is opaque when it is a smart pointer or optional + // (kCppStdArrowTemplates — the record keeps no template argument, so the pointee is unrecorded) and is a raw pointer to a + // std type otherwise (`typedef std::vector IntVec; IntVec* p; p->size()`); a non-std target answers as that class + // does (an in-tree class with its own `operator->`, a class the tree does not define). Any target that is opaque makes + // the alias opaque. Aliases of aliases follow to a fixed depth. + const auto arrowOpaqueAt = [ & ]( const auto& self, std::string_view type, int depth ) -> bool + { + const std::string key( type ); + bool aliased = false; + if( const auto ait = typeAliasTargets.find( key ); ait != typeAliasTargets.end() && depth < 8 ) + { + aliased = true; + for( const std::uint32_t i : ait->second ) + { + const Reference& r = ing.references[ i ]; + if( fieldTypeWrittenInStd( r ) ? StdMemberGate::isStdArrowTemplate( r.calleeName ) : ( r.calleeName != type && self( self, r.calleeName, depth + 1 ) ) ) + { + return true; + } + } + } + if( !aliased && !classNames.contains( key ) && !chaUp.contains( key ) ) + { + return true; // a class the tree does not define: its `->` is unknown + } + if( arrowOperators == nullptr ) + { + return false; + } + const ChaConeMemo::Cone cone = chaCones.coneFor( type ); + return std::any_of( arrowOperators->begin(), arrowOperators->end(), [ & ]( NodeId c ) { return chaCones.contains( cone, type, ing.symbols[ c ].scope ); } ); + }; + const auto arrowIsOpaque = [ & ]( std::string_view type ) -> bool { return arrowOpaqueAt( arrowOpaqueAt, type, 0 ); }; + // one written type into the receiver facts: the type itself, and for a `->` call its pointee or its opacity + const auto addWritten = [ & ]( StdMemberGate::Receiver& out, std::string_view typeName, std::string_view qualified, bool viaArrow ) + { + if( !qualified.empty() && namesStdType( qualified ) ) + { + const std::string_view pointee = viaArrow ? StdMemberGate::stdArrowPointee( qualified ) : std::string_view{}; + if( !pointee.empty() ) + { + out.types.push_back( pointee ); + } + return; // a std type's own members are never in-tree + } + if( typeName.empty() ) + { + return; + } + out.types.push_back( typeName ); + out.opaqueArrow = out.opaqueArrow || ( viaArrow && arrowIsOpaque( typeName ) ); + }; + // a field's recorded type: a std smart pointer's pointee is reached through `->` alone (`w_.reset()` on + // `std::unique_ptr w_;` is the pointer's own member), and a pointee is the class itself, never a wrapper of it + const auto addField = [ & ]( StdMemberGate::Receiver& out, const FlatRecvType* t, bool viaArrow ) + { + if( t == nullptr || t->type.empty() || ( t->arrowOnly && !viaArrow ) ) + { + return; + } + out.types.push_back( t->type ); + out.opaqueArrow = out.opaqueArrow || ( viaArrow && !t->arrowOnly && arrowIsOpaque( t->type ) ); + }; + const auto namedKey = [ & ]( NodeId from, std::string_view var ) -> const std::string& + { + stdRecvKey.clear(); Narrower::appendUint( stdRecvKey, from ); stdRecvKey.push_back( '#' ); stdRecvKey.append( var ); + return stdRecvKey; + }; + StdMemberGate::Receiver stdRecvRoot; // reused: a FieldOfVar root's written classes + // a named receiver's declarations, in C++'s lookup order: its in-scope local or parameter declaration (Rule 2's own answer, + // and every qualified declaration of the name in the definition), else the caller's class's field, else a namespace-scope + // variable of that name — declared in the caller's file or a file it includes, else anywhere in the tree. onWritten( type + // name, qualified text ) takes each declaration's recorded type; onField( class, member ) takes the field and answers whether + // the class declares that member typed (a typed field hides the namespace-scope variable). + const auto forNamedDecls = [ & ]( const Reference& ref, const std::string& callerScope, const auto& onWritten, const auto& onField ) + { + const std::string_view var = ref.recvVar; // a NamedVar's name, or a FieldOfVar's root + onWritten( narrower.recvVarTypeName( ref ), std::string_view{} ); + if( const auto qit = stdQualifiedDecls.find( namedKey( ref.fromSymbol, var ) ); qit != stdQualifiedDecls.end() ) + { + for( const std::uint32_t i : qit->second ) + { + onWritten( std::string_view{}, std::string_view( ing.bindings[ i ].importedName ) ); // a non-std qualified one is Rule 2's answer above + } + } + if( const auto lit = fieldNarrow.localNameSet.find( namedKey( ref.fromSymbol, var ) ); lit != fieldNarrow.localNameSet.end() && ( lit->second & kLocalNameDeclared ) != 0 ) + { + return; // a declared local hides a field and a namespace-scope variable + } + if( onField( callerScope, var ) ) + { + return; + } + stdRecvKey.assign( callerScope ).push_back( '#' ); + stdRecvKey.append( var ); + const auto nit = memberFields.contains( stdRecvKey ) ? stdNamespaceDecls.end() : stdNamespaceDecls.find( std::string( var ) ); + if( nit == stdNamespaceDecls.end() ) + { + return; // no namespace-scope declaration, or a field of the caller's class hides it + } + const auto visible = [ & ]( std::uint32_t i ) + { + const std::uint32_t f = ing.bindings[ i ].fileId; + return f == ref.fileId || ( ref.fileId < fileIncludes.size() && std::binary_search( fileIncludes[ ref.fileId ].begin(), fileIncludes[ ref.fileId ].end(), f ) ); + }; + const bool anyVisible = std::any_of( nit->second.begin(), nit->second.end(), visible ); + for( const std::uint32_t i : nit->second ) + { + if( !anyVisible || visible( i ) ) + { + onWritten( std::string_view( ing.bindings[ i ].typeName ), std::string_view( ing.bindings[ i ].importedName ) ); + } + } + }; + const auto addNamed = [ & ]( StdMemberGate::Receiver& out, const Reference& ref, const std::string& callerScope, bool viaArrow ) + { + forNamedDecls( ref, callerScope, [ & ]( std::string_view typeName, std::string_view qualified ) { addWritten( out, typeName, qualified, viaArrow ); }, + [ & ]( const std::string& cls, std::string_view member ) + { + const FlatRecvType* field = fieldTypeIn( cls, member ); + addField( out, field, viaArrow ); + return field != nullptr; + } ); + }; + // ELEMENT receivers (parser version 166): a call on ONE element access of a one-hop container — `m[ k ].empty()`, `v.at( i ) + // .size()`, `this->items_[ i ].size()`, `rule->bindings_[ k ].empty()` (memberPath = model.h kCppElementMark + the container's + // shape). The container's recorded written text names its element type (stdElementType): a local's, a + // parameter's or a namespace-scope variable's qualified text (Binding::importedName), a member field written as a std + // container (its composeRel "elem" record, stdElemFields), and a type alias's recorded target text, followed alias to alias + // (`typedef std::map Bindings; Bindings bindings_;`). The element class joins the receiver's written + // types, so the call keeps the ladder's hedge exactly when that class's cone holds a candidate — `std::vector`'s + // element names no class and is declined as before. + HashMap> stdElemFields; // "Class#field" -> its "elem" record indices + if( anyCppDefinition ) + { + std::string key; + for( std::uint32_t i = 0; i < std::uint32_t( ing.references.size() ); ++i ) + { + const Reference& r = ing.references[ i ]; + if( r.isCompose && r.composeRel == "elem" && r.fromSymbol != kNoNode && !r.recvVar.empty() && !r.memberPath.empty() ) + { + key.assign( ing.symbols[ r.fromSymbol ].name ).push_back( '#' ); + key.append( r.recvVar ); + stdElemFields[ key ].push_back( i ); + } + } + } + const auto addElementOfText = [ & ]( StdMemberGate::Receiver& out, std::string_view written ) + { + if( const std::string_view element = stdElementType( written ); !element.empty() ) + { + out.types.push_back( element ); + } + }; + const auto elementOfAliasAt = [ & ]( const auto& self, StdMemberGate::Receiver& out, std::string_view type, int depth ) -> void + { + const auto it = type.empty() || depth >= 8 ? typeAliasTargets.end() : typeAliasTargets.find( std::string( type ) ); + if( it == typeAliasTargets.end() ) + { + return; + } + for( const std::uint32_t i : it->second ) + { + const Reference& r = ing.references[ i ]; + if( !r.memberPath.empty() ) + { + addElementOfText( out, r.memberPath ); + } + else if( !fieldTypeWrittenInStd( r ) && r.calleeName != type ) + { + self( self, out, r.calleeName, depth + 1 ); + } + } + }; + const auto addElementOfAlias = [ & ]( StdMemberGate::Receiver& out, std::string_view type ) { elementOfAliasAt( elementOfAliasAt, out, type, 0 ); }; + // a field of `cls` (final segment, as Rule 2b keys it) as a container: its "elem" record, else its recorded type as an alias. + // true ⇒ the class declares that member with a record of either kind + const auto addElementOfField = [ & ]( StdMemberGate::Receiver& out, std::string_view cls, std::string_view member ) -> bool + { + const std::size_t cut = cls.rfind( "::" ); + stdRecvKey.assign( cut == std::string_view::npos ? cls : cls.substr( cut + 2 ) ).push_back( '#' ); + stdRecvKey.append( member ); + const auto eit = stdElemFields.find( stdRecvKey ); + if( eit != stdElemFields.end() ) + { + for( const std::uint32_t i : eit->second ) + { + addElementOfText( out, ing.references[ i ].memberPath ); + } + } + const FlatRecvType* field = fieldTypeIn( std::string( cls ), member ); + if( field != nullptr && !field->arrowOnly ) + { + addElementOfAlias( out, field->type ); + } + return field != nullptr || eit != stdElemFields.end(); + }; + Reference stdElemProbe; // reused: the container root of an element receiver, as a NamedVar receiver of the same call + const auto addElementReceiver = [ & ]( StdMemberGate::Receiver& out, const Reference& ref, const std::string& callerScope ) + { + const std::string_view shape = std::string_view( ref.memberPath ).substr( kCppElementMark.size() ); + const std::size_t dot = shape.find( '.' ); + if( shape.starts_with( "this." ) ) + { + addElementOfField( out, callerScope, shape.substr( 5 ) ); // `this->items_[ i ].size()` + return; + } + stdElemProbe.fileId = ref.fileId; + stdElemProbe.fromSymbol = ref.fromSymbol; + stdElemProbe.lang = ref.lang; + stdElemProbe.line = ref.line; + stdElemProbe.startByte = ref.startByte; + stdElemProbe.role = ref.role; + stdElemProbe.recv = RecvKind::NamedVar; + stdElemProbe.recvVar.assign( shape.substr( 0, dot ) ); + if( dot == std::string_view::npos ) + { + // `m[ k ].empty()` / `bindings_[ k ].empty()`: the named container's declarations, as a named receiver's + forNamedDecls( stdElemProbe, callerScope, + [ & ]( std::string_view typeName, std::string_view qualified ) + { + addElementOfAlias( out, typeName ); + addElementOfText( out, qualified ); + }, + [ & ]( const std::string& cls, std::string_view member ) { return addElementOfField( out, cls, member ); } ); + return; + } + // `rule->bindings_[ k ].empty()` / `r.vec_[ 0 ].empty()`: the root's written classes (a smart pointer's pointee + // included, as a FieldOfVar root), then each one's field as a container + stdRecvRoot.clear(); + addNamed( stdRecvRoot, stdElemProbe, callerScope, /*viaArrow=*/true ); + for( const std::string_view r : stdRecvRoot.types ) + { + addElementOfField( out, r, shape.substr( dot + 1 ) ); + } + }; + const auto stdReceiverOf = [ & ]( const Reference& ref, StdMemberGate::Receiver& out ) + { + out.clear(); + if( ref.lang != Lang::Cpp || ref.fromSymbol == kNoNode ) + { + return; + } + const std::string& callerScope = ing.symbols[ ref.fromSymbol ].scope; + if( ref.recv == RecvKind::NamedVar && !ref.recvVar.empty() ) + { + addNamed( out, ref, callerScope, ref.viaArrow ); + } + else if( ref.recv == RecvKind::FieldOfThis && !ref.fieldName.empty() ) + { + addField( out, fieldTypeIn( callerScope, ref.fieldName ), ref.viaArrow ); // `this->items_.m()` + } + else if( ref.recv == RecvKind::FieldOfVar && ref.recvVar.empty() && !ref.memberCall && ref.memberPath.size() > kCppElementMark.size() + && ref.memberPath.starts_with( kCppElementMark ) ) + { + addElementReceiver( out, ref, callerScope ); // `m[ k ].empty()`: the container's element type + } + else if( ref.recv == RecvKind::FieldOfVar && !ref.recvVar.empty() && !ref.fieldName.empty() ) + { + // one hop off a typed root (`b.items_.m()`, `up->items_.m()`): the root's written classes, then each one's field. + // A root written as a std smart pointer or optional reaches its fields through `->` alone, so its pointee is read. + stdRecvRoot.clear(); + addNamed( stdRecvRoot, ref, callerScope, /*viaArrow=*/true ); + for( const std::string_view r : stdRecvRoot.types ) + { + addField( out, fieldTypeIn( std::string( r ), ref.fieldName ), ref.viaArrow ); + } + } + }; + StdMemberGate::Receiver stdRecv; // reused per call: the receiver facts of a call the gate is about + const auto writtenTypeReaches = [ & ]( std::string_view type, NodeId from, std::span ids ) -> bool + { + if( type.empty() ) + { + return false; + } + const ChaConeMemo::Cone cone = chaCones.coneFor( type ); + return std::any_of( ids.begin(), ids.end(), [ & ]( NodeId c ) { return c != from && chaCones.contains( cone, type, ing.symbols[ c ].scope ); } ); + }; + // the exception's answer for one id list: keep the ladder's hedge? + const auto stdReceiverKeeps = [ & ]( const StdMemberGate::Receiver& recv, NodeId from, std::span ids ) -> bool + { + return recv.opaqueArrow || std::any_of( recv.types.begin(), recv.types.end(), [ & ]( std::string_view t ) { return writtenTypeReaches( t, from, ids ); } ); + }; rw::SmallVec rubyReachable; // reused per-call buffer: the candidates a Ruby call to self can reach (RubySelfReach) + // ---- the resolve loop's dense per-candidate views (cli-perf-068) --------------------------------------------------- + // The loop below walks a name's WHOLE candidate list several times per call — the language filter, the namespace gate, + // the same-file and same-directory tiers — and each step read one field out of a Symbol at a random id. That is cheap + // for a name with two definitions and was the resolve loop's cost for a name with hundreds: since every module-scope + // call got an owner (24bb6e5d), the test scripts' top-level `ok "…"` / `no "…"` calls (~650 same-named shell + // definitions each, ~28k calls on this repository) reached those walks, and the loop went 22 -> 79 ms on that commit + // alone. symLang/symKind are copies of the two Symbol fields those walks read (symFileId, above, is the third), so + // each walk reads a dense array in id order instead: same values, same order, same answer. `nameHasFnLocalDef` is + // the one precomputed fact reachableByName needs to say "nothing to set aside": a name none of whose definitions is + // function-local cannot yield (localDefOutOfReach is false for every candidate, and every list the loop hands + // reachableByName is a subset of the name's byName list), so its walk — a Symbol load plus a binary search per + // candidate — is skipped with the answer it would have returned. Keyed by the byName entry's address: nothing + // inserts into or erases from byName past its construction, so the address is stable for the loop. + std::vector symLang( N ); + std::vector symKind( N ); + ankerl::unordered_dense::set*> nameHasFnLocalDef; + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/2l: dense candidate views + function-local name set" ); + for( std::size_t symIndex = 0; symIndex < N; ++symIndex ) + { + symLang[ symIndex ] = ing.symbols[ symIndex ].lang; + symKind[ symIndex ] = ing.symbols[ symIndex ].kind; + } + if( !ing.fnLocalScopes.empty() ) + { + for( const auto& [ name, ids ] : byName ) + { + if( std::any_of( ids.begin(), ids.end(), [ & ]( NodeId c ) { return ing.symbols[ c ].fnLocal != 0; } ) ) + { + nameHasFnLocalDef.insert( &ids ); + } + } + } + } + ENSURES( symLang.size() == N && symKind.size() == N && symFileId.size() == N, "the dense views cover every symbol id the loop can hold" ); + + // ---- the per-(name, language) candidate memo: work that is invariant across the calls to one name --------------- + // For a name with many definitions, three walks over its list decide the same thing for every call written in the + // same language: which candidates survive the language filter (the name-based fallback's admission loop), whether + // the namespace gate removes any of them for the call's role, and how the survivors fall by file. The resolve loop + // used to redo all three per call — 650 candidates for every one of ~28k module-scope `ok` / `no` calls in this + // repository's test scripts (the cand-build stage alone was the loop's largest). This memo computes them ONCE per + // (byName entry, caller language), on the first call that needs them, and every later call copies the survivors and + // finds its same-file tier by binary search over the file-sorted list. Only the unnarrowed whole-name path reads it + // (the list handed in IS the byName entry, single root, nothing in cand yet), only for lists of kLangCandMemoMin or + // more, and `candMemo` is dropped the moment anything but a removal could have touched cand (the Ruby side cut), or + // any removal did (its size no longer matches). Same survivors, same order, same tiers: the answer is unchanged. + struct LangCandidates + { + std::vector ids; // the name's candidates the language filter admits, in list order + std::uint32_t nsPassRoles = 0; // bit k: every one passes namespaceCompatible( RefRole( k ), … ) + bool sortedByFile = false; // ids is non-decreasing in symFileId, so one file's ids are one run + }; + struct NameLangCandidates + { + std::uint32_t filledLangs = 0; // bit k: byLang[ k ] is computed + std::array byLang; + }; + static_assert( kLangCount <= 32 && kRefRoleCount <= 32, "the memo's language and role sets are 32-bit masks" ); + constexpr std::size_t kLangCandMemoMin = 16; // below this the walks are cheaper than the memo lookup + HashMap*, NameLangCandidates> langCandMemo; + langCandMemo.reserve( 1024 ); + const auto fillLangCandidates = [ & ]( const rw::SmallVec& ids, Lang lang, LangCandidates& out ) + { + for( NodeId c : ids ) + { + if( langCompatible( symLang[ c ], lang ) ) + { + out.ids.push_back( c ); + } + } + for( std::size_t roleIndex = 0; roleIndex < kRefRoleCount; ++roleIndex ) + { + const RefRole role = static_cast( roleIndex ); + if( std::all_of( out.ids.begin(), out.ids.end(), [ & ]( NodeId c ) { return namespaceCompatible( role, symKind[ c ] ); } ) ) + { + out.nsPassRoles |= 1u << roleIndex; + } + } + out.sortedByFile = std::is_sorted( out.ids.begin(), out.ids.end(), [ & ]( NodeId a, NodeId b ) { return symFileId[ a ] < symFileId[ b ]; } ); + }; + // ---- census arming + the ORACLE side (eval-only; src/pincensus.h) ------------------------------ // The oracle rows are a straight transcription of the overlay's own (from, calleeName) → target table // into the census's id space — the same table graph.h consults below, so the two sides of the join @@ -6112,6 +7198,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null cand.clear(); tier.clear(); + const LangCandidates* candMemo = nullptr; // set when cand is exactly a memoized list (see langCandMemo) float tierConf = 1.0f; // tier 1: same file (default; overridden below) // census bookkeeping (inert unless armed): which narrowing stages actually FIRED on this site, and // how wide the tier was when it reached the locality tie-break. Read only at the emission point. @@ -6269,7 +7356,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null if( !scipPinned && r.lang != Lang::Elixir && !r.qualifier.empty() ) { appendCanonicalCandidates( cand, qkey, r, CanonicalScopes { canonByName, canonFamilyByName, specializationsWithBases, narrower, chaUp, ing.symbols }, - [ & ]( NodeId c ) { return langCompatible( ing.symbols[c].lang, r.lang ) && sameRoot( c, r.fileId ); } ); + [ & ]( NodeId c ) { return langCompatible( symLang[ c ], r.lang ) && sameRoot( c, r.fileId ); } ); canonical = !cand.empty(); } // ── L3 fn-pointer/callback binding resolve — BEFORE Rule 1, because a local variable shadows a @@ -6578,7 +7665,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::size_t found = 0; for( NodeId c : it->second ) { - if( langCompatible( ing.symbols[c].lang, r.lang ) && symFileId[c] == aliasFile ) + if( langCompatible( symLang[ c ], r.lang ) && symFileId[c] == aliasFile ) { only = c; if( ++found > 1 ) @@ -6627,7 +7714,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // whichever same-named def competes, and yielding would move them from external= into declined=, so such a call // resolves exactly as it did before local defs had bodies. const bool localsYield = !scipPinned && !canonical && !narrowed && !builtinGated && it != byName.end() && !ing.fnLocalScopes.empty() - && reachableByName( ing, *baseIds, r, reachScratch ) + && nameHasFnLocalDef.contains( &it->second ) && reachableByName( ing, *baseIds, r, reachScratch ) && !( r.role == RefRole::Call && r.qualifier.empty() && bindingTier.empty() && externalVeto.isExternalBound( r ) ); const rw::SmallVec* nameIds = localsYield ? &reachScratch : baseIds; // FE-B: for a MEMBER call the include graph says which file the caller imports, never what its receiver is — the @@ -6675,7 +7762,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::vector& into = ( hedgeEligible && ( isMemberCallRef( r ) || implicitReceiverLang( r.lang ) ) ) ? memberRule3 : cand; for( NodeId c : rule3Out ) { - if( langCompatible( ing.symbols[c].lang, r.lang ) ) + if( langCompatible( symLang[ c ], r.lang ) ) { into.push_back( c ); } @@ -6709,11 +7796,26 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null continue; } } + else if( !multiRoot && nameIds == baseIds && baseIds == &it->second && cand.empty() && nameIds->size() >= kLangCandMemoMin ) + { + // the whole-name path for a name with many definitions: the language filter's survivors, memoized + // per (name, language) — sameRoot is constant-true on a single root, so they are this loop's own + NameLangCandidates& memo = langCandMemo[ nameIds ]; + const std::size_t langIdx = static_cast( r.lang ); + ASSUME( langIdx < kLangCount, "a reference's language is a Lang enumerator" ); + if( ( memo.filledLangs >> langIdx & 1u ) == 0 ) + { + fillLangCandidates( *nameIds, r.lang, memo.byLang[ langIdx ] ); + memo.filledLangs |= 1u << langIdx; + } + candMemo = &memo.byLang[ langIdx ]; + cand.assign( candMemo->ids.begin(), candMemo->ids.end() ); + } else { for( NodeId c : *nameIds ) { - if( langCompatible( ing.symbols[c].lang, r.lang ) && sameRoot( c, r.fileId ) ) + if( langCompatible( symLang[ c ], r.lang ) && sameRoot( c, r.fileId ) ) { cand.push_back( c ); // same lang, or ObjC↔C++ bridge; same ROOT (A10) } @@ -6721,7 +7823,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null for( std::size_t i = 0; localsYield && cand.empty() && i < baseIds->size(); ++i ) // nothing reachable survived: the whole list { const NodeId c = ( *baseIds )[ i ]; - if( langCompatible( ing.symbols[c].lang, r.lang ) && sameRoot( c, r.fileId ) ) + if( langCompatible( symLang[ c ], r.lang ) && sameRoot( c, r.fileId ) ) { cand.push_back( c ); } @@ -6735,7 +7837,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { for( NodeId c : *baseIds ) { - if( !langCompatible( ing.symbols[c].lang, r.lang ) || sameRoot( c, r.fileId ) ) + if( !langCompatible( symLang[ c ], r.lang ) || sameRoot( c, r.fileId ) ) { continue; } @@ -6759,6 +7861,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null : RubyCut::Unchanged; cut != RubyCut::Unchanged ) { + candMemo = nullptr; // cand is the side cut's list now, not the memoized one cand.swap( filtScratch ); if( cut == RubyCut::Declined ) { @@ -6782,12 +7885,12 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // uniquely a macro by construction); see namespaceCompatible's comment for the derivation and // test/nsfiltercheck.sh for the gate that keeps it that way. It is here because it is the seam a // future round would edit, and a silent edit is the failure mode the gate exists to catch. - if( !cand.empty() ) + if( !cand.empty() && !( candMemo != nullptr && ( candMemo->nsPassRoles >> static_cast( r.role ) & 1u ) != 0 ) ) { std::size_t keepCount = 0; for( std::size_t ci = 0; ci < cand.size(); ++ci ) { - if( namespaceCompatible( r.role, ing.symbols[ cand[ci] ].kind ) ) + if( namespaceCompatible( r.role, symKind[ cand[ci] ] ) ) { cand[ keepCount++ ] = cand[ci]; } @@ -6815,6 +7918,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null disposition = vetoExternal( r ); // nothing inside std answers → external=, counted External continue; } + if( candMemo != nullptr && cand.size() != candMemo->ids.size() ) + { + candMemo = nullptr; // a gate or guard above removed a candidate: cand is no longer the memoized list + } // ---- FE-B: PROVEN or NAME-ONLY (receiverevidence.h, point 2). A call no rule above answered binds by name; the // candidates the language's own lookup proves are its answer (the rest never were candidates of that lookup), and @@ -6856,6 +7963,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null if( !provedScratch.empty() ) { cand.swap( provedScratch ); + candMemo = nullptr; // cand is Rule 3's proven list now, not the memoized one: the ladder may not read it as that file-sorted run narrowed = true; } } @@ -6893,6 +8001,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } } } + // StdMemberGate: a NAME-ONLY C++ member call named like a standard container or string member — its table index, else + // kNotStd — and what its receiver's written types say (stdReceiverOf). Decided once here: the ladder's own + // tier-3 decline and the post-filter below both read them. + const std::uint16_t stdIndex = nameOnly ? StdMemberGate::tableIndexOf( r ) : StdMemberGate::kNotStd; + stdRecv.clear(); + if( stdIndex != StdMemberGate::kNotStd ) + { + stdReceiverOf( r, stdRecv ); + } // ---- tier ladder (the name-based fallback) — SKIPPED when SCIP pinned this site, and for Rule 2's class-identity CLAIM (a type fact, not a locality guess) const bool identityClaim = narrowed && narrower.identityClaimFor( r ); if( !scipPinned && ( r.lang == Lang::Elixir || identityClaim ) ) { tier = cand; } @@ -6976,6 +8093,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { ++g.declinedOut[ r.fromSymbol ]; internDeclinedList( g, declinedListsByHash, cand ); + if( stdIndex != StdMemberGate::kNotStd && !stdReceiverKeeps( stdRecv, r.fromSymbol, cand ) ) + { + g.stdMemberDeclines.emplace_back( r.fromSymbol, stdIndex ); // the line names it too + } disposition = CallDisposition::Declined; continue; } @@ -6983,11 +8104,23 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } else { - for( NodeId c : cand ) + if( candMemo != nullptr && candMemo->sortedByFile ) + { + // the memoized list is file-sorted, so this file's candidates are one run, in list order + const auto fileLess = [ & ]( NodeId c, std::uint32_t fileId ) noexcept { return symFileId[ c ] < fileId; }; + const auto lessFile = [ & ]( std::uint32_t fileId, NodeId c ) noexcept { return fileId < symFileId[ c ]; }; + const auto runBegin = std::lower_bound( cand.begin(), cand.end(), r.fileId, fileLess ); + const auto runEnd = std::upper_bound( runBegin, cand.end(), r.fileId, lessFile ); + tier.insert( tier.end(), runBegin, runEnd ); + } + else { - if( ing.symbols[c].fileId == r.fileId ) + for( NodeId c : cand ) { - tier.push_back( c ); // tier 1: same file + if( symFileId[ c ] == r.fileId ) + { + tier.push_back( c ); // tier 1: same file + } } } if( tier.empty() ) // tier 2: same directory @@ -6996,7 +8129,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const std::uint32_t rdir = fileDir[ r.fileId ]; for( NodeId c : cand ) { - if( fileDir[ing.symbols[c].fileId] == rdir ) + if( fileDir[ symFileId[ c ] ] == rdir ) { tier.push_back( c ); } @@ -7271,6 +8404,48 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } } + // ---- the C++ standard-member gate (StdMemberGate above) — it only ever removes an edge ------------------------- + // The ladder decided this name-only call as it decides every other; when the name is a standard container or string + // member and no written non-std type of the receiver reaches them, the in-tree definitions it reached by NAME are declined + // instead of bound: the standard library's own member is the call's other candidate. A self-only tier is left to the + // Self exit, exactly as the builtin-method gate leaves it. + if( stdIndex != StdMemberGate::kNotStd && !bindingPinned && !identityClaim && !stdReceiverKeeps( stdRecv, r.fromSymbol, tier ) ) + { + ASSUME( stdIndex < std::size( externalnames::kCppStdMemberNames ), "tableIndexOf returns a table index or kNotStd" ); + gateRefused.clear(); + for( const NodeId c : tier ) + { + if( c != r.fromSymbol ) + { + gateRefused.push_back( c ); // what the edge loop below would bind: what the call could have meant + } + } + std::size_t byName = 0; // the name's in-tree candidates the ladder chose among (cand and the included file's), the caller excluded + for( const NodeId c : cand ) + { + byName += ( c != r.fromSymbol ) ? 1u : 0u; + } + for( const NodeId c : memberRule3 ) + { + byName += ( c != r.fromSymbol && std::find( cand.begin(), cand.end(), c ) == cand.end() ) ? 1u : 0u; + } + if( !gateRefused.empty() && byName >= StdMemberGate::kMinInTreeCandidates ) + { + std::sort( gateRefused.begin(), gateRefused.end() ); // one sequence per set, as internDeclinedList keys it + ++g.declinedOut[ r.fromSymbol ]; + ++g.stdMemberDeclinedCalls; + internDeclinedList( g, declinedListsByHash, gateRefused ); + g.gateDeclinedTarget.resize( N, 0 ); // no-op after the first decline: a definition a declined call could have meant is not dead + for( const NodeId c : gateRefused ) + { + g.gateDeclinedTarget[ c ] = 1; + } + g.stdMemberDeclines.emplace_back( r.fromSymbol, stdIndex ); + disposition = CallDisposition::Declined; + continue; + } + } + // ambiguity clue (S6-C): count when, AFTER all narrowing (canonical / Rule-1 / locality), the call STILL // resolves to >1 in-repo target — every one of which receives a 1/k-split edge below — excluding a // self-loop. HONESTY INVARIANT: the amb count must reflect EXACTLY the multi-way pick the edge emission @@ -7426,6 +8601,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } disposition = CallDisposition::Bound; } + std::sort( g.stdMemberDeclines.begin(), g.stdMemberDeclines.end() ); // grouped by caller: stdMemberCallsMadeBy's equal_range // A reference that left the loop naming no disposition is a resolver bug behind a correct-looking map: the // edges are right and the census's conservation line is not. Every plain build says so, census or not. if( g.callDispositions[ std::size_t( CallDisposition::Unaccounted ) ] > 0 ) @@ -7541,6 +8717,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const auto isClassLike = []( SymKind k ) noexcept { return namespaceCompatible( RefRole::Extends, k ); }; std::vector baseCand; // one inheritance reference's base candidates, reused across references + std::vector baseProven; // … and the one of them the language's lookup proves (provenBasesOf), reused + std::vector boundEdges; // every edge drawn below with how it was bound; merged after the dedup + const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv }; { PROFILE_SCOPE_DESCRIBE( "buildGraph/5: inheritance edges" ); for( const Reference& r : ing.references ) @@ -7608,7 +8787,14 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null baseCand.clear(); for( NodeId baseId : *baseIds ) { - if( !isClassLike( ing.symbols[baseId].kind ) ) + // lane lego-transitive (M1): a Ruby MODULE is a base here too. `include M` / `extend M` / `prepend M` put it in + // the class's ancestors (captureRubyMixinBases, parser version 131), so the class implements its contract the way + // a subclass does — and --lego=M listed nobody (rake: `include Rake::DSL`). The test is BuiltinMethodGate's + // class-like rule, the one that already counts a Ruby module (Ruby && SymKind::Other: @definition.module maps to + // Other for every language, and only Ruby's tags have no other Other). Other languages need nothing here: an + // Elixir module is a struct-kind symbol (defimpl / @behaviour already list), a PHP trait an interface (its + // `use T;` is not captured — the stated floor in captureBases), a Rust trait an interface, a Python mixin a class. + if( !isClassLike( ing.symbols[baseId].kind ) && !BuiltinMethodGate::isClassLike( ing.symbols[baseId] ) ) { continue; } @@ -7632,9 +8818,13 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // the call edges' JVM rule, applied to bases: a Kotlin class implements a same-named Java interface only when // Kotlin defines no candidate of that name, and the reverse (keepOwnJvmLanguageCandidates; kotlincheck §14c). keepOwnJvmLanguageCandidates( ing, r, baseCand ); + // lane lego-transitive fix 1: how each candidate edge is bound. A Ruby base its constant scope placed is proven as a + // whole (classesByFqn holds that ONE constant's opens); every other base asks provenBasesOf. + provenBasesOf( baseEvidence, r, baseCand, baseProven, rubyScoped != nullptr ); for( NodeId baseId : baseCand ) { g.implementors[ baseId ].push_back( derived ); + boundEdges.push_back( ImplementorBoundEdge{ baseId, derived, boundOfCandidate( baseProven, baseId ) } ); } } } @@ -7643,6 +8833,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::sort( v.begin(), v.end() ); v.erase( std::unique( v.begin(), v.end() ), v.end() ); } + fillImplementorBound( g, boundEdges ); // doc↔code edges: isDocLink refs (markdown `backtick` → code name) → mentions[codeDef] += docFileNode. // Resolve the name to real DEFINITIONS (body present), any file; stored OUT of the call graph so a doc @@ -9592,11 +10783,24 @@ inline std::string memberSelectorUnservedRefusal( const IngestResult& ing, std:: // through, so it is written on every path exactly as resolveAllByNameQualified writes it. inline NodeId resolveFocus( const IngestResult& ing, std::string_view spec, std::size_t* unprovenDefCountOut = nullptr ) { - const std::vector matches = resolveAllByNameQualified( ing, spec, unprovenDefCountOut ); + std::vector matches = resolveAllByNameQualified( ing, spec, unprovenDefCountOut ); if( matches.empty() ) { return kNoNode; } + // lane lego-transitive (M3): a CODE definition is the focus whenever one matched. The lowest id is crawl order, and a + // docs/ heading sorted first answered `--lego=Loader` about docs/api.md (defs=3, implementors=0) while src/loader.js + // held the class. A non-code match (a markdown heading, a JSON/YAML/TOML key) stays reachable by its file:name + // selector, and is still the answer when it is all that matched. Every verb that names one focus reads this + // (--lego, --around, --connect and their MCP twins), so the rule is stated once, here. + if( !isCodeLang( ing.symbols[ matches.front() ].lang ) ) + { + const auto code = std::find_if( matches.begin(), matches.end(), [ & ]( NodeId id ) { return isCodeLang( ing.symbols[ id ].lang ); } ); + if( code != matches.end() ) + { + std::rotate( matches.begin(), code, code + 1 ); // the first code match leads; the rest keep their order + } + } const Symbol& lowest = ing.symbols[ matches.front() ]; const auto cOrCpp = []( Lang lang ) noexcept { return lang == Lang::Cpp || lang == Lang::C; }; if( isDefinitionNotDeclaration( lowest ) || !cOrCpp( lowest.lang ) ) @@ -10255,6 +11459,7 @@ struct ImportTier std::string next; // cut-fix E: the call that lists the whole tier; empty when uncut std::uint64_t importsUnresolved = 0; // #220 part 1: importers= is a floor while > 0 std::uint64_t tsconfigUnread = 0; // #220 part 2: configs with an unread extends/references base (tsconfig_unread=) + bool umbrella = false; // idea #6: an umbrella header — see impactImportTier }; // cut-fix C: the tier's DISPLAY size, split from its measurement (callhierarchy.h's rule: the cap policy is the @@ -10270,7 +11475,10 @@ struct ImportTier // already does. Gate: impactimportcheck.sh #9d (a 130 B call). inline void sizeImportTier( ImportTier& t, int pageLimit, std::string_view sym = {} ) { - t.shown = std::min( t.files.size(), std::size_t( rw::effectiveRowCap( pageLimit, rw::kImportReachRowCap ) ) ); + // idea #6: an UMBRELLA def file's importers are counted, not listed, unless --limit asks for rows — a list of every + // file that includes tmux.h answers nothing about SYM (round-2 tmux-09: 40 such rows, the two gold files absent). + t.shown = ( t.umbrella && pageLimit == 0 ) ? 0 + : std::min( t.files.size(), std::size_t( rw::effectiveRowCap( pageLimit, rw::kImportReachRowCap ) ) ); t.capped = t.shown < t.files.size(); t.next.clear(); if( t.capped && !sym.empty() ) @@ -10284,11 +11492,34 @@ inline void sizeImportTier( ImportTier& t, int pageLimit, std::string_view sym = + " importers_capped=\"" + ( t.capped ? "1" : "0" ) + "\"" + rw::nextAttrXml( t.next, "importers_next" ) + rw::importsUnresolvedAttrXml( t.importsUnresolved ) // #220: absent at zero; the root's counts_floor covers it - + rw::countAttrXmlOrEmpty( "tsconfig_unread", std::size_t( t.tsconfigUnread ) ); // #220 part 2: likewise + + rw::countAttrXmlOrEmpty( "tsconfig_unread", std::size_t( t.tsconfigUnread ) ) // #220 part 2: likewise + + ( t.umbrella ? " importers_umbrella=\"1\"" : "" ); // idea #6: present-only ENSURES( t.shown <= t.files.size(), "the page is a prefix of the ranked tier" ); ENSURES( t.next.empty() || t.capped, "a follow-up is offered only for a cut tier" ); } +// idea #6: an umbrella header — imported by at least half the files that import ANYTHING (the population an include/ +// import can reach; docs and scripts with no import never count), and by more than one default page of them — makes the +// import tier a census of the corpus, not a blast radius. Its rows are then a count (sizeImportTier). +inline bool importTierIsUmbrella( const IngestResult& ing, std::size_t importerCount ) +{ + if( importerCount <= std::size_t( rw::kImportReachRowCap ) ) + { + return false; + } + std::vector imports( ing.files.size(), 0 ); + std::size_t importing = 0; + for( const Include& inc : ing.includes ) + { + if( inc.fileId < imports.size() && imports[inc.fileId] == 0 ) + { + imports[inc.fileId] = 1; + ++importing; + } + } + return importerCount * 2 >= importing; +} + inline ImportTier impactImportTier( const IngestResult& ing, const std::vector& seeds ) { std::vector defFiles; @@ -10337,10 +11568,104 @@ inline ImportTier impactImportTier( const IngestResult& ing, const std::vector kNoImpactHedge{}; +inline constexpr std::size_t kImpactFileRollupCap = 20; + +// the --impact row (provenCallerReach below)'s hedge: reached, but by no all-proven path +inline bool impactRowNameOnly( const std::vector& proven, NodeId n ) noexcept +{ + return !proven.empty() && n < proven.size() && proven[n] == 0; +} + +struct ImpactFileRow +{ + std::uint32_t fileId = 0; + std::uint32_t syms = 0; + std::uint32_t minDepth = 0; + std::uint32_t nameOnly = 0; // fix27: of syms, the via="name" rows (impactRowNameOnly) — counted only on a multi-def selector +}; + +struct ImpactFileRollup +{ + std::vector rows; // every file the reach set touches, ranked + std::size_t shown = 0; + bool capped = false; + std::string next; // the call listing every row; empty when uncut +}; + +inline ImpactFileRollup impactFileRollup( const IngestResult& ing, std::span reach, const std::vector& depth, + int pageLimit, std::string_view sym, bool windowCut, + const std::vector& hedgeProven = kNoImpactHedge ) +{ + ImpactFileRollup r; + if( !windowCut ) + { + return r; // the rows name every reached file already (see the block comment); nothing to roll up + } + std::vector rowOfFile( ing.files.size(), UINT32_MAX ); + for( const NodeId n : reach ) + { + if( n >= ing.symbols.size() ) + { + continue; + } + const std::uint32_t f = ing.symbols[n].fileId; + const std::uint32_t d = n < depth.size() ? depth[n] : 0; + if( rowOfFile[f] == UINT32_MAX ) + { + rowOfFile[f] = std::uint32_t( r.rows.size() ); + r.rows.push_back( ImpactFileRow{ f, 0, d } ); + } + ImpactFileRow& row = r.rows[rowOfFile[f]]; + ++row.syms; + row.minDepth = std::min( row.minDepth, d ); + row.nameOnly += std::uint32_t( impactRowNameOnly( hedgeProven, n ) ); // never on kNoImpactHedge (empty: no row hedged) + } + std::sort( r.rows.begin(), r.rows.end(), [ & ]( const ImpactFileRow& a, const ImpactFileRow& b ) + { + if( a.minDepth != b.minDepth ) { return a.minDepth < b.minDepth; } + if( a.syms != b.syms ) { return a.syms > b.syms; } + return ing.files[a.fileId] < ing.files[b.fileId]; + } ); + r.shown = std::min( r.rows.size(), std::size_t( rw::effectiveRowCap( pageLimit, int( kImpactFileRollupCap ) ) ) ); + r.capped = r.shown < r.rows.size(); + if( r.capped && !sym.empty() ) + { + r.next = rw::nextFlag( "--impact=", sym ) + " --limit=" + std::to_string( r.rows.size() ); + } + ENSURES( r.shown <= r.rows.size(), "the rollup page is a prefix of the ranked files" ); + return r; +} + +// fix27: a LISTED rollup row carries a hedge (the legend's rf via=/by_name= sentence rides exactly then) +inline bool impactRollupHedged( const ImpactFileRollup& r ) noexcept +{ + return std::any_of( r.rows.begin(), r.rows.begin() + std::ptrdiff_t( r.shown ), []( const ImpactFileRow& row ) { return row.nameOnly > 0; } ); +} + // Tarjan SCC on the file→file graph → cycles (SCCs with >1 node). Cyclic physical dependencies are // Lakos's cardinal sin: a cycle must be compiled/tested/reused as one unit. Iterative (no stack risk). inline std::vector> sccCycles( const std::vector>& adj ) @@ -10668,11 +11993,7 @@ inline std::vector provenCallerReach( const Graph& g, std::span& proven, NodeId n ) noexcept -{ - return !proven.empty() && n < proven.size() && proven[n] == 0; -} +// the --impact row's hedge (impactRowNameOnly) lives above impactFileRollup, which folds it per file (fix27) // ── Depth-labelled --impact (0.6.5) — the hop depth the walk above already records, put on the listing ────── // A flat blast radius cannot tell a direct caller from a four-hop dependent, and a page window cut over a @@ -11819,4 +13140,44 @@ inline std::size_t declinedCallsMadeBy( const Graph& g, std::span return callCount; } +// The callees answer's line: of the declines `sources` made (declinedCallsMadeBy), the C++ calls named like a +// standard container or string member on a receiver no rule tied to an in-tree class (Graph::stdMemberDeclines) — the names, +// each once in table (byte) order and comma-joined, and how many calls. Empty on a graph with none, so every other answer +// keeps its bytes. The names are table literals (identifier characters only): neither dialect needs to escape them. +struct StdMemberCallsMade +{ + std::string names; + std::size_t calls = 0; +}; +inline StdMemberCallsMade stdMemberCallsMadeBy( const Graph& g, std::span sources ) +{ + StdMemberCallsMade out; + if( g.stdMemberDeclines.empty() ) + { + return out; + } + EXPECTS( std::is_sorted( g.stdMemberDeclines.begin(), g.stdMemberDeclines.end() ), "buildGraph sorts the declines after its resolve loop" ); + std::vector named( std::size( externalnames::kCppStdMemberNames ), 0 ); + for( const NodeId s : sources ) + { + const auto lo = std::lower_bound( g.stdMemberDeclines.begin(), g.stdMemberDeclines.end(), std::pair( s, 0 ) ); + for( auto it = lo; it != g.stdMemberDeclines.end() && it->first == s; ++it ) + { + ASSUME( it->second < named.size(), "the resolve loop records a kCppStdMemberNames index" ); + named[ it->second ] = 1; + ++out.calls; + } + } + for( std::size_t i = 0; i < named.size(); ++i ) + { + if( named[ i ] != 0 ) + { + out.names.append( out.names.empty() ? "" : "," ).append( externalnames::kCppStdMemberNames[ i ] ); + } + } + ENSURES( out.names.empty() == ( out.calls == 0 ), "a named call and a listed name come together" ); + DASSERT( out.calls <= declinedCallsMadeBy( g, sources ), "every listed call is one of the declines the sources made" ); + return out; +} + } // namespace rw diff --git a/src/graphlegend.h b/src/graphlegend.h index ce8475cf6..d2cbddbfc 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -615,14 +615,29 @@ inline constexpr const char* kDeclinedCallsLegend = // answer on a tree the gate never touched keeps its bytes. inline constexpr const char* kDeclinedCallsGateClause = "It also counts a call named like a builtin-type method (dict.get, list.append) whose bound definitions' classes the caller's file never names. "; +// The C++ standard-member gate's declines (graph.h StdMemberGate) ride the same count, with ONE definition possible and in +// the caller's own file or directory too; charged only where that gate declined at least one call in the graph +// (Graph::stdMemberDeclinedCalls), so a tree it never touched keeps its bytes. What it does NOT mean is in the same breath. +inline constexpr const char* kDeclinedCallsStdClause = + "It also counts a C++ call named like a member of a standard container, string, optional or smart pointer (v.push_back, s.size) on a receiver no rule tied to an in-tree class: the standard library's own member may be the one called, so no in-tree namesake is bound; it is not a claim that the receiver is a standard type. "; inline const char* declinedCallsLegend( bool on ) noexcept { return on ? kDeclinedCallsLegend : ""; } inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDeclinedCallsGateClause : ""; } +inline const char* declinedCallsStdLegend( bool on ) noexcept { return on ? kDeclinedCallsStdClause : ""; } +// The callees answer's line (graph.h stdMemberCallsMadeBy): rides exactly when the line does. +inline constexpr const char* kStdMemberCallsLegend = + ": of declined_calls=, the calls these definitions make that are named like a member of a C++ standard container, string, optional or smart pointer (n=, each name once) on a receiver no rule tied to an in-tree class; calls= how many. No edge and no row: the standard library's own member may be the one called, and so may an in-tree namesake. It does not list every standard-library call the body makes: a name with no in-tree definition never had a candidate to decline. "; +inline const char* stdMemberCallsLegend( bool on ) noexcept { return on ? kStdMemberCallsLegend : ""; } // --test-gate's own short form: the same attribute and unit, sized for a verb whose legend has an absolute byte budget // (test/testgatelegendbudgetcheck.sh), so it defines the one form it emits rather than all three. inline constexpr const char* kDeclinedCallsTestGateLegend = "declined_calls=K (absent when 0): K call SITES the resolver declined to bind that could have reached the change or its radius (the map's declined=); a test behind one is in no row here. "; // The clause and, where the gate declined in this graph, its gate sentence — the one spelling every declined_calls= emitter uses. inline std::string declinedCallsLegendWithGate( bool on, bool gateDeclined ) { return std::string( declinedCallsLegend( on ) ) + declinedCallsGateLegend( on && gateDeclined ); } +// The same, plus the C++ standard-member gate's sentence where that gate declined in this graph (graph.h StdMemberGate). +inline std::string declinedCallsLegendWithGates( bool on, bool gateDeclined, bool stdDeclined ) +{ + return declinedCallsLegendWithGate( on, gateDeclined ) + declinedCallsStdLegend( on && stdDeclined ); +} // ── reference-as-value round (src/valuerefs.h, src/ingest_valuerefs.h) ───────────────────────────────────────── // Each clause rides ONLY a document that carries the attribute or rows it defines (value_refs= / ), so an answer @@ -740,6 +755,31 @@ inline constexpr const char* kImpactDepthColumnarLegend = "the depth column: each row's hop depth (1 = calls SYM directly; the shortest call chain). Rows run depth 1 first, " "PageRank order within a depth, so a cut drops the deepest rows first; by_depth=k:n,… counts reaches= per depth and sums " "to it, so a capped answer states the depth it stopped in. "; +// idea #6 (lane lean-trio): the per-file rollup and the umbrella-header count, each said exactly when it rides — the +// rollup whenever the reach set is non-empty (XML forms only: the JSON form carries it as "files", columnar not at all), +// the umbrella sentence only beside importers_umbrella=. Shared by the CLI arm and the MCP twin, byte-identical. +// fix27: `hedged` = a listed rf row carries via= or by_name= (a multi-definition selector only); its sentence rides then. +inline std::string impactRollupLegend( bool rollup, bool umbrella, bool hedged = false ) +{ + std::string out; + if( rollup ) + { + out += "FILES (only beside a cut symbol window): the WHOLE reach set by file. files= in all, shown_files= listed; rf " + "syms= reached symbols there, d= fewest hops; by d=, syms=, path; files_capped=\"1\" with files_next= (lists all) when cut. "; + } + if( rollup && hedged ) + { + out += "rf via=\"name\": of= matched several definitions and every reached symbol there is reached only by name, so the " + "file may depend on a namesake, not the definition meant; by_name=K: K of its syms= are. "; + } + if( umbrella ) + { + out += "importers_umbrella=\"1\": SYM's file is an umbrella header (imported by at least half the files that import anything, and by more than 40), so " + "its importers are a COUNT here, not rows (shown_importers= is 0); importers_next= lists them. "; + } + return out; +} + inline const char* impactDepthLegend( bool columnar ) noexcept { return columnar ? kImpactDepthColumnarLegend : kImpactDepthLegend; @@ -876,6 +916,22 @@ inline constexpr const char* kModScopeLegend = "t=\"modscope\" is a row for a file's MODULE SCOPE, named : the statements outside every named definition, which is where a top-level call and an anonymous callback body's calls live. It is a CALLER, not a function — nothing in the source can name it, so it never appears as a callee, and it has no body, so expanding it returns none. A file that has no such call has no such row. "; inline const char* modScopeLegend( bool on ) noexcept { return on ? kModScopeLegend : ""; } +// CALLSITE-AT (2026-10-09): the reading of a caller row's sites_at= — the callers verb's s rows, safe-delete's c rows, and MCP's +// calledBy[].sites_at — ONE wording for all three. Present-only: `on` is the emitter's own "a row on this page carries sites_at=" +// condition and `cut` its "a row carries sites_total=" condition, so an answer without them pays 0 bytes. `cap` is +// pageview.h kCallSitesPerRowCap, passed in (this header sits below pageview.h). No double hyphen: inside an XML comment. +inline constexpr const char* kCallSitesAtLegend = + "sites_at= on a caller row: its calls of the called LEAF name, any receiver or qualifier, as file:line tokens " + "(space-separated, ascending; two calls on one line are one site); p= stays where the caller is DEFINED. Matched by name like " + "the uses verb's call rows, so not proof each binds here; absent when no call there is spelled like it (an aliased import). "; +inline std::string callSitesAtLegend( bool on, bool cut, std::size_t cap ) +{ + std::string clause = on ? std::string( kCallSitesAtLegend ) : std::string(); + clause += on && cut ? "sites_total=N (absent when sites_at= is whole): this row has N sites and sites_at= lists the first " + std::to_string( cap ) + "; the uses verb on the same selector lists every one. " + : std::string(); + return clause; +} + // FE-B (test/receiverevidencecheck.sh): the reading of via="name" — ONE wording for every surface that marks a row with it // (--callees/--callers, --impact, --path, --connect's , the rows of --expand and --for, and MCP's "via":"name"). // Gated like modScopeLegend: an answer carrying no such row pays 0 bytes. What it does NOT mean is said in the sentence, diff --git a/src/historyread.h b/src/historyread.h new file mode 100644 index 000000000..aa75ef315 --- /dev/null +++ b/src/historyread.h @@ -0,0 +1,95 @@ +#pragma once + +// historyread.h — the ONE spelling of "the git history behind churn= and amp= could not be read in this run" (K51). +// +// The history walk (`git log` over the 18-month window) feeds two numbers every rich verb prints: churn= (--for, the +// commit count of a file) and amp= (a symbol's callers plus the files that share a commit with it). When git cannot +// finish that walk the stream is EMPTY (git never ran: not on PATH, not executable, no popen) or a newest-first PREFIX +// (git ran and stopped part-way: an unreadable object, a bad ref). Undisclosed, both numbers read as "this file never +// changed" / "no co-change partners", or as the whole window when they are only its newest part. This header is the +// disclosure for it, in the dialect of each host document, so the failure is a field of the answer, not a trace. +// +// THE ATTRIBUTE. history_unread="1" on the root of a document that prints churn= or amp=; absent when the walk was read +// (including a read that found no commit — an unborn branch, git's own exit 128 with no HEAD: that is a measurement, and +// it stays silent). It means: churn= and the co-change half of amp= count only the commits that WERE read, so both are +// FLOORS — none read when git did not run (churn= absent, amp= callers only), the newest part of the window when it +// stopped part-way. A prefix is kept rather than dropped because a floor is still information (a file with churn="3" from +// a prefix changed at least 3 times); the attribute is what stops it reading as the full count. An absent churn= under +// the attribute is not zero churn. It does NOT mean the repository is damaged beyond that walk, that the other numbers +// in the document are affected, or that a retry will fail: the failed walk is never cached, so the next call walks again. +// The comment names which cause this run hit. A verb that runs no history walk (a plain map, --callers) never carries it. +// +// NOT COVERED (pre-existing, deferred): the other git walks — --cochange and the MCP cochange tool, --rank-by=churn and +// --owners — do not yet tell a failed walk from an empty one (CHANGELOG, the K51 entry). +// +// ONE RENDERER (the prconverge.h rule): the forms differ only in the syntax of their host, never in what they say, so a +// new host adds a DiscloseAs case here, never a sibling function. XmlAttrs and the comment form (LegendComment) go on every +// XML root that prints the numbers; JsonKeys is the JSON twin. The comment is its own definition, so a first screen that +// carries the attribute defines it (legendcoveragecheck), and it holds no "--" digraph and no "<" (it lives in an XML comment). +// +// STATE. One process-wide report, written once by main.cpp's amp=/churn= block on the main thread, after the walks were +// joined and before any emitter runs; emitters only read it. The MCP server never runs that block, so it never sets it. +// Gate: test/qchurncheck.sh (the K51 block). + +#include +#include + +#include "prconverge.h" // DiscloseAs — the shared "which spelling does the host accept" enum + +namespace rw +{ + +// The DISCLOSE sink (CONTRIBUTING §3): the roots whose walk could not be read, by cause. +struct HistoryReadReport +{ + enum class DisclosureWhy : std::uint8_t + { + WalkNotRun, // git did not run (not on PATH, not executable, no popen): the root's stream is empty + WalkStopped, // git ran and exited non-zero part-way: the root's stream is a newest-first prefix (possibly empty) + }; + std::uint32_t unreadRoots = 0; + std::uint32_t notRunRoots = 0; + std::uint32_t stoppedRoots = 0; + void disclose( DisclosureWhy why ) noexcept + { + ++unreadRoots; + ++( why == DisclosureWhy::WalkNotRun ? notRunRoots : stoppedRoots ); + } +}; + +inline HistoryReadReport& historyRead() noexcept +{ + static HistoryReadReport report; + return report; +} + +// "" when every walk was read, so a caller that appends unconditionally keeps its exact bytes on the ordinary path. +inline std::string renderHistoryUnread( DiscloseAs as ) +{ + if( historyRead().unreadRoots == 0 ) + { + return {}; + } + if( as == DiscloseAs::XmlAttrs ) + { + return " history_unread=\"1\""; + } + if( as == DiscloseAs::JsonKeys ) + { + return ",\"history_unread\":true"; + } + std::string comment = ""; + return comment; +} + +} // namespace rw diff --git a/src/ingest.cpp b/src/ingest.cpp index 840d28e7f..b1c23fa9c 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -58,6 +58,7 @@ #include #include #include +#include // cli-perf-068: the git ignore probe on a worker (ingest_crawl.h IgnoreProbeSpeculation) #include #include #include @@ -315,10 +316,18 @@ IngestResult ingest( const char* rootDir, const std::vector& exclud // #350 layer 3: one memory watch for this ingest, shared by the crawl and the parse pool (memguard.h) memguard::Watch memWatch; - // 1) deterministic crawl -> sorted file list (this list IS result.files / the fileId space) + // cli-perf-068: the git ignore probe runs beside the walk and the cache load, on the previous call's answer, and is + // checked against this call's own answer before the parse pool (ingest_crawl.h IgnoreProbeSpeculation). No sidecar + // under --no-cache: the probe then runs inside the crawl, as it always did. + IgnoreProbeSpeculation ignoreSpeculation( !cacheFile.empty() ); + + // 1) deterministic crawl -> sorted file list (this list IS result.files / the fileId space). A lambda because a lost + // ignore speculation runs it a second time, on git's fresh answer (below, before the parse pool). + const auto crawlInto = [ & ]() { PROFILE_SCOPE_DESCRIBE( "ingest: crawl (collectSources)" ); - auto [ crawledPaths, oversizeSkipped, taxonomySkips ] = collectSources( rootDir, excludeSubstr, maxFileBytes, excludeLabel, respectGitignore, &memWatch ); + auto [ crawledPaths, oversizeSkipped, taxonomySkips ] = collectSources( rootDir, excludeSubstr, maxFileBytes, excludeLabel, respectGitignore, &memWatch, + &ignoreSpeculation ); result.files = std::move( crawledPaths ); // #228: record the root once, for rootRelPath (model.h). A directory crawl joins it onto every path; a // single-file root IS its one path, so the root-relative view anchors at that file's directory instead. @@ -344,7 +353,8 @@ IngestResult ingest( const char* rootDir, const std::vector& exclud DISCLOSE( result.memoryStop, MemoryStop::DisclosureWhy::CrawlOverLimit, "ingest: the memory guard stopped the crawl — files= is what it saw, a floor of the tree" ); } } - } + }; + crawlInto(); // Win 1 (PERF.md P1) — lazy grammar compilation: load the cache FIRST, then compile only the // grammars needed by cache-miss files (new or hash-changed). On a fully-warm zero-change run, @@ -373,10 +383,25 @@ IngestResult ingest( const char* rootDir, const std::vector& exclud HashMap cache = cacheFile.empty() ? HashMap{} : loadCache( std::string( cacheFile ), rootDir, captureValueUses, result.files, cacheStats ); - const long long cacheWriteNs = cacheStats.blobWriteNs; // per-fileId scan arrays (language classify + hash/stat-gate + health slots) — ingest_prewarm.h IngestFileScan scan = makeFileScan( result.files ); + // The ignore speculation is decided HERE: nothing past this point has run on the crawl's file list but the cache load + // and the scan arrays, and both are redone. A lost speculation (the previous call's ignore answer differs from git's + // answer for this call) re-walks on the fresh answer; the crawl's memory stop is re-armed and re-recorded with it. + if( !ignoreSpeculation.crawlStands() ) + { + result.memoryStop = MemoryStop{}; + memWatch.rearmForParse(); // clears the crawl's stop flags: the second walk is measured on its own + crawlInto(); + cacheStats = CacheLoadStats{}; + cache = cacheFile.empty() ? HashMap{} + : loadCache( std::string( cacheFile ), rootDir, captureValueUses, result.files, cacheStats ); + scan = makeFileScan( result.files ); + } + ENSURES( scan.lang.size() == result.files.size(), "the scan arrays describe the crawl the parse pool will read" ); + const long long cacheWriteNs = cacheStats.blobWriteNs; + // lazy tags.scm prewarm: detect the cache-miss set (prefilling scan.hash/stat), launch the async // grammar compiles, and hand the compile/ready state to the parse pool via `prewarm` (ingest_prewarm.h). QueryPrewarm prewarm; diff --git a/src/ingest.h b/src/ingest.h index 162938b60..8e454dc2a 100644 --- a/src/ingest.h +++ b/src/ingest.h @@ -881,4 +881,34 @@ inline bool astQueryNestsTooDeep( std::string_view query ) // local to the re-parsed substring. std::vector collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ); +// ---- --whereis on other refs: where the TAGS PATH defines one name inside blobs the index never read ---- +// HEAD's rows take kind="def" from the index; a row on another ref comes from a git blob the index never saw. This +// runs the index's own extraction on such a blob — the crawl's extension → grammar rule (with the .h → ObjC content +// reroute), the nesting refusal, the member-macro re-parse, the `#if 0` filter, the language's tags query through +// captureTagsFacts, and the markdown section walk — so a branch definition is labelled by the same captures that +// label HEAD's. `lines` is the 1-based @name line of every definition named `name`, ascending (RawDef::line, which +// is Symbol::line: the line HEAD's relabel keys on). +struct BlobText +{ + std::string_view path; // the tree path: its extension picks the grammar, as in the crawl + std::string_view bytes; // the blob's content +}; +enum class BlobDefsStatus : std::uint8_t +{ + Parsed, // the tags path ran over the whole blob: `lines` is its complete answer for the name + NoGrammar, // no indexed language for this path (the index defines nothing in such a file either) + Unread, // not parsed: binary, over the crawl's default size ceiling, or a nesting refusal (the crawl skips these too) + Failed, // the parser or the tags query could not finish (a null tree, an unavailable query, a partial extract) +}; +struct BlobDefinitions +{ + std::vector lines; + BlobDefsStatus status = BlobDefsStatus::Failed; +}; +// Parallel over hardware_concurrency() workers, biggest blob first (spanTiersOfFiles' pool shape); each blob writes +// only its own slot, so the result is the serial one. The tags queries a batch needs are compiled and installed into +// the process-global query cache BEFORE any worker starts — the single-writer rule ingest() itself follows — so a +// long-lived server must call this under quality::headSnapshotIngestMutex, like every other ingest call site. +std::vector definitionLinesInBlobs( std::span blobs, std::string_view name ); + } // namespace rw diff --git a/src/ingest_astquery.h b/src/ingest_astquery.h index f1c0b8d7b..d3a38808d 100644 --- a/src/ingest_astquery.h +++ b/src/ingest_astquery.h @@ -2143,4 +2143,240 @@ std::vector collectGatedLocalNames( std::string_view defBytes, st return out; } +// ---- definitionLinesInBlobs (ingest.h): the tags path over blobs from other git refs ---- +// +// The DISCLOSE sink of one blob: a degrade sets its slot's status, which is exactly what the caller reads (a blob +// that is not Parsed never confirms a definition, and --whereis says how many such blobs it met). +struct BlobDefsSlot +{ + enum class DisclosureWhy : std::uint8_t + { + NoParser, // ts_parser_new / ts_query_cursor_new gave nothing: the worker parses no blob + GrammarRejected, // the grammar could not be set on the parser (ABI): this blob is not parsed + ParseNull, // tree-sitter returned no tree + ExtractPartial, // the tags pass disclosed a shortfall (ExtractShortfall): its definitions may be incomplete + WorkerThrew, // an exception inside the worker: the slot keeps Failed + }; + BlobDefinitions& slot; + void disclose( DisclosureWhy ) noexcept // every reason records the same fact: no complete answer for this blob + { + slot.lines.clear(); + slot.status = BlobDefsStatus::Failed; + } +}; + +// The worker's non-markdown extraction over one blob, in its order: the .astro frontmatter restriction, the parse, the +// member-macro re-parse when the first parse holds errors, the `#if 0` ranges, the definitions-only tags pass. False when +// the slot is already final (a template-only .astro, or a degrade it discloses). +inline bool tagsDefinitionsOfBlob( std::string_view bytes, const LangEntry& le, TSParser* parser, TSQueryCursor* cursor, MemberMacroReparse& macroWork, + std::vector& defs, BlobDefinitions& slot ) +{ + IncludedRangeGuard rangeGuard; + const AstroFrontmatter astro = restrictAstroToFrontmatter( parser, le, bytes, rangeGuard ); + if( astro == AstroFrontmatter::Unterminated ) + { + DISCLOSE( ( BlobDefsSlot{ slot } ), BlobDefsSlot::DisclosureWhy::ExtractPartial, "whereis: an .astro blob opens a frontmatter fence it never closes — not extracted" ); + return false; + } + if( astro == AstroFrontmatter::None ) + { + slot.status = BlobDefsStatus::Parsed; // a template-only .astro: the index extracts nothing from it either + return false; + } + TreeGuard tree( parseTree( parser, bytes ) ); + if( tree.get() == nullptr ) + { + DISCLOSE( ( BlobDefsSlot{ slot } ), BlobDefsSlot::DisclosureWhy::ParseNull, "whereis: a blob did not parse — its rows confirm no definition" ); + return false; + } + if( ts_node_has_error( ts_tree_root_node( tree.get() ) ) ) // a clean parse has no error bytes: the re-parse never adopts + { + (void)measureHealthAdoptingMemberMacroReparse( parser, le.lang, bytes, tree, macroWork ); // may adopt it, as the worker does + } + const TSNode root = ts_tree_root_node( tree.get() ); + const std::vector ppDead = preprocDeadRangesFor( le, root, bytes ); + std::vector refs; + std::vector binds; + std::vector incs; + ExtractShortfall shortfall; + captureTagsFacts( cursor, le, 0, bytes, root, defs, refs, binds, incs, ppDead, shortfall, TagsCapture::DefinitionsOnly ); + if( shortfall.isShort ) + { + DISCLOSE( ( BlobDefsSlot{ slot } ), BlobDefsSlot::DisclosureWhy::ExtractPartial, "whereis: the tags pass over a blob was partial — its rows confirm no definition" ); + return false; + } + return true; +} + +// One blob through the crawl's language rule and the parse-pool worker's extraction steps, in the worker's order. +// `scan` is a one-slot scan the nesting refusal records into (its --skipped bookkeeping has no reader here: the +// slot's Unread status is this blob's disclosure). +inline void definitionLinesOfOneBlob( const BlobText& blob, std::string_view name, TSParser* parser, TSQueryCursor* cursor, + MemberMacroReparse& macroWork, IngestFileScan& scan, BlobDefinitions& slot ) +{ + const std::string ext = lowerExtensionOf( blob.path ); + const LangEntry* le = lookupLang( ext ); + if( le == nullptr ) + { + slot.status = BlobDefsStatus::NoGrammar; + return; + } + if( blob.bytes.size() > kDefaultMaxFileBytes || looksBinary( blob.bytes ) ) + { + slot.status = BlobDefsStatus::Unread; // the crawl's own ceiling and binary sniff: the index skips these too + return; + } + if( le->ext == ".h" && looksObjC( blob.bytes ) ) + { + if( const LangEntry* objcLe = lookupLang( ".m" ) ) + { + le = objcLe; + } + } + const std::string pathText( blob.path ); + scan.nestRefusedBytes[ 0 ] = 0; + if( refuseNesting( *le, blob.bytes, pathText.c_str(), 0, scan ) ) + { + slot.status = BlobDefsStatus::Unread; + return; + } + if( !prepareParserFor( parser, *le ) ) + { + DISCLOSE( ( BlobDefsSlot{ slot } ), BlobDefsSlot::DisclosureWhy::GrammarRejected, "whereis: a blob's grammar could not be set — its rows confirm no definition" ); + return; + } + std::vector defs; + if( le->lang == Lang::Markdown ) + { + TreeGuard mdTree( parseTree( parser, blob.bytes ) ); + if( mdTree.get() == nullptr ) + { + DISCLOSE( ( BlobDefsSlot{ slot } ), BlobDefsSlot::DisclosureWhy::ParseNull, "whereis: a markdown blob did not parse — its rows confirm no definition" ); + return; + } + std::vector refs; + extractMarkdown( 0, blob.bytes, fs::path( pathText ).stem().string(), ts_tree_root_node( mdTree.get() ), defs, refs ); + } + else if( !tagsDefinitionsOfBlob( blob.bytes, *le, parser, cursor, macroWork, defs, slot ) ) + { + return; // the slot says why (template-only .astro: Parsed with nothing; a degrade: Failed) + } + // The model build's steps that decide which captured definitions become SYMBOLS (ingest.cpp 3a..3a-ter), so a + // blob answers what HEAD's index answers for the same bytes: overlapping patterns collapse to one definition, an + // ObjC @interface declaration shadowed by its same-file @implementation drops, and a field leaves the symbol + // universe (fields are not Symbols, so on HEAD a field line is never a def site either). + dedupRawDefs( defs ); + collapseObjCDeclDefs( defs ); + (void)partitionFieldDefs( defs ); + for( const RawDef& d : defs ) + { + if( d.name == name ) + { + slot.lines.push_back( d.line ); + } + } + std::sort( slot.lines.begin(), slot.lines.end() ); + slot.status = BlobDefsStatus::Parsed; +} + +// The query set, single-threaded and BEFORE any worker: every grammar a blob can reach — its extension's, plus ObjC's for a +// .h (the content reroute is decided inside the worker). compiledQueryFor only ever READS the cache, so a grammar is +// compiled here once and the cache owns it from then on (CompiledQueryCache frees it at teardown). +inline void installBlobQueries( std::span blobs ) +{ + std::vector want; + const auto need = [ & ]( const LangEntry* le ) + { + if( le == nullptr || le->grammar == nullptr || le->querySub.empty() || compiledQueryCache().find( le->grammar() ) != compiledQueryCache().end() ) + { + return; + } + if( std::none_of( want.begin(), want.end(), [ & ]( const LangEntry* w ) { return w->grammar() == le->grammar(); } ) ) + { + want.push_back( le ); + } + }; + for( const BlobText& b : blobs ) + { + const std::string ext = lowerExtensionOf( b.path ); + need( lookupLang( ext ) ); + if( ext == ".h" ) + { + need( lookupLang( ".m" ) ); + } + } + for( const LangEntry* le : want ) + { + if( TSQuery* q = compileQueryStandalone( *le ); q != nullptr ) // a compile error stays absent: captureTagsFacts discloses it + { + compiledQueryCache()[ le->grammar() ] = q; + } + } +} + +// One parse worker: its own parser, query cursor and scratch, claiming blobs in `walkOrder` until none is left. Each blob +// writes only its own slot of `out`. +inline void definitionLinesWorker( std::span blobs, std::string_view name, std::span walkOrder, + std::atomic& nextSlot, std::span out ) noexcept +{ + ParserGuard pg; + TSQueryCursor* cursor = ts_query_cursor_new(); + if( pg.p == nullptr || cursor == nullptr ) + { + for( std::size_t s = nextSlot.fetch_add( 1, std::memory_order_relaxed ); s < blobs.size(); s = nextSlot.fetch_add( 1, std::memory_order_relaxed ) ) + { + DISCLOSE( ( BlobDefsSlot{ out[ walkOrder[ s ] ] } ), BlobDefsSlot::DisclosureWhy::NoParser, "whereis: no tree-sitter parser for a blob worker" ); + } + if( cursor != nullptr ) + { + ts_query_cursor_delete( cursor ); + } + return; + } + MemberMacroReparse macroWork; + IngestFileScan scan = makeFileScan( std::vector{ std::string() } ); + for( std::size_t s = nextSlot.fetch_add( 1, std::memory_order_relaxed ); s < blobs.size(); s = nextSlot.fetch_add( 1, std::memory_order_relaxed ) ) + { + BlobDefinitions& slot = out[ walkOrder[ s ] ]; + try + { + definitionLinesOfOneBlob( blobs[ walkOrder[ s ] ], name, pg.p, cursor, macroWork, scan, slot ); + } + catch( ... ) // a throw escaping a worker thread is std::terminate: the blob degrades instead + { + DISCLOSE( ( BlobDefsSlot{ slot } ), BlobDefsSlot::DisclosureWhy::WorkerThrew, "whereis: a blob worker threw — that blob confirms no definition" ); + } + } + ts_query_cursor_delete( cursor ); +} + +std::vector definitionLinesInBlobs( std::span blobs, std::string_view name ) +{ + std::vector out( blobs.size() ); + if( blobs.empty() ) + { + return out; + } + warmFieldIdTable(); // idempotent; the AST walks behind captureTagsFacts read it lock-free + installBlobQueries( blobs ); + + std::vector walkOrder( blobs.size() ); + std::iota( walkOrder.begin(), walkOrder.end(), std::uint32_t( 0 ) ); + std::stable_sort( walkOrder.begin(), walkOrder.end(), [ & ]( std::uint32_t a, std::uint32_t b ) noexcept + { return blobs[ a ].bytes.size() > blobs[ b ].bytes.size(); } ); // biggest first: no straggler at the tail + const unsigned threadCount = static_cast( std::min( std::max( 1u, std::thread::hardware_concurrency() ), blobs.size() ) ); + std::atomic nextSlot{ 0 }; + std::vector pool; + pool.reserve( threadCount ); + for( unsigned t = 0; t < threadCount; ++t ) + { + pool.emplace_back( [ & ]() noexcept { definitionLinesWorker( blobs, name, walkOrder, nextSlot, out ); } ); + } + for( std::thread& w : pool ) + { + w.join(); + } + return out; +} + } // namespace rw — ingest_astquery.h section of ingest.cpp diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 273a7332f..d839243a9 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -1730,7 +1730,7 @@ inline void rubyNoteClassSideDef( TSNode defNode, std::uint32_t fileId, std::str // The bindings for what the `class << self` call `n` declares on the class object's `side` — an accessor, or a // delegation-DSL call (captureRubySingletonAccessors below). An accessor's binding sits at its symbol, -// where captureRubyAttrDefs starts its def (no owner), and carries the side's mark (one a concern's `included do` +// where captureRubyDefs starts its def (no owner), and carries the side's mark (one a concern's `included do` // declares is the includer's); a delegated name has no def, and is read against the class that owns the singleton. inline void rubyNoteSingletonDecl( TSNode n, RubyDefSide side, std::uint32_t fileId, std::string_view src, std::vector& binds ) { @@ -1753,7 +1753,7 @@ inline void rubyNoteSingletonDecl( TSNode n, RubyDefSide side, std::uint32_t fil } // What a `class << self` declares beside its defs is the class object's too (parser version 137): each accessor -// (`attr_accessor :config`, one RubySingletonDef binding per name, where captureRubyAttrDefs starts its def) and each name +// (`attr_accessor :config`, one RubySingletonDef binding per name, where captureRubyDefs starts its def) and each name // the delegation DSL defines (model.h kRubyDelegationCalls — `delegate :reset, to: :instance`: a binding per name against // the owning class; `delegate_missing_to` one with no name, as it answers every name). A file that never writes `<<` is not // walked; the walk is iterative, so a hostile nesting depth costs heap, not stack. @@ -2154,6 +2154,54 @@ inline RecvShape classifyReceiver( TSNode node, Lang lang, std::string_view src, return {}; // parenthesized / subscripted / call receiver → not one-hop } +// The C++ ELEMENT-ACCESS receiver mark (model.h Reference::memberPath, its C++ reading; parser version 165): a member call whose +// receiver is ONE element access of a one-hop container — `m[ k ].empty()`, `v.at( i ).size()`, `v.front().size()`, +// `v.back().size()`, `p->at( i ).size()` — records that container's shape as model.h kCppElementMark + "root" (a variable or field +// NAME, `m`), "this.field" (`this->m[ k ]`) or "root.field" (`rule->bindings_[ k ]`, `r.vec_[ 0 ]`). graph.h StdMemberGate reads +// the container's recorded written type for its element type (stdReceiverOf). "" for every other receiver: a two-level +// access (`m[ a ][ b ]`), a dereference, a call result, a deeper chain, and an element access whose container is itself one. +inline std::string cppElementContainer( TSNode recvNode, std::string_view src ) +{ + const char* rt = ts_node_type( recvNode ); + TSNode container = {}; + if( kindIs( rt, "subscript_expression" ) ) + { + container = fieldChild( recvNode, NodeField::Argument ); + } + else if( kindIs( rt, "call_expression" ) ) + { + const TSNode fn = fieldChild( recvNode, NodeField::Function ); + if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "field_expression" ) ) + { + const std::string_view accessor = nodeFieldText( fn, NodeField::Field, src ); + if( accessor == "at" || accessor == "front" || accessor == "back" ) + { + container = memberAccessReceiver( fn, Lang::Cpp ); + } + } + } + if( ts_node_is_null( container ) ) + { + return {}; + } + const RecvShape shape = classifyReceiver( container, Lang::Cpp, src, /*allowChain=*/true ); + std::string out; + if( shape.kind == RecvKind::NamedVar && !shape.var.empty() ) + { + out.assign( kCppElementMark ).append( shape.var ); + } + else if( shape.kind == RecvKind::FieldOfThis && !shape.field.empty() ) + { + out.assign( kCppElementMark ).append( "this." ).append( shape.field ); + } + else if( shape.kind == RecvKind::FieldOfVar && !shape.var.empty() && !shape.field.empty() ) + { + out.assign( kCppElementMark ).append( shape.var ).push_back( '.' ); + out.append( shape.field ); + } + return out; +} + // TS/JS literal-receiver kinds (issue #163). A member call whose receiver type the syntax already // proves is a built-in; it must not take the bare-name ladder. Object literals, identifier // receivers, this, casts, and element-returning links stay RecvKind::None. Do NOT widen @@ -2420,6 +2468,10 @@ inline RecvShape receiverOf( TSNode nameNode, Lang lang, std::string_view src ) // "truly bare". Found by the veto's own precision listing: `self.to_cartesian().sum()` was read as a // bare builtin `sum(…)` and refused. Closes the depth-3 residual test/chainguardcheck.sh arm (h) pinned. rs.kind = RecvKind::FieldOfVar; + if( lang == Lang::Cpp ) + { + rs.path = cppElementContainer( recvNode, src ); // `m[ k ].empty()`: the container's shape (parser version 165) + } } return rs; } diff --git a/src/ingest_cache.h b/src/ingest_cache.h index ed48f68cb..ad43744cd 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -89,6 +89,8 @@ struct RawRef std::string memberCtor; // FE-B: the class a constructed receiver names (`new X().m()`, Go `X{}.m()`); see model.h std::string memberVia; // FE-B on #373: the method a Ruby typed receiver was built by (`new`, a finder); see model.h bool memberFactory = false; // FE-B on #373: memberCtor names a FactoryBot factory, not a class + std::uint32_t baseBind = 0; // lane lego-transitive fix 1, an inherit ref only: 1 + the name byte of the same-file + // declaration the base binds to (ingest_relations.h localBaseBinding); 0 none, ~0u unknown }; // P2-D Rule 2 raw local-variable type binding (pre-attribution). startByte sits inside the enclosing @@ -138,7 +140,13 @@ 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 = 29; // 29: FE-B (test/receiverevidencecheck.sh) — RawRef gains `memberPath` (str) and +constexpr std::uint32_t kCacheVersion = 31; // 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); + // every other ref record is unchanged in size. A FORMAT change: a v29 blob lacks it. + // The train renumbers if another lane takes 30. + // 29: FE-B (test/receiverevidencecheck.sh) — RawRef gains `memberPath` (str) and // `memberCtor` (str) after `memberRoot` (ref record 46 -> 54 bytes lean; // on #373 also `memberVia` (str) + `memberFactory` (u8), 54 -> 59: the Ruby // typed receiver moved out of recvVar — same version, one FE-B format), @@ -312,7 +320,36 @@ constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiver // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 156; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 167; // bump on any grammar/.scm/extraction change + // 167 = 2026-10-11 (train 27 release): the fix27-cpp-subscript extraction below, renumbered above the + // lane build's 166 (rich file tag 167 there), so no cache a lane build wrote is read as this release's. + // Rich file tag 168. No other extraction change; kCacheVersion stays 31. + // 166 = lane fix27-cpp-subscript (2026-10-11): a C++ member call on ONE element access of a + // one-hop container (`m[ k ].empty()`, `v.at( i ).size()`) records the container's shape in + // memberPath (ingest_binds.h cppElementContainer); a type alias of a std element container and + // a member field written as one record its whole text (ingest_relations.h stdElementContainerText; + // the field as a new composeRel "elem" record). 166, not 165: 165 is 164's rich file tag. No + // record layout change (kCacheVersion stays 31). Renumbered to 167 by the train. + // 164 = 2026-10-11 (train 27: the signed lanes merged onto train 26c's 158): cache-key hygiene. One merged lane + // changed extraction under lane-local numbers: lego-transitive (157, 160, then 162; the 160 and 162 notes are below). Live + // branch builds have used numbers up to 162 (rich file tag 163). 164 (rich file tag 165) is above every one + // of them, so no cache another build wrote is read as this build's. kCacheVersion moves with it (31). + // 162 = lane lego-transitive fix 1 (2026-10-10): an inherit ref records its + // same-file base binding; Python dotted / subscript / keyword bases and JS + // member / call heritage read as one base or none; a JS class factory needs a + // same-file class receiver. 162, not 161: 161 is 160's rich file tag. The train renumbers. + // 160 = lane lego-transitive (2026-10-10): a JS/TS class factory + // `const Sub = Base.extend('Sub', ...)` is a class definition extending + // its receiver (tags.scm @definition.classfactory). 160, not 157: 157 is + // 156's rich file tag, and train 26c holds 158 (rich 159). The train renumbers. + // 158 = 2026-10-09 (train 26c: PR #339, Rails db/schema.rb columns, merged onto main's 156): + // a Ruby file whose tree holds a `create_table "x", … do |t| … end` call is a rendered + // db/schema.rb BY CONTENT; each `t. "name"` / `t. :name` in the table block + // mints a SymKind::Section def at Lang::Ruby (plus the implicit / `id:` / `primary_key:` + // id rule and `t.timestamps`). DEFINITIONS ONLY: buildGraph's byName skips Section-Ruby, + // so a column takes no call edge. The branch carried 122 -> 126 -> 144 -> 149 across its + // merges with main; 158, not 157: 156's rich file tag is 157. No record layout change + // (kCacheVersion 29, kQSnapCacheScheme 19 stay). // 156 = 2026-10-08 (train 26b: signed lanes merged onto main's 148): cache-key hygiene. // Two merged lanes changed extraction under lane-local numbers, both 145: // honesty-small-068 (Go named-type kinds, `type A = B` aliases) and @@ -2266,7 +2303,7 @@ inline void writeDef( ByteW& w, const RawDef& d, bool withLex, std::size_t fileD } } } -inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8( std::uint8_t( r.lang ) ); w.str( r.name ); w.u8( r.isInherit ? 1 : 0 ); w.u8( r.isDocLink ? 1 : 0 ); w.str( r.qualifier ); w.u8( std::uint8_t( r.recv ) ); w.str( r.recvVar ); w.u8( r.isCompose ? 1 : 0 ); w.str( r.fieldName ); w.str( r.composeRel ); w.u8( std::uint8_t( r.role ) ); w.u32( r.line ); w.u32( r.argCount ); w.u8( r.argCountKnown ? 1 : 0 ); w.u8( r.viaArrow ? 1 : 0 ); w.u8( r.qualifierRootsStd ? 1 : 0 ); w.u8( r.memberCall ? 1 : 0 ); w.str( r.memberRoot ); w.str( r.memberPath ); w.str( r.memberCtor ); w.str( r.memberVia ); w.u8( r.memberFactory ? 1 : 0 ); } +inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8( std::uint8_t( r.lang ) ); w.str( r.name ); w.u8( r.isInherit ? 1 : 0 ); w.u8( r.isDocLink ? 1 : 0 ); w.str( r.qualifier ); w.u8( std::uint8_t( r.recv ) ); w.str( r.recvVar ); w.u8( r.isCompose ? 1 : 0 ); w.str( r.fieldName ); w.str( r.composeRel ); w.u8( std::uint8_t( r.role ) ); w.u32( r.line ); w.u32( r.argCount ); w.u8( r.argCountKnown ? 1 : 0 ); w.u8( r.viaArrow ? 1 : 0 ); w.u8( r.qualifierRootsStd ? 1 : 0 ); w.u8( r.memberCall ? 1 : 0 ); w.str( r.memberRoot ); w.str( r.memberPath ); w.str( r.memberCtor ); w.str( r.memberVia ); w.u8( r.memberFactory ? 1 : 0 ); if( r.isInherit ) { w.u32( r.baseBind ); } } // loadCache's countFits() bounds a corrupt on-disk record COUNT against remaining bytes / // minRecordBytes BEFORE reserve() — the guard that keeps a hostile blob's 0xFFFFFFFF count from reaching @@ -2400,7 +2437,7 @@ inline RawDef readDef( ByteR& r, bool withLex, const std::vector& } return d; } -inline RawRef readRef ( ByteR& r ) { RawRef x; x.startByte = r.u32(); x.lang = r.enumU8( kLangCount ); x.name = r.str(); x.isInherit = r.u8() != 0; x.isDocLink = r.u8() != 0; x.qualifier = r.str(); x.recv = r.enumU8( kRecvKindCount ); x.recvVar = r.str(); x.isCompose = r.u8() != 0; x.fieldName = r.str(); x.composeRel = r.str(); x.role = r.enumU8( kRefRoleCount ); x.line = r.u32(); x.argCount = r.u16Of32(); x.argCountKnown = r.u8() != 0; x.viaArrow = r.u8() != 0; x.qualifierRootsStd = r.u8() != 0; x.memberCall = r.u8() != 0; x.memberRoot = r.str(); x.memberPath = r.str(); x.memberCtor = r.str(); x.memberVia = r.str(); x.memberFactory = r.u8() != 0; return x; } +inline RawRef readRef ( ByteR& r ) { RawRef x; x.startByte = r.u32(); x.lang = r.enumU8( kLangCount ); x.name = r.str(); x.isInherit = r.u8() != 0; x.isDocLink = r.u8() != 0; x.qualifier = r.str(); x.recv = r.enumU8( kRecvKindCount ); x.recvVar = r.str(); x.isCompose = r.u8() != 0; x.fieldName = r.str(); x.composeRel = r.str(); x.role = r.enumU8( kRefRoleCount ); x.line = r.u32(); x.argCount = r.u16Of32(); x.argCountKnown = r.u8() != 0; x.viaArrow = r.u8() != 0; x.qualifierRootsStd = r.u8() != 0; x.memberCall = r.u8() != 0; x.memberRoot = r.str(); x.memberPath = r.str(); x.memberCtor = r.str(); x.memberVia = r.str(); x.memberFactory = r.u8() != 0; if( x.isInherit ) { x.baseBind = r.u32(); } return x; } inline void writeBind( ByteW& w, const RawBind& b ) { w.u32( b.startByte ); w.u8( std::uint8_t( b.lang ) ); w.u8( std::uint8_t( b.kind ) ); w.u8( b.isFromAssignment ? 1 : 0 ); w.u32( b.spanStart ); w.u32( b.spanEnd ); w.str( b.var ); w.str( b.typeName ); w.str( b.importedName ); } inline RawBind readBind( ByteR& r ) { RawBind b; b.startByte = r.u32(); b.lang = r.enumU8( kLangCount ); b.kind = r.enumU8( kLocalBindKindCount ); b.isFromAssignment = r.u8() != 0; b.spanStart = r.u32(); b.spanEnd = r.u32(); b.var = r.str(); b.typeName = r.str(); b.importedName = r.str(); return b; } inline void writeFfi( ByteW& w, const BindingAlias& a ) { w.u8( std::uint8_t( a.kind ) ); w.u8( a.lowConf ? 1 : 0 ); w.str( a.aliasName ); w.str( a.targetName ); w.str( a.targetScope ); } @@ -2760,7 +2797,12 @@ inline HashMap loadCache( const std::string& path, std:: } fileBuf.resize( std::size_t( spanEnd - spanStart ) ); - if( !preadExact( frame.blob.fd, fileBuf.data(), fileBuf.size(), spanStart ) ) + bool spanRead = false; + { + PROFILE_SCOPE_DESCRIBE( "ingest/loadCache: pread one coalesced span" ); + spanRead = preadExact( frame.blob.fd, fileBuf.data(), fileBuf.size(), spanStart ); + } + if( !spanRead ) { DISCLOSE( Diagnostics::answerUnchanged, "the affected files are reparsed from source: the answer is byte-identical, only slower", "ingest: cache blob read failed mid-load — the unread records are reparsed" ); @@ -2771,7 +2813,12 @@ inline HashMap loadCache( const std::string& path, std:: { const CacheEntry& e = frame.entries[ wanted[i].entryIndex ]; const char* rec = fileBuf.data() + std::size_t( e.recOffset - spanStart ); - if( recordSum32( std::string_view( rec, e.recLength ) ) != e.recSum ) + bool recordSumOk = false; + { + PROFILE_SCOPE_DESCRIBE( "ingest/loadCache: record digest (recordSum32)" ); + recordSumOk = recordSum32( std::string_view( rec, e.recLength ) ) == e.recSum; + } + if( !recordSumOk ) { // A record torn on its own while the table survived: drop THIS file (it reparses) and // keep the rest of the blob. The table is what must be trusted whole, not each record. @@ -2782,7 +2829,12 @@ inline HashMap loadCache( const std::string& path, std:: ByteR r{ rec, rec + e.recLength }; std::string rel; FileFacts ff; - if( !readFileRecord( r, captureValueUses, dictScratch, rel, ff ) ) + bool decoded = false; + { + PROFILE_SCOPE_DESCRIBE( "ingest/loadCache: decode one record (readFileRecord)" ); + decoded = readFileRecord( r, captureValueUses, dictScratch, rel, ff ); + } + if( !decoded ) { continue; // corrupt record → that file reparses (readFileRecord already disclosed) } diff --git a/src/ingest_crawl.h b/src/ingest_crawl.h index d20cc2da4..ad5503360 100644 --- a/src/ingest_crawl.h +++ b/src/ingest_crawl.h @@ -372,6 +372,10 @@ SymKind defKind( std::string_view tail ) noexcept { return SymKind::Function; } + if( tail == "classfactory" ) // JS/TS `const Sub = Base.extend('Sub', …)` — gated (isJsClassFactory) + { + return SymKind::Class; + } if( tail == "protomethod" ) // JS `Foo.prototype.NAME = fn` — gated (isPrototypeMemberTarget) { return SymKind::Method; @@ -1392,49 +1396,99 @@ bool underGitRoot( const char* rootDir ) } } -GitIgnoreSet collectGitIgnored( const char* rootDir ) +// The probe's PROCESS half, split from its parse so it can run off the main thread (IgnoreProbeSpeculation below): one +// `git ls-files --others --ignored` popen, read whole, classified exactly as collectGitIgnored always classified it — +// a non-zero exit outranks an overflow (the overflow break still reaches pclose, and its status is read first). No +// DISCLOSE and no parse here: the parse below runs on the thread that applies the answer, so the disclosure ledger +// sees exactly the degrades of the answer the crawl used. +struct GitIgnoreProbeRaw { - GitIgnoreSet out; + enum class State : std::uint8_t + { + NotGit, // no `.git` above the root: no git child was started (nongitqmetricscheck) + NotRunnable, // popen failed + Failed, // git exited non-zero + OverCeiling, // more than kMaxIgnoreProbeBytes + Answered, // `bytes` is git's whole -z answer + }; + State state = State::NotGit; + std::string bytes; + bool operator==( const GitIgnoreProbeRaw& ) const = default; +}; + +GitIgnoreProbeRaw runGitIgnoreProbe( const char* rootDir ) +{ + PROFILE_SCOPE_DESCRIBE( "ingest: git ignore probe (the git process; off the main thread when speculated)" ); + GitIgnoreProbeRaw raw; if( !underGitRoot( rootDir ) ) { - return out; + return raw; } const std::string cmd = gitCmd( " -C " ) + shSingleQuote( rootDir == nullptr ? std::string( "." ) : std::string( rootDir ) ) + " -c core.quotepath=false ls-files --others --ignored --exclude-standard --directory -z 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( pipe == nullptr ) { - DISCLOSE( out, GitIgnoreSet::DisclosureWhy::GitNotRunnable, "ingest: cannot run git for the ignore probe — full walk" ); - return out; + raw.state = GitIgnoreProbeRaw::State::NotRunnable; + return raw; } - std::string buf; - char chunk[ 8192 ]; - bool overflowed = false; + char chunk[ 8192 ]; + bool overflowed = false; for( std::size_t n = std::fread( chunk, 1, sizeof( chunk ), pipe ); n > 0; n = std::fread( chunk, 1, sizeof( chunk ), pipe ) ) { - if( buf.size() + n > kMaxIgnoreProbeBytes ) + if( raw.bytes.size() + n > kMaxIgnoreProbeBytes ) { overflowed = true; break; } - buf.append( chunk, n ); + raw.bytes.append( chunk, n ); } const int rc = os::pclose( pipe ); - if( rc != 0 ) + raw.state = rc != 0 ? GitIgnoreProbeRaw::State::Failed : overflowed ? GitIgnoreProbeRaw::State::OverCeiling : GitIgnoreProbeRaw::State::Answered; + if( raw.state != GitIgnoreProbeRaw::State::Answered ) { - // Not a git work tree, or no git binary — the DESIGNED degrade: the fallback (a full walk, no - // ignore-set pruning) is safe either way. `available` is already false here (its default), so this - // changes no output — it disclose()s the fact for the self-check ledger instead of leaving the - // degrade to a bare `return`, matching the GitNotRunnable/ProbeOverCeiling sites just above/below. - DISCLOSE( out, GitIgnoreSet::DisclosureWhy::ProbeFailed, "ingest: the git ignore probe exited non-zero — full walk" ); - return out; + raw.bytes.clear(); // only an answer carries bytes, so two probes that failed the same way compare equal } - if( overflowed ) + ENSURES( raw.state == GitIgnoreProbeRaw::State::Answered || raw.bytes.empty(), "a probe that did not answer carries no bytes" ); + return raw; +} + +// The probe's PARSE half: every degrade disclosed exactly where collectGitIgnored disclosed it, then the -z entries. +GitIgnoreSet parseGitIgnoreProbe( const GitIgnoreProbeRaw& raw ) +{ + GitIgnoreSet out; + switch( raw.state ) { - DISCLOSE( out, GitIgnoreSet::DisclosureWhy::ProbeOverCeiling, "ingest: git ignore probe exceeded its byte ceiling — full walk" ); - return out; + case GitIgnoreProbeRaw::State::NotGit: + { + return out; + } + case GitIgnoreProbeRaw::State::NotRunnable: + { + DISCLOSE( out, GitIgnoreSet::DisclosureWhy::GitNotRunnable, "ingest: cannot run git for the ignore probe — full walk" ); + return out; + } + case GitIgnoreProbeRaw::State::Failed: + { + // Not a git work tree, or no git binary — the DESIGNED degrade: the fallback (a full walk, no + // ignore-set pruning) is safe either way. `available` is already false here (its default), so this + // changes no output — it disclose()s the fact for the self-check ledger instead of leaving the + // degrade to a bare `return`, matching the GitNotRunnable/ProbeOverCeiling sites just above/below. + DISCLOSE( out, GitIgnoreSet::DisclosureWhy::ProbeFailed, "ingest: the git ignore probe exited non-zero — full walk" ); + return out; + } + case GitIgnoreProbeRaw::State::OverCeiling: + { + DISCLOSE( out, GitIgnoreSet::DisclosureWhy::ProbeOverCeiling, "ingest: git ignore probe exceeded its byte ceiling — full walk" ); + return out; + } + case GitIgnoreProbeRaw::State::Answered: + { + break; + } } + const std::string& buf = raw.bytes; for( std::size_t i = 0; i < buf.size(); ) { const std::size_t nul = buf.find( '\0', i ); @@ -1473,6 +1527,119 @@ GitIgnoreSet collectGitIgnored( const char* rootDir ) return out; } +GitIgnoreSet collectGitIgnored( const char* rootDir ) +{ + return parseGitIgnoreProbe( runGitIgnoreProbe( rootDir ) ); +} + +// ── the ignore probe, SPECULATED (cli-perf-068) ─────────────────────────────────────────────────────── +// +// The probe is one git process on every call — ~60 ms of a ~400 ms warm call on this repository, measured, and most of +// it git walking the same tree the crawl is about to walk. Its answer rarely changes between two calls, but WHAT can +// change it is wide: any .gitignore in the tree, .git/info/exclude, the index, core.excludesFile and the config that +// names it, and the untracked files themselves (a new `x.log` under an existing `*.log` rule changes the answer while +// no ignore FILE changed). A cache keyed on the files that hold rules would miss that last one; this does not key on +// anything. It SPECULATES: the crawl walks with the answer the previous call got for this root (a sidecar under the +// cache dir) while git computes this call's answer on a worker, and ingest() compares the two — byte for byte, the +// raw -z stream — before anything downstream of the crawl has run (crawlStands). Equal: the crawl that ran IS the +// crawl the fresh answer gives, so nothing is redone. Different: the crawl and the cache load are run again on the +// fresh answer, and the sidecar is replaced. Every answer is therefore git's own answer for THIS call, exactly as +// before; what is saved is the wait — git runs beside the walk and the cache load instead of before them. +// +// Spawn conditions are the ones the probe always had: it starts at the same point of collectSources (a directory root +// whose walk opened), only under a `.git` (no child at all otherwise), and not under --no-ignore. With the cache off +// (--no-cache) there is no sidecar, so the probe runs on the calling thread, as before. One per ingest() call. +class IgnoreProbeSpeculation +{ +public: + explicit IgnoreProbeSpeculation( bool sidecarEnabled ) noexcept : sidecarEnabled_( sidecarEnabled ) {} + + // collectSources' one consult, at the point the probe always ran. On the redo pass (after a lost speculation) it + // hands back the fresh answer the join already holds. + GitIgnoreSet setFor( const char* rootDir, const std::string& rootReal ) + { + if( fresh_ ) + { + return parseGitIgnoreProbe( *fresh_ ); + } + if( !sidecarEnabled_ || !underGitRoot( rootDir ) ) + { + fresh_ = runGitIgnoreProbe( rootDir ); // no sidecar to speculate from, or no git child to overlap: as before + return parseGitIgnoreProbe( *fresh_ ); + } + // Named by the house root key (quality::cacheRootKeyHex, realpath-normalized), so the eviction sweep pins this + // file with every other family of the root (test/evictioncheck.sh (k)/(l)); the header still names the root. + sidecarPath_ = quality::resolveCacheBlobPath( quality::cacheDirLadder(), "ripwire-ignoreprobe-" + quality::cacheRootKeyHex( rootDir ) + ".bin" ); + sidecarHeader_ = std::string( kSidecarMagic ) + rootReal + '\0'; + pending_ = std::async( std::launch::async, [ root = std::string( rootDir ) ] { return runGitIgnoreProbe( root.c_str() ); } ); + speculation_ = loadSidecar(); + if( !speculation_ ) + { + return parseGitIgnoreProbe( join() ); // nothing to speculate from (first call on this root): wait, as before + } + return parseGitIgnoreProbe( *speculation_ ); + } + + // ingest()'s check, before the parse pool: true when the crawl that ran used this call's own git answer — the fresh + // answer itself, or a speculation byte-identical to it. False means the caller redoes the crawl (setFor now returns + // the fresh answer). Publishes the fresh answer for the next call whenever it differs from what was speculated. + bool crawlStands() + { + PROFILE_SCOPE_DESCRIBE( "ingest: ignore probe join (wait for git, compare with the speculation)" ); + if( pending_.valid() ) + { + (void)join(); + } + if( !speculation_ ) + { + return true; // the crawl waited for the fresh answer, or no probe ran at all + } + const bool stands = *speculation_ == *fresh_; + speculation_.reset(); // decided: a redo pass reads fresh_, and a second call to this is a no-op + return stands; + } + +private: + static constexpr std::string_view kSidecarMagic = "ripwire-ignore-speculation/1\n"; + + // The previous call's answer for this root, or nothing. The header names the root in full, so two roots whose names + // hash alike never read each other's answer; anything unreadable or misshapen is simply no speculation. + std::optional loadSidecar() const + { + std::optional blob = pathguard::readRegularFileNoFollow( sidecarPath_ ); + if( !blob || blob->size() > sidecarHeader_.size() + kMaxIgnoreProbeBytes || !blob->starts_with( sidecarHeader_ ) ) + { + return std::nullopt; + } + return GitIgnoreProbeRaw{ GitIgnoreProbeRaw::State::Answered, blob->substr( sidecarHeader_.size() ) }; + } + + const GitIgnoreProbeRaw& join() + { + fresh_ = pending_.get(); // a throw on the worker (allocation) surfaces here, where the synchronous probe threw + if( fresh_->state == GitIgnoreProbeRaw::State::Answered && ( !speculation_ || !( *speculation_ == *fresh_ ) ) ) + { + if( !quality::atomicWriteFile( sidecarPath_, sidecarHeader_ + fresh_->bytes ) ) + { + DISCLOSE( Diagnostics::answerUnchanged, "this call used git's own ignore answer; only the next call's speculation is not saved", + "ingest: the ignore-probe sidecar could not be written — the next call waits for git, as before" ); + } + } + else if( fresh_->state != GitIgnoreProbeRaw::State::Answered && speculation_ ) + { + os::unlink( sidecarPath_.c_str() ); // git stopped answering here: a stale answer must not be speculated on again + } + return *fresh_; + } + + bool sidecarEnabled_ = false; + std::string sidecarPath_; + std::string sidecarHeader_; + std::future pending_; + std::optional speculation_; + std::optional fresh_; +}; + // Sorted-vector membership. A binary search over a contiguous, already-sorted vector beats a hash map at // these sizes and costs no allocation, and — unlike a HashMap — has no iteration order that could reach // output (the G2 container rule's standing caveat). @@ -1503,14 +1670,17 @@ bool pathInIgnoreSet( const std::vector& sorted, std::string_view r // §N6-C — the probe AND the mode it implies, as one decision, so collectSources reads the answer instead // of computing it. One fork per root, paid only for a directory root the walk actually opened: the probe // is worthless on a single-file root and must not be charged to a root the walk already refused. -GitIgnoreSet probeIgnoreSet( const char* rootDir, bool respectGitignore, IgnoreMode& modeOut ) +// `speculation` (ingest()'s, see IgnoreProbeSpeculation) lets the git process run beside the walk; without one the probe +// runs here, on this thread, as it always did. +GitIgnoreSet probeIgnoreSet( const char* rootDir, bool respectGitignore, IgnoreMode& modeOut, IgnoreProbeSpeculation* speculation, + const std::string& rootReal ) { if( !respectGitignore ) { return {}; // modeOut was already set to Off by the caller, before any early return could skip it } PROFILE_SCOPE_DESCRIBE( "ingest: crawl (git ignore probe)" ); - GitIgnoreSet set = collectGitIgnored( rootDir ); + GitIgnoreSet set = speculation != nullptr ? speculation->setFor( rootDir, rootReal ) : collectGitIgnored( rootDir ); modeOut = set.available ? IgnoreMode::Git : set.rootIgnored ? IgnoreMode::RootIgnored : IgnoreMode::Unavailable; return set; } @@ -1567,7 +1737,7 @@ struct CrawlResult // finished walk's list is, so a partial corpus is still a deterministic one; ingest() discloses the stop. CrawlResult collectSources( const char* rootDir, const std::vector& excludeSubstr, std::size_t maxFileBytes, std::string_view excludeLabel = {}, bool respectGitignore = true, - memguard::Watch* memWatch = nullptr ) + memguard::Watch* memWatch = nullptr, IgnoreProbeSpeculation* ignoreSpeculation = nullptr ) { std::vector out; std::vector skipped; @@ -1625,7 +1795,7 @@ CrawlResult collectSources( const char* rootDir, const std::vector& return { std::move( out ), std::move( skipped ), std::move( skips ) }; } - const GitIgnoreSet ignoreSet = probeIgnoreSet( rootDir, respectGitignore, skips.ignoreMode ); // §N6-C + const GitIgnoreSet ignoreSet = probeIgnoreSet( rootDir, respectGitignore, skips.ignoreMode, ignoreSpeculation, rootReal ); // §N6-C const fs::recursive_directory_iterator end; { diff --git a/src/ingest_docpass.h b/src/ingest_docpass.h index 0c98717d0..893c3cd39 100644 --- a/src/ingest_docpass.h +++ b/src/ingest_docpass.h @@ -64,11 +64,17 @@ inline void publishDocBridgeBlob( const std::string& bridgeBlobPath, std::uint32 // cacheEnabled=false (--no-cache) bypasses the sidecar entirely. tmpKey keeps concurrent workers' // unpublished temp files distinct; the publish itself is a whole-file rename, so a concurrent // reader sees every byte or none. +// A bridge doc whose bytes have no cached extraction asks docparse::markitdownMayRun() before the popen: false means +// the shell could not start markitdown, so the answer is "" without the popen — what the popen answered (rc 127), +// minus one shell start per doc per run. The cache is still consulted first, so a blob extracted while markitdown +// was installed keeps answering after it is removed, exactly as before. inline std::string docTextViaBridgeCache( const std::string& path, const std::string& ext, bool cacheEnabled, std::uint32_t tmpKey ) { + EXPECTS( !path.empty(), "doc post-pass: every crawled doc path is a non-empty spelling" ); std::string text; std::string bridgeBlobPath; - if( cacheEnabled && docparse::docKindOf( ext ) == docparse::DocKind::Markitdown ) + const bool viaBridge = docparse::docKindOf( ext ) == docparse::DocKind::Markitdown; + if( cacheEnabled && viaBridge ) { if( const std::optional docBytes = docparse::detail::readWholeFile( path ) ) { @@ -79,6 +85,10 @@ inline std::string docTextViaBridgeCache( const std::string& path, const std::st text = docparse::detail::readWholeFile( bridgeBlobPath ).value_or( std::string() ); // miss ⇒ text stays empty } } + if( text.empty() && viaBridge && !docparse::markitdownMayRun() ) + { + return text; // no cached extraction, and no markitdown the shell could start: the popen's own answer, "" + } if( text.empty() ) { text = docparse::parseDocFile( path, ext ); diff --git a/src/ingest_importcap.h b/src/ingest_importcap.h index ffe957316..b0cacc31f 100644 --- a/src/ingest_importcap.h +++ b/src/ingest_importcap.h @@ -25,26 +25,35 @@ // // THE VOCABULARY IS A FAMILY, AND ONLY ITS FIRST MEMBER IS SPENT. `@import.path` is the only capture name // in use, because C-family imports have no other part: `#include` names a path and nothing else — no -// alias to record, no list of imported names. The other two names the issue sketches are reserved BY NAME -// so the next language lands on them rather than inventing a spelling: `@import.alias` is the local name an -// import renames a target to (`import a.b as c`, `use Foo\Bar as Baz`, `using X = Foo.Bar`), and +// alias to record, no list of imported names — and because Go's alias/dot/blank spellings ride inside the +// declaration clause its Include has always carried (see DepDialect::Go below), not in a field of their own. +// The other two names the issue sketches are reserved BY NAME so the next language lands on them rather +// than inventing a spelling: `@import.alias` is the local name an import renames a target to (`import a.b as c`, `use Foo\Bar as Baz`, `using X = Foo.Bar`), and // `@import.names` is a group of targets ONE directive names (`use Foo\{A, B}`, `alias MyApp.{Bar, Baz}`), // which is why both of those are emitted by the C++ side today through a per-directive helper rather than // by one capture — a capture names one node, and a group is many. No enum value reserves them: an unused // arm that no query can reach is a lie about what the vocabulary supports. // +// SECOND MEMBER: DepDialect::Web (TypeScript, TSX, JavaScript, the .astro frontmatter that rides TypeScript). +// Its three spellings are three patterns on the SAME capture name — `import … from 'x'`, a re-export +// `export … from 'x'`, and the CommonJS / dynamic call `require('x')` / `import('x')` — and one normaliser. +// Neither `@import.alias` nor `@import.names` is used for it, for the same reason C-family has none: the +// Include record a TS/JS import produces names a MODULE and nothing else. The bound names (`import { a as b }`) +// are a different record — captureJsImportFacts' RawBinds, in src/ingest_jsimports.h — which this slice does +// not touch, so there is no current JS/TS semantics for those two names to carry yet. +// // ORDERING. ts_query_cursor_next_match yields matches by the match's start byte, so a capture arrives in // SOURCE order and the emitted Includes do too — the same order captureIncludes' walk produced, because // emitCapturedImport runs straight from the capture dispatch with nothing staging it in between. The // import-role use-site refs move into the tags pass's own window, where orderReferences re-sorts them by // (startByte, name, role, isInherit) — a total key, so the move cannot reorder a published row. // -// WHAT THE WALK STILL OWNS, AND DOES NOT TRAVEL WITH THE MOVE. Three things captureIncludes computed from +// WHAT THE WALK USED TO OWN, AND DOES NOT TRAVEL WITH THE MOVE. Three things captureIncludes computed from // its own frame are re-derived here rather than dropped: the import-container nesting bound -// (importContainerDepth + the same DISCLOSE, below), the lazy bit a TS/JS require needs from `insideFn`, -// and Ruby's innermost-open index for the constant-receiver dedupe. The first is recovered now because it -// is one parent hop per import; the other two are why a language still on the walk cannot move until the -// emission can re-derive them from ancestry too — the tags pass never sees a walk frame. +// (importContainerReach + the same DISCLOSE, below), the lazy bit a TS/JS require needs from `insideFn` +// (the same ancestor loop, one more bit), and Ruby's innermost-open index for the constant-receiver dedupe. +// The first two are recovered because each is one parent hop per import; the third is why Ruby cannot move +// until the emission can re-derive it from ancestry too — the tags pass never sees a walk frame. #include "depdialect.h" // DepDialect + dependencyDialect — the unit one normaliser is written per @@ -54,8 +63,16 @@ namespace rw namespace { -// The import-container REACH of a captured directive, and its DEPTH — the walk's own two rules, -// re-derived from the captured node because the walk no longer runs for this language. +// What importContainerReach reads off one directive's ancestry. +struct ImportReach +{ + std::uint16_t depth = 0; // containers above the directive, stopped at kMaxImportContainerDepth + 1 + bool reachable = true; // every node between the file root and the directive is an import container + bool insideFn = false; // some such container is a function body (kJsFunctionContainers): TS/JS lazy bit +}; + +// The import-container REACH of a captured directive, its DEPTH and its function-body bit — the walk's own +// rules, re-derived from the captured node because the walk no longer runs for this language. // // REACH. captureIncludes seeded the frame stack with root's DIRECT children and then entered only // `isImportContainer` nodes, so a directive was reachable exactly when every node between the file root @@ -69,29 +86,57 @@ namespace // So reach is restored here and the widening is left as its own follow-up. // // The file root is deliberately NOT tested: its children are what the walk always started from, so -// `isImportContainer(root)` being false must not reject a file-scope include. The loop stops when -// `p`'s own parent is null, which is the root. -std::uint16_t importContainerReach( TSNode directive, Lang lang, bool& reachable ) noexcept +// `isImportContainer(root)` being false must not reject a file-scope include. The ancestors read are the +// nodes STRICTLY BETWEEN the root and the directive. +// +// ONE DESCENT FROM THE ROOT, not an upward walk. tree-sitter nodes hold no parent pointer, so every +// ts_node_parent is itself a descent from the root and walking k ancestors up costs k of them (the precedent +// and its measurement: ingest_names.h::enclosingFunctionScope — 600 nested blocks, 19.7 s upward against +// 9.4 s down). The first version of this function walked up, bounded to kMaxImportContainerDepth hops, and +// that bound capped the HOPS, not the cost: a TS/JS file of 2 000 nested `foo( "x", foo( "x", … ) )` calls +// (a hyperscript tree; every one matches the call pattern) took 152 s against 0.5 s for the walk it replaced. +// Descending once is O(depth) per directive, and what the upward loop answered is read off the same descent: +// `run` is how many container ancestors sit directly above the directive (the loop's reach before it met a +// non-container or the bound), `total` is how many ancestors there are. Reachable means the run is all of them. +// A run PAST the bound is reported by `depth` (clamped one past it), which the caller tests BEFORE `reachable`: the +// upward loop stopped at the bound without ever seeing a non-container above it, so a too-deep import is announced +// whether or not the walk would have entered it — the one LOUDER disclosure difference test/importcapcheck.sh pins. +ImportReach importContainerReach( TSNode directive, Lang lang ) noexcept { - std::uint16_t depth = 0; - reachable = true; - for( TSNode p = ts_node_parent( directive ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + const bool directiveIsNode = !ts_node_is_null( directive ); // hoisted: a promise holds no call (selfcheckcheck C) + EXPECTS( directiveIsNode, "the directive is a node of the parsed tree: importDirectiveOf never returns null for a captured specifier" ); + std::uint32_t total = 0; + std::uint32_t run = 0; // container ancestors directly above the directive (reset by a non-container) + bool fnInRun = false; // some container in `run` is a function body + TSNode n = ts_node_child_with_descendant( ts_tree_root_node( directive.tree ), directive ); + for( ; !ts_node_is_null( n ) && !ts_node_eq( n, directive ); n = ts_node_child_with_descendant( n, directive ) ) { - if( ts_node_is_null( ts_node_parent( p ) ) ) + const char* t = ts_node_type( n ); + ++total; + if( isImportContainer( lang, t ) ) { - break; // p is the file root: reached unconditionally + ++run; + // The walk's `childInsideFn = frame.insideFn || isFunctionLike( lang, t )` is sticky downward, so the + // directive is inside a function exactly when ANY container above it is a function-body kind. + fnInRun = fnInRun || isFunctionLike( lang, t ); } - if( !isImportContainer( lang, ts_node_type( p ) ) ) + else { - reachable = false; // the walk would not have entered this node - break; + run = 0; + fnInRun = false; } - if( ++depth > kMaxImportContainerDepth ) - { - break; // STOPS AT THE BOUND: past it the answer cannot change, and - } // a hostile file must not buy an unbounded parent walk } - return depth; + ImportReach reach; + if( ts_node_is_null( n ) ) + { + reach.reachable = false; // the descent lost the directive: read as "the walk would not have entered", a floor + return reach; + } + reach.depth = static_cast( std::min( run, kMaxImportContainerDepth + 1u ) ); + reach.reachable = ( run == total ); + reach.insideFn = fnInRun; + ENSURES( reach.depth <= kMaxImportContainerDepth + 1, "the depth is clamped one past the bound" ); + return reach; } // One captured `@import.path`, normalised. `text` empty is the "not an import after all" signal and the @@ -101,6 +146,7 @@ struct ImportSpec { std::string text; bool isAngle = false; // CFamily: is external (unresolvable without a build system), "x.h" is not + bool lazyInClosure = false; // Web: this is a require()/import() CALL, so it is Include::isLazy iff it sits in a function body }; // ── DepDialect::CFamily ─────────────────────────────────────────────────────────────────────────────── @@ -131,8 +177,58 @@ ImportSpec normaliseCFamilyImport( TSNode directive, std::string_view raw, std:: return out; } +// ── DepDialect::Web ────────────────────────────────────────────────────────────────────────────────── +// TypeScript, TSX, JavaScript and the .astro frontmatter. Two directive shapes reach this normaliser, told +// apart by the DIRECTIVE node (the captured string's statement, or its call): +// import_statement / export_statement — `import … from 'x'`, `import 'x'`, `import type …`, and the re-exports +// `export * from`, `export * as n from`, `export { a as b } from`, `export type { T } from`. The grammar +// gives all of them the same `source:` string, which is the whole capture; every other export_statement +// has no `source:` and is not matched by the query at all. +// call_expression — the CommonJS `require('x')` and the dynamic `import('x')`. A query cannot say "the +// callee's TEXT is require" (tags-pass predicates never run), so the three guards that keep an ordinary +// call out of the dependency graph stay C++: jsModuleLoadTarget (src/ingest_relations.h), UNCHANGED by +// this round and shared with captureJsImportFacts, reads the callee text, the argument count and the +// string. The query only says where a candidate call is; the guards say whether it is a module load. +// Whether the hit is LAZY (written inside a function body) is the ancestry's answer, not the text's: this +// arm only says the directive is a call (`lazyInClosure`), and emitCapturedImport ANDs it with the walk up. +// `import x = require('y')` (TS) is `import_require_clause` with its own `source:` and matches no pattern — +// the extractor never read it either, so it is still not an edge: a disclosed floor, unchanged by the round. +ImportSpec normaliseWebImport( TSNode directive, std::string_view raw, std::string_view src ) +{ + ImportSpec out; + if( kindIs( ts_node_type( directive ), "call_expression" ) ) + { + out.text = jsModuleLoadTarget( directive, src ); + out.lazyInClosure = !out.text.empty(); // kParserVer 72: a hit found inside a function body is LAZY (the caller knows where it sits) + return out; + } + out.text = std::string( pattern::stripQuotePair( raw ) ); // the captured node is a `string`: strip the one quote pair + return out; +} + +// ── DepDialect::Go ─────────────────────────────────────────────────────────────────────────────────── +// A Go import is ONE `import_declaration` — `import "fmt"`, `import f "fmt"` (aliased), `import . "x"` (dot), +// `import _ "x"` (blank), cgo's `import "C"`, or a parenthesised group of any of those — and the Include +// target it has always carried is the declaration's own CLAUSE (importClauseTarget, shared with the one +// language that still reads it from the walk), not a per-spec specifier. So the captured node is the +// declaration itself (queries/go/tags.scm: `(import_declaration) @import.path`) and the normaliser is the +// clause reader, nothing more. A GROUPED import therefore stays ONE record whose text is the whole group cut +// at 96 bytes, exactly as before: resolve.h's resolveGoImport documents that ("grouped imports collapse to +// one node upstream and are left unresolved"), and splitting a group into one record per spec would add +// edges — a behaviour change, so its own slice. The alias / dot / blank spellings are the same: they ride +// INSIDE the clause text (`f "fmt"`), which is why `@import.alias` and `@import.names` are NOT used here — +// today's Go semantics have no separate alias or names field on an Include. (The alias a Go import binds is +// a different record altogether, the ModuleAlias RawBind captureGoImportFacts writes; it is not a dependency +// edge and is left where it is.) +ImportSpec normaliseGoImport( TSNode declaration, std::string_view src ) +{ + ImportSpec out; + out.text = importClauseTarget( declaration, src ); + return out; +} + // ── the dispatch: ONE normaliser per DepDialect ────────────────────────────────────────────────────── -// CFamily is the only dialect that has moved over so far; every other arm returns an empty ImportSpec, +// CFamily, Web and Go are the dialects that have moved over so far; every other arm returns an empty ImportSpec, // which is what makes the capture INERT rather than wrong on a language whose tags.scm has not adopted // `@import.path` yet. Each `case` is deleted as its language moves, and the compiler then names every // other dialect still to do — the same "one language at a time" landing the issue asks for. @@ -141,10 +237,46 @@ ImportSpec normaliseImportSpecifier( DepDialect dialect, TSNode directive, std:: switch( dialect ) { case DepDialect::CFamily: return normaliseCFamilyImport( directive, raw, src ); - default: return {}; // no language but C-family captures @import.path yet + case DepDialect::Web: return normaliseWebImport( directive, raw, src ); + case DepDialect::Go: return normaliseGoImport( directive, src ); + default: return {}; // no other language captures @import.path yet } } +// True for a dialect whose directives come from `@import.path` and NOT from captureIncludes' walk. The one +// place the walk is switched off (captureSideFacts) asks this, so a dialect cannot be moved in the normaliser +// above and still be walked a second time, or walked-off without a normaliser. +inline bool importsFromCapture( DepDialect dialect ) noexcept +{ + switch( dialect ) // one case per dialect normaliseImportSpecifier has an arm for + { + case DepDialect::CFamily: + case DepDialect::Web: + case DepDialect::Go: + return true; + default: + return false; + } +} + +// The DIRECTIVE a captured specifier belongs to — the node both records are SITED at. For a C include or a +// JS import/re-export it is the specifier's parent; for a call it is the call, one hop further, because the +// specifier sits in the call's `arguments`. +TSNode importDirectiveOf( DepDialect dialect, TSNode pathNode ) noexcept +{ + if( dialect == DepDialect::Go ) + { + return pathNode; // the Go capture IS the whole import_declaration + } + const TSNode parent = ts_node_parent( pathNode ); + const bool hasParent = !ts_node_is_null( parent ); + ASSUME( hasParent, "a captured specifier is a string or path token, never the file root" ); + const TSNode directive = kindIs( ts_node_type( parent ), "arguments" ) ? ts_node_parent( parent ) : parent; + const bool hasDirective = !ts_node_is_null( directive ); + ENSURES( hasDirective, "an `arguments` list always belongs to a call" ); + return directive; +} + // One captured `@import.path` → the Include record plus its ABS-3 import-role use-site ref, which is // exactly the pair captureIncludes' `emitDirective` lambda minted for a C-family directive. // @@ -178,23 +310,28 @@ ImportSpec normaliseImportSpecifier( DepDialect dialect, TSNode directive, std:: void emitCapturedImport( TSNode pathNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& includes, std::vector& refs, ExtractShortfall& shortfall ) { - const TSNode directive = ts_node_parent( pathNode ); - bool reachable = false; - if( importContainerReach( directive, lang, reachable ) > kMaxImportContainerDepth ) + const TSNode directive = importDirectiveOf( dependencyDialect( lang ), pathNode ); + // TEXT FIRST, ancestry second. The Web call pattern matches every bare `f( "s" … )`, so most captures here are + // not imports at all, and the reach read below is a descent from the root — paying it to learn that `t( "key" )` + // is not a dependency is the cost that made a 2 000-deep hyperscript tree 300x slower. Dropping a non-import + // BEFORE the bound test also means a `#pragma once` / `foo( "x" )` under a too-deep nest no longer announces + // "an import was cut" when none was (the walk announced on any too-deep container, import or not). + const ImportSpec spec = normaliseImportSpecifier( dependencyDialect( lang ), directive, nodeTextOf( pathNode, src ), src ); + if( spec.text.empty() ) + { + return; + } + const ImportReach reach = importContainerReach( directive, lang ); + if( reach.depth > kMaxImportContainerDepth ) { DISCLOSE( shortfall, ExtractShortfall::DisclosureWhy::ImportNestingTooDeep, "ingest: import-container nesting past the depth bound — deeper imports not captured" ); return; // the same degrade the walk performed: not captured, file still indexed } - if( !reachable ) + if( !reach.reachable ) { return; // a non-container ancestor the walk would never have entered — see importContainerReach } - const ImportSpec spec = normaliseImportSpecifier( dependencyDialect( lang ), directive, nodeTextOf( pathNode, src ), src ); - if( spec.text.empty() ) - { - return; - } if( std::string name = importName( spec.text ); !name.empty() ) { RawRef r; @@ -205,7 +342,7 @@ void emitCapturedImport( TSNode pathNode, std::uint32_t fileId, Lang lang, std:: r.name = std::move( name ); refs.push_back( std::move( r ) ); } - includes.push_back( { fileId, spec.isAngle, false, false, ts_node_start_byte( directive ), false, spec.text } ); + includes.push_back( { fileId, spec.isAngle, spec.lazyInClosure && reach.insideFn, false, ts_node_start_byte( directive ), false, spec.text } ); } } // namespace diff --git a/src/ingest_metrics.h b/src/ingest_metrics.h index ba093e357..23f1b685b 100644 --- a/src/ingest_metrics.h +++ b/src/ingest_metrics.h @@ -1213,7 +1213,8 @@ struct Complexity { std::uint32_t cx; std::uint32_t ccx; std::uint32_t maxNest; // per node and gets locals=0, which the caller (this file, RawDef→Symbol) leaves at 0 and serialize.h // never emits (absent, not a bare "0" — see localsCountedLang's own comment). inline Complexity complexityOf( TSNode root, std::string_view src, Lang lang ) // one fused DFS → cx, ccx, maxNest, locals, ppAlt, the nesting profile, AND ev -{ // A4-F25: NOT noexcept — cc_walk (and kids here) allocate +{ + PROFILE_SCOPE_DESCRIBE( "ingest/metrics: complexityOf one function (fused DFS)" ); // A4-F25: NOT noexcept — cc_walk (and kids here) allocate CcAccum acc; const bool countLocals = localsCountedLang( lang ); // essential complexity rides the SAME walk (zero new tree-sitter queries). ONE arena/label/pending set diff --git a/src/ingest_model.h b/src/ingest_model.h index 02a5d3815..1d14889f6 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -928,6 +928,7 @@ inline void emitReferences( IngestResult& result, std::vector& rawRefs, ref.memberCtor = std::move( r.memberCtor ); // FE-B: the class a constructed receiver names ref.memberVia = std::move( r.memberVia ); // FE-B on #373: the method a Ruby typed receiver was built by ref.memberFactory = r.memberFactory; // FE-B on #373: memberCtor names a FactoryBot factory + ref.baseBind = r.baseBind; // lane lego-transitive fix 1: an inherit ref's same-file base binding ref.fieldName = std::move( r.fieldName ); // S5-E: the member variable name (e.g. "m_pool") ref.composeRel = std::move( r.composeRel ); // S5-E: "creates" or "uses" ref.startByte = r.startByte; // shadow fix round: for the block-span containment test diff --git a/src/ingest_names.h b/src/ingest_names.h index 0f3d19b3d..cef8eb1d0 100644 --- a/src/ingest_names.h +++ b/src/ingest_names.h @@ -1163,9 +1163,18 @@ inline void captureRubyAttrDefsCall( TSNode n, std::uint32_t fileId, std::string } ); } -inline void captureRubyAttrDefs( TSNode root, std::uint32_t fileId, std::string_view src, std::vector& defs ) -{ - if( src.find( "attr" ) == std::string_view::npos ) // file signal: every family name contains "attr" +// ONE pre-order walk of the whole tree dispatching BOTH Ruby def-side lanes — the class-level +// attribute DSL and the rendered-schema column capture — from the same explicit stack (the duplication +// the maintainer's quality-delta flagged: two byte-identical walkers). The per-file signals (contains +// "attr" / contains "create_table") decide which dispatch fires before the walk starts, exactly like +// each lane's former early-out. Everything else in each lane is unchanged. +// The schema table helper is defined with its lane below (this walker lives with the attr lane). +inline void rubySchemaTableCall( TSNode n, std::uint32_t fileId, std::string_view src, std::vector& defs ); +inline void captureRubyDefs( TSNode root, std::uint32_t fileId, std::string_view src, std::vector& defs ) +{ + const bool wantAttr = src.find( "attr" ) != std::string_view::npos; // every family name contains "attr" + const bool wantSchema = src.find( "create_table" ) != std::string_view::npos; // the schema gate name must appear + if( !wantAttr && !wantSchema ) { return; } @@ -1185,10 +1194,17 @@ inline void captureRubyAttrDefs( TSNode root, std::uint32_t fileId, std::string_ stack.pop_back(); if( kindIs( ts_node_type( n ), "call" ) ) { - const std::string_view fam = rubyNamedDirective( n, src, kRubyAttrFamilyNames ); - if( !fam.empty() && rubyAttrAtClassBodyLevel( n, src ) ) + if( wantAttr ) + { + const std::string_view fam = rubyNamedDirective( n, src, kRubyAttrFamilyNames ); + if( !fam.empty() && rubyAttrAtClassBodyLevel( n, src ) ) + { + captureRubyAttrDefsCall( n, fileId, src, fam, defs ); + } + } + if( wantSchema && fieldIdentifierText( n, NodeField::Method, src ) == "create_table" ) { - captureRubyAttrDefsCall( n, fileId, src, fam, defs ); + rubySchemaTableCall( n, fileId, src, defs ); } } collectChildren( n, cursor.cur, kids ); @@ -1199,6 +1215,315 @@ inline void captureRubyAttrDefs( TSNode root, std::uint32_t fileId, std::string_ } } + +// ─── the Rails schema capture (test/rubyschemacheck.sh): RUBY db/schema.rb COLUMNS ARE Section DEFS ─── +// A schema file is recognized BY CONTENT, never by path: any Ruby file whose tree holds a +// `create_table "x", … do |t| … end` call — the call's `block:` field is a do_block whose `parameters:` +// field is a block_parameters carrying ONE bare identifier — is a rendered-schema document (the block +// is the create_table call's OWN do_block, found by named-child kind because the shared NodeField set +// has no Block enumerator). Path plays no part: a rendered dump sits at file level or under +// `ActiveRecord::Schema[].define … do`. WHAT A WALK OF THE FILE IS NOT: a MIGRATION wraps its +// create_table inside a class/module body, and rubySchemaTableCall's Gate 0 refuses every +// class/module-nested table — so the container DOES matter exactly there (the migration shape +// contributes ZERO defs, pinned by the three migration fixtures). The file signal below is a +// contains-"create_table" reject like the attribute-family capture's contains-"attr" reject; the +// semantic gate is the AST shape. +// +// Each `t. "name"` / `t. :name` receiver-CALL in the table's do-block body mints ONE +// SymKind::Section def at Lang::Ruby — the same data-kind slot as a YAML key / doc heading (model.h; +// note the wording change there: a Section is no longer "isolated in the graph" once these are +// Lang::Ruby and admit call edges) — with span = the name token and name = the literal (both quote +// spellings via stringLiteralText, or a simple_symbol's text minus ':'). The DSL CALLS keep their +// external-surface reference posture exactly like the attr family: no def named `string`/`datetime` +// appears. `t.timestamps` (a receiver call with NO arguments field) mints `created_at` AND `updated_at` +// — the DSL names them literally, symmetric with the id rule; the two defs anchor at the method +// token's start/end bytes, since no literal name tokens exist in the source. +// +// Non-column DSL — t.index / t.references / t.belongs_to / t.polymorphic — names no column and defines +// nothing (stated floor: `t.references :owner` would otherwise mint a phantom "owner" column). A column +// call whose receiver is not a bare identifier (`registry.string`, a receiver-less `string`) is not a +// column; a string named by a leading-comment-skipped FIRST argument is (the attribute-DSL comment posture). +// +// The id rule — the four grounded spellings, all pinned by test/rubyschemacheck.sh: +// – no column named `id`, no `id:` pair, no primary_key pair → an implicit `id` def anchored at +// the TABLE-NAME string's content start (no `id` token exists; the anchor is per-table unique, so +// the multi-def floor — `id` on N tables → --uses=id defs="N" — holds naturally) +// – `id: false` (the pair VALUE is the anonymous `false` token) → no id def (a key-less table) +// – `id: :uuid` (pair value a simple_symbol) → `id` def AT THE PAIR KEY (the key's +// text minus its trailing ':'; the uuid TYPE is not modelled — stated floor) +// – `primary_key: "x"` (pair value a string) → a def named the string's content +// instead of the implicit `id` +// Duplicate column NAMES across tables stay SEPARATE defs (distinct nameByte identities — the dedup +// ladder only folds same-byte captures). +inline constexpr std::array kRubySchemaNonColumns = { "index", "references", "belongs_to", "polymorphic", "check_constraint", "exclusion_constraint", "unique_constraint" }; + +// The FIRST non-comment named child of an argument_list — the attribute-DSL comment posture ("attribute( # note +// then :x"): a comment is an n-ary extra, never an argument. Null when the list is all comments/empty. +inline TSNode rubyFirstNonCommentArg( TSNode args ) noexcept +{ + ChildCursor cursor( args ); + TSNode first{}; + bool got = false; + forEachNamedChild( args, cursor.cur, [ & ]( TSNode c ) + { + if( kindIs( ts_node_type( c ), "comment" ) ) + { + return true; // skip the comment train, keep looking + } + first = c; + got = true; + return false; // stop at the first real argument + } ); + return got ? first : TSNode{}; +} + +// A pair node's KEY text: a hash_key_symbol carries its trailing ':' (the whole `force:` token) — strip +// it so the id/primary_key comparisons see the bare word. A key without the colon (already bare) passes +// through unchanged. +inline std::string_view rubyPairKeyText( TSNode pair, std::string_view src ) noexcept +{ + const TSNode key = fieldChild( pair, NodeField::Key ); + std::string_view t = nodeTextOf( key, src ); + if( !t.empty() && t.back() == ':' ) + { + t.remove_suffix( 1 ); + } + return t; +} + +// One `t. ` receiver-call inside a table block: a Section def per the rules above. The +// RECEIVER must BE the block parameter the create_table call bound (`t` in a rendered schema's do |t|); +// any other bare identifier (`helper.string "x"`) is somebody's own helper call and names no column. +// The `sawId` out-param records an explicit `id` column so the implicit rule can see it. +inline void rubySchemaColumnCall( TSNode c, std::uint32_t fileId, std::string_view src, std::string_view receiverName, std::vector& defs, bool& sawId ) +{ + if( !kindIs( ts_node_type( c ), "call" ) ) + { + return; + } + const TSNode recv = fieldChild( c, NodeField::Receiver ); + if( ts_node_is_null( recv ) || !kindIs( ts_node_type( recv ), "identifier" ) || nodeTextOf( recv, src ) != receiverName ) + { + return; // receiver-less, receiver-qualified, or a DIFFERENT bare name: not a column on the block parameter + } + const std::string_view m = fieldIdentifierText( c, NodeField::Method, src ); + if( m.empty() ) + { + return; + } + const std::uint32_t line = ts_node_start_point( c ).row + 1; + if( m == "timestamps" && ts_node_is_null( fieldChild( c, NodeField::Arguments ) ) ) + { + const TSNode meth = fieldChild( c, NodeField::Method ); + const std::uint32_t s = ts_node_start_byte( meth ), e = ts_node_end_byte( meth ); + RawDef d; + d.fileId = fileId; d.line = line; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; + d.nameByte = s; d.name = "created_at"; defs.push_back( d ); // the two anchors are the + d.nameByte = e; d.name = "updated_at"; defs.push_back( d ); // method token's edges — no literal tokens exist + return; + } + if( std::find( kRubySchemaNonColumns.begin(), kRubySchemaNonColumns.end(), m ) != kRubySchemaNonColumns.end() ) + { + return; // t.index / references / belongs_to / polymorphic name no column (stated floor) + } + const TSNode args = fieldChild( c, NodeField::Arguments ); + if( ts_node_is_null( args ) ) + { + return; + } + const TSNode nameNode = rubyFirstNonCommentArg( args ); + if( ts_node_is_null( nameNode ) ) + { + return; + } + const std::string lit = stringLiteralText( nameNode, src ); // "" when not a string node + std::string_view txt = lit; + if( txt.empty() && kindIs( ts_node_type( nameNode ), "simple_symbol" ) ) + { + txt = nodeTextOf( nameNode, src ); + if( txt.size() > 1 && txt.front() == ':' ) + { + txt.remove_prefix( 1 ); + } + } + if( txt.empty() ) + { + return; // `""` / `: ` defensive: an empty name is no column + } + const std::uint32_t s = ts_node_start_byte( nameNode ), e = ts_node_end_byte( nameNode ); + RawDef d; + d.fileId = fileId; d.line = line; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s + 1; // content start — past the quote or ':' + d.name = std::string( txt ); + if( d.name == "id" ) + { + sawId = true; + } + defs.push_back( d ); +} + +// One create_table call: the schema-file gate plus the whole table's defs (name, options, columns, id). +inline void rubySchemaTableCall( TSNode n, std::uint32_t fileId, std::string_view src, std::vector& defs ) +{ + std::string_view receiverName; // the block parameter's name (`t`); empty until Gate 2 passes + // Gate 0: a rendered schema's tables live at file level or under `ActiveRecord::Schema[].define … do` + // — NEVER inside a class/module body. A MIGRATION's `create_table "users" … do |t|` (class-wrapped, + // often string-named) is the same AST shape, and without this gate it would mint the SAME columns as + // the schema that migration created — double-counting every added column on every corpus with both + // files indexed. `add_column`/`remove_column` are not read anywhere (a column added then dropped never + // registers; pinned by the migration fixtures), but the create_table of a real migration must not + // double its schema either. Excluding at least one `class`/`module` ancestor keeps the capture + // content-gated (still no path heuristic): a migration file next to the schema contributes ZERO defs. + // Checked FIRST — the cheapest gate — before the argument/block walks. + { + for( TSNode p = ts_node_parent( n ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + { + const std::string_view k( ts_node_type( p ) ); + if( k == "class" || k == "module" ) // a locals distilled walk — no cross-helper dependency + { + return; + } + } + } + const TSNode args = fieldChild( n, NodeField::Arguments ); + if( ts_node_is_null( args ) ) + { + return; + } + // Gate 1: a STRING table name as the first argument (a symbol-named table is a migration spelling, + // not a rendered-schema shape — stated floor; no name node → no schema surface either). + const TSNode tableName = rubyFirstNonCommentArg( args ); + if( ts_node_is_null( tableName ) || !kindIs( ts_node_type( tableName ), "string" ) ) + { + return; + } + // Gate 2: the call's OWN do_block, and its parameters: field carries ONE bare identifier (the `|t|`). + TSNode block{}; + { + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) + { + if( kindIs( ts_node_type( c ), "do_block" ) ) + { + block = c; + return false; + } + return true; + } ); + } + if( ts_node_is_null( block ) ) + { + return; + } + const TSNode params = fieldChild( block, NodeField::Parameters ); + if( ts_node_is_null( params ) ) + { + return; // `create_table "x" do … end` with no handle — not the schema shape + } + { + ChildCursor cursor( params ); + TSNode only{}; + std::size_t named = 0; + forEachNamedChild( params, cursor.cur, [ & ]( TSNode c ) { only = c; ++named; return true; } ); + if( named != 1 || !kindIs( ts_node_type( only ), "identifier" ) ) + { + return; // multi-parameter or non-identifier handle — not a rendered schema's do |t| + } + receiverName = nodeTextOf( only, src ); // the `|t|` spelling — column calls must use it + } + // Options: the id/primary_key pairs among the call's arguments. + bool idFalse = false, primaryKey = false; + TSNode idKey{}, pkValue{}; + { + ChildCursor cursor( args ); + forEachNamedChild( args, cursor.cur, [ & ]( TSNode a ) + { + if( !kindIs( ts_node_type( a ), "pair" ) ) + { + return true; + } + const std::string_view k = rubyPairKeyText( a, src ); + const TSNode v = fieldChild( a, NodeField::Value ); + if( k == "id" ) + { + if( kindIs( ts_node_type( v ), "false" ) ) + { + idFalse = true; + } + else if( kindIs( ts_node_type( v ), "simple_symbol" ) ) + { + idKey = fieldChild( a, NodeField::Key ); + } + } + else if( k == "primary_key" ) + { + primaryKey = true; + pkValue = v; + } + return true; + } ); + } + // The columns: the do_block's body: field (its body_statement) — walk its named children directly + // (a fresh cursor; the outer walks cannot be reused mid-flight, tschildren.h). + bool sawIdColumn = false; + const TSNode body = fieldChild( block, NodeField::Body ); + if( !ts_node_is_null( body ) ) + { + ChildCursor cursor( body ); + forEachNamedChild( body, cursor.cur, [ & ]( TSNode c ) + { + rubySchemaColumnCall( c, fileId, src, receiverName, defs, sawIdColumn ); + return true; + } ); + } + // The id rule, in the grounded order: an explicit id column wins; then the pairs decide. + if( sawIdColumn || idFalse ) + { + return; + } + if( primaryKey && !ts_node_is_null( pkValue ) && kindIs( ts_node_type( pkValue ), "string" ) ) + { + const std::string name = stringLiteralText( pkValue, src ); + if( name.empty() ) + { + return; + } + const std::uint32_t s = ts_node_start_byte( pkValue ), e = ts_node_end_byte( pkValue ); + RawDef d; + d.fileId = fileId; d.line = ts_node_start_point( pkValue ).row + 1; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s + 1; d.name = name; + defs.push_back( d ); + return; + } + if( !ts_node_is_null( idKey ) ) + { + const std::uint32_t s = ts_node_start_byte( idKey ), e = ts_node_end_byte( idKey ); + RawDef d; + d.fileId = fileId; d.line = ts_node_start_point( idKey ).row + 1; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s; d.name = "id"; + defs.push_back( d ); + return; + } + // A primary_key pair with a NON-string value (composite `["a","b"]`, hand-written `:symbol`): the + // key is NOT an `id` and the rendered columns are the block's own explicit `t.` lines (already + // minted above) — suppress the implicit id entirely (the composite case is pinned by the fixture). + if( primaryKey ) + { + return; + } + // Implicit `id` — anchored at the table-name string's content start (a stable per-table byte). + { + const std::uint32_t s = ts_node_start_byte( tableName ), e = ts_node_end_byte( tableName ); + RawDef d; + d.fileId = fileId; d.line = ts_node_start_point( tableName ).row + 1; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s + 1; d.name = "id"; + defs.push_back( d ); + } +} + + + // F5: a Swift LOCAL binding — `let a = f()` / `var b = ...` inside a function/closure body — parses to the // same `property_declaration` node as a real stored/computed MEMBER property, so the @definition.var pattern // captures it as a spurious top-level `var` symbol AND (being the nearest enclosing symbol above the body's @@ -2111,6 +2436,8 @@ inline bool cppBlockScopeDirectInit( TSNode fnDeclarator, std::string_view src ) inline bool isCjsExportTarget( TSNode nameNode, std::string_view src ) noexcept; inline bool isPrototypeMemberTarget( TSNode nameNode, std::string_view src ) noexcept; inline bool isPyEnumMemberTarget( TSNode nameNode, std::string_view src ) noexcept; +inline TSNode jsClassFactoryReceiver( TSNode nameNode, std::string_view src ) noexcept; +inline bool jsClassFactoryMakesClass( TSNode nameNode, std::string_view src, int depth = 0 ); // allocates (the scope index) // The @definition.constant drop decision, in its own function for the same reason isCjsExportTarget and // isPyEnumMemberTarget have theirs: dropGatedCapture is a dispatcher, and this is the one arm with a @@ -2218,7 +2545,7 @@ inline bool yamlKeyCaptureDropped( std::string_view name, TSNode roleNode ) noex // the LEFT side is not really exports/module.exports/.prototype. — the query captures every `a.b = fn` // shape and cannot text-test, because tags-pass predicates never run (see constCaptureNeedsScreamingGate // above). -inline bool dropGatedCapture( std::string_view defCapSv, Lang lang, std::string_view name, TSNode nameNode, TSNode roleNode, std::string_view src ) noexcept +inline bool dropGatedCapture( std::string_view defCapSv, Lang lang, std::string_view name, TSNode nameNode, TSNode roleNode, std::string_view src ) // NOT noexcept: the class-factory gate's scope lookup allocates { if( defCapSv == "definition.function" ) { @@ -2236,6 +2563,10 @@ inline bool dropGatedCapture( std::string_view defCapSv, Lang lang, std::string_ { return !isCjsExportTarget( nameNode, src ); } + if( defCapSv == "definition.classfactory" ) + { + return !jsClassFactoryMakesClass( nameNode, src ); // lane lego-transitive (M2; fix 1: the receiver must be a class) + } if( defCapSv == "definition.protomethod" ) { return !isPrototypeMemberTarget( nameNode, src ); @@ -2319,6 +2650,134 @@ inline bool isPrototypeMemberTarget( TSNode nameNode, std::string_view src ) noe return nodeTextOf( fieldChild( obj, NodeField::Property ), src ) == "prototype"; } +// lane lego-transitive (M2): the RECEIVER of a JS/TS class factory `const Sub = Base.extend('Sub', …)` — the class the +// factory's result extends — or a null node when `nameNode` (the declarator's name) is not that shape. The shape is all +// three: the value is a call of a member `X.extend`, its first argument is a string literal, and that literal spells the +// declared name. The name test is what tells a class factory from an object merge: `$.extend({}, a)`, `_.extend(o, p)` +// and Backbone's unnamed `Base.extend({…})` have the same call shape and no name, and `const A = B.extend('C')` binds a +// class to a name other than the one it was made under. The receiver is an identifier or a member chain +// (`nodes.Node.extend`); a call or `this` receiver is not a class this tool can name. Floors, stated: a template +// literal name, an assignment without a declaration (`Sub = Base.extend('Sub')`), and `exports.Sub = …` are not read. +inline TSNode jsClassFactoryReceiver( TSNode nameNode, std::string_view src ) noexcept +{ + const TSNode decl = ts_node_parent( nameNode ); + if( ts_node_is_null( decl ) || !kindIs( ts_node_type( decl ), "variable_declarator" ) ) + { + return TSNode{}; + } + const TSNode call = fieldChild( decl, NodeField::Value ); + const TSNode fn = ts_node_is_null( call ) ? call : fieldChild( call, NodeField::Function ); + if( ts_node_is_null( fn ) || !kindIs( ts_node_type( fn ), "member_expression" ) + || nodeTextOf( fieldChild( fn, NodeField::Property ), src ) != "extend" ) + { + return TSNode{}; + } + const TSNode args = fieldChild( call, NodeField::Arguments ); + if( ts_node_is_null( args ) || ts_node_named_child_count( args ) == 0 ) + { + return TSNode{}; + } + const TSNode first = ts_node_named_child( args, 0 ); + const std::string_view lit = nodeTextOf( first, src ); + if( !kindIs( ts_node_type( first ), "string" ) || lit.size() < 2 || lit.substr( 1, lit.size() - 2 ) != nodeTextOf( nameNode, src ) ) + { + return TSNode{}; + } + const TSNode recv = fieldChild( fn, NodeField::Object ); + if( ts_node_is_null( recv ) || !( kindIs( ts_node_type( recv ), "identifier" ) || kindIs( ts_node_type( recv ), "member_expression" ) ) ) + { + return TSNode{}; + } + return recv; +} + +// lane lego-transitive fix 1 (M2, orchestrator ruling 2): the shape above is not yet a class. The `debug` package's sub-logger +// `const error = log.extend('error')` has it exactly — a string literal spelling the declared name — and so does an object +// merge that happens to pass a matching string. A factory MAKES a class only when the parser shows one: the +// factory is handed a class BODY (an object literal holding a method, jsFactoryHasClassBody), or its +// receiver is an identifier that the file's own lexical lookup (ingest_relations.h findOutward) binds to a class declaration +// (`class Node extends Obj { }`), a class expression (`const Node = class { }`), or another declarator this same rule keeps +// (`const BinOp = Node.extend('BinOp', …)` makes `const Add = BinOp.extend('Add')` a class, chained up to kExtendChainCap). +// What it does NOT read without a class body, each a stated floor (test/legotransitivecheck.sh M2): a receiver imported +// from another file (`const { Node } = require('./a')` — the index, not the parser, would have to say that `Node` is a +// class), a member chain receiver (`nodes.Node.extend('X')`), and a receiver reassigned after its declaration. +inline constexpr int kExtendChainCap = 16; // factory-of-a-factory links the receiver rule follows (nunjucks: 2); past it, no class + +// a class BODY passed to the factory: an object literal argument after the name holding at least one method +// (`Base.extend('View', { render() { … } })`, `{ init: function() { … } }`). A data object (`{ a: 1 }`, nunjucks' +// `{ fields: [ … ] }`) is not one — such a factory is a class only by its receiver. +inline bool jsFactoryHasClassBody( TSNode nameNode ) noexcept +{ + const TSNode call = fieldChild( ts_node_parent( nameNode ), NodeField::Value ); + const TSNode args = ts_node_is_null( call ) ? call : fieldChild( call, NodeField::Arguments ); + const std::uint32_t n = ts_node_is_null( args ) ? 0u : ts_node_named_child_count( args ); + for( std::uint32_t i = 1; i < n; ++i ) + { + const TSNode obj = ts_node_named_child( args, i ); + if( !kindIs( ts_node_type( obj ), "object" ) ) + { + continue; + } + bool method = false; + ChildCursor cursor( obj ); + forEachNamedChild( obj, cursor.cur, [ & ]( TSNode member ) + { + const char* mt = ts_node_type( member ); + const TSNode val = kindIs( mt, "pair" ) ? fieldChild( member, NodeField::Value ) : TSNode{}; + method = kindIs( mt, "method_definition" ) + || ( !ts_node_is_null( val ) && ( kindIs( ts_node_type( val ), "function_expression" ) || kindIs( ts_node_type( val ), "function" ) + || kindIs( ts_node_type( val ), "arrow_function" ) ) ); + return !method; + } ); + if( method ) + { + return true; + } + } + return false; +} + +inline bool jsClassFactoryMakesClass( TSNode nameNode, std::string_view src, int depth ) +{ + const TSNode recv = jsClassFactoryReceiver( nameNode, src ); + if( ts_node_is_null( recv ) || depth >= kExtendChainCap ) + { + return false; + } + if( jsFactoryHasClassBody( nameNode ) ) + { + return true; // the parser shows the class itself: its body is the argument + } + if( !kindIs( ts_node_type( recv ), "identifier" ) ) + { + return false; + } + const TSNode declarator = ts_node_parent( nameNode ); // jsClassFactoryReceiver proved it a variable_declarator + const TSNode statement = ts_node_parent( declarator ); + const BaseScopeHit hit = findOutward( recv, statement, nodeTextOf( recv, src ), /*scopeOwner=*/false, src ); + if( hit.cut || hit.ambiguous || ts_node_is_null( hit.decl ) ) + { + return false; + } + const char* kind = ts_node_type( hit.decl ); + if( kindIs( kind, "class_declaration" ) || kindIs( kind, "abstract_class_declaration" ) || kindIs( kind, "class" ) ) + { + return true; + } + // a declarator: baseDeclNames answered with its NAME node, whose parent is the variable_declarator + const TSNode boundDecl = ts_node_parent( hit.name ); + const TSNode value = ts_node_is_null( boundDecl ) ? boundDecl : fieldChild( boundDecl, NodeField::Value ); + if( ts_node_is_null( value ) ) + { + return false; + } + if( kindIs( ts_node_type( value ), "class" ) ) + { + return true; + } + return jsClassFactoryMakesClass( hit.name, src, depth + 1 ); +} + // Constructor identifier of `Foo.prototype.NAME` (the object of the inner `.prototype` member). // `String.prototype.shout` → "String"; `net.Socket.prototype.x` → "Socket". Empty if the shape // is not a prototype member. Used as RawDef::scope so a Lit* call can bind only an extension of diff --git a/src/ingest_parsepool.h b/src/ingest_parsepool.h index 02ca53606..f583c78a0 100644 --- a/src/ingest_parsepool.h +++ b/src/ingest_parsepool.h @@ -407,7 +407,10 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) const TSNode root = ts_tree_root_node( pending.tree ); const std::size_t firstNewDefIndex = out.defs.size(); ExtractShortfall tagsShortfall; - captureTagsFacts( cursor, *pending.le, pending.fileId, pending.bytes, root, out.defs, out.refs, out.binds, out.incs, pending.ppDead, tagsShortfall ); + underExtractScope( pending.le->lang, [ & ] + { + captureTagsFacts( cursor, *pending.le, pending.fileId, pending.bytes, root, out.defs, out.refs, out.binds, out.incs, pending.ppDead, tagsShortfall ); + } ); notePartialExtract( scan, pending.fileId, tagsShortfall, pending.bytes.size() ); buildLexForNewDefs( out.defs, firstNewDefIndex, pending.bytes ); // B0.2: bytes still in memory ts_tree_delete( pending.tree ); @@ -565,7 +568,7 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) { continue; } - TreeGuard mdTree( parseTree( pg.p, bytes ) ); + TreeGuard mdTree( underExtractScope( le->lang, [ & ] { return parseTree( pg.p, bytes ); } ) ); if( mdTree.get() == nullptr ) { continue; @@ -602,7 +605,7 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) continue; } - TreeGuard tree( parseTree( pg.p, bytes ) ); + TreeGuard tree( underExtractScope( le->lang, [ & ] { return parseTree( pg.p, bytes ); } ) ); if( tree.get() == nullptr ) { continue; @@ -616,7 +619,10 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) // above), which is why it is computed here and not inside either driver. std::vector ppDead = preprocDeadRangesFor( *le, root, bytes ); ExtractShortfall shortfall; // every pass's disclosure for THIS file; noted on its slot once both have run - captureSideFacts( *le, static_cast( fileId ), bytes, root, out.refs, out.incs, out.binds, out.ffis, out.routeDefs, out.routeUses, out.constOpens, sh.captureValueUses, ppDead, shortfall ); + underExtractScope( le->lang, [ & ] + { + captureSideFacts( *le, static_cast( fileId ), bytes, root, out.refs, out.incs, out.binds, out.ffis, out.routeDefs, out.routeUses, out.constOpens, sh.captureValueUses, ppDead, shortfall ); + } ); notePartialExtract( scan, fileId, shortfall, bytes.size() ); // before a queued file's tags pass, which notes its own appendBlankedMacroUses( macroWork, le->lang, static_cast( fileId ), bytes, out.refs, ppDead ); @@ -636,7 +642,10 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) waitForQueryPrewarm( &sh.gate ); } const std::size_t firstNewDefIndex = out.defs.size(); - captureTagsFacts( cursor, *le, static_cast( fileId ), bytes, root, out.defs, out.refs, out.binds, out.incs, ppDead, shortfall ); + underExtractScope( le->lang, [ & ] + { + captureTagsFacts( cursor, *le, static_cast( fileId ), bytes, root, out.defs, out.refs, out.binds, out.incs, ppDead, shortfall ); + } ); notePartialExtract( scan, fileId, shortfall, bytes.size() ); buildLexForNewDefs( out.defs, firstNewDefIndex, bytes ); // B0.2: bytes still in memory } diff --git a/src/ingest_relations.h b/src/ingest_relations.h index b3f1e9ea5..77bab2833 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -71,17 +71,460 @@ inline RawRef rawRefAt( TSNode at, std::uint32_t fileId, Lang lang, RefRole role return r; } +// ---- lane lego-transitive fix 1: WHICH same-file declaration a base name binds to (parser evidence) ---------------- +// `class Leaf(Mid)` used to reach the implementors builder as the bare NAME `Mid`, which graph.h bound to every class of +// that name in the tree — so `--lego=Base` listed b.py's `Leaf` under a.py's unrelated `Mid` as a depth-2 subtype. The +// parser can see what the language's own lookup sees for a name written in the file: a class declared in an enclosing +// scope (the innermost wins), reached the way the source writes it (`Outer.Changed` → the `Changed` inside `Outer`, +// `b::Mid` → the `Mid` inside `namespace b`, including a reopened `namespace b { }` block). It records the NAME BYTE of +// that declaration on the reference (RawRef::baseBind), and graph.h proves the edge to the one candidate whose span holds +// it. A name no enclosing scope declares records nothing here: graph.h then asks the file's imports / includes / package. +// What it does NOT read: a `using namespace`, a star import, an alias assignment (`Base = make_base()`), a class declared +// inside a function the reference is not in. Those leave the reference without a local binding — never a wrong one. +inline constexpr std::uint32_t kBaseBindNone = 0; // no enclosing scope declares the name +inline constexpr std::uint32_t kBaseBindUnknown = 0xFFFFFFFFu; // a scope was too large to scan: nothing proven either way +// kBaseScopeScanCap bounds the two SCANS only: the unindexed lookupInScope fallback and the reopened-namespace sibling scan +// (a runaway guard: past it those two bind nothing and the edge stays name-only). The indexed path (lookupInScopeIndexed, +// the one captureTagsFacts installs) reads the scope's table and has no cap: a 32,000-class module scope binds every base. +inline constexpr std::uint32_t kBaseScopeScanCap = 4096; // statements the unindexed scope scans read (not the index) + +// What a scope-level declaration can be to a base lookup: a class-like declaration (each with a `name:` field), a +// namespace a qualifier segment can name (C++/PHP namespace_definition, C# namespace_declaration, TS internal_module), or +// neither. Kotlin's class_declaration has no `name:` field (kotlinEnclosingScopeOf), so a Kotlin base never binds here. +enum class BaseScopeKind : std::uint8_t { None, Class, Namespace }; + +inline BaseScopeKind baseScopeKindOf( const char* t ) noexcept +{ + if( kindIs( t, "class_specifier" ) || kindIs( t, "struct_specifier" ) || kindIs( t, "class_definition" ) // C/C++, Python + || kindIs( t, "class_declaration" ) || kindIs( t, "abstract_class_declaration" ) || kindIs( t, "interface_declaration" ) + || kindIs( t, "enum_declaration" ) || kindIs( t, "record_declaration" ) || kindIs( t, "struct_declaration" ) + || kindIs( t, "trait_declaration" ) || kindIs( t, "protocol_declaration" ) || kindIs( t, "class" ) ) // …JS class expression + { + return BaseScopeKind::Class; + } + if( kindIs( t, "namespace_definition" ) || kindIs( t, "namespace_declaration" ) || kindIs( t, "internal_module" ) ) + { + return BaseScopeKind::Namespace; + } + return BaseScopeKind::None; +} + +inline bool isNamespaceDeclKind( const char* t ) noexcept +{ + return baseScopeKindOf( t ) == BaseScopeKind::Namespace; +} + +// the declaration a scope-level statement carries: `export class X`, a decorated Python class, a C++ template, a C++ +// `struct X { } x;` declaration or a nested `struct In { };` member — the node itself for every other statement +inline TSNode baseDeclOf( TSNode stmt ) noexcept +{ + const char* t = ts_node_type( stmt ); + if( kindIs( t, "export_statement" ) ) + { + return fieldChild( stmt, NodeField::Declaration ); + } + if( kindIs( t, "decorated_definition" ) ) + { + return fieldChild( stmt, NodeField::Definition ); + } + if( kindIs( t, "template_declaration" ) ) + { + const std::uint32_t n = ts_node_named_child_count( stmt ); + return n == 0 ? TSNode{} : ts_node_named_child( stmt, n - 1 ); // the templated entity is the last named child + } + if( kindIs( t, "declaration" ) || kindIs( t, "field_declaration" ) ) + { + return fieldChild( stmt, NodeField::Type ); + } + return stmt; +} + +// JS/TS `const Node = class extends Obj {}` and the class factory `const BinOp = Node.extend('BinOp', …)`: the declarator's +// NAME is the class; whether a factory IS one is ingest_names.h's gate, and an edge to a declarator the gate dropped finds +// no candidate in graph.h (it stays name-only). Returns the declarator's name node, or a null node. +inline TSNode jsClassDeclaratorNamed( TSNode d, std::string_view name, std::string_view src ) noexcept +{ + TSNode hit{}; + ChildCursor cursor( d ); + forEachNamedChild( d, cursor.cur, [ & ]( TSNode decl ) + { + const TSNode nm = fieldChild( decl, NodeField::Name ); + const TSNode val = fieldChild( decl, NodeField::Value ); + if( !kindIs( ts_node_type( decl ), "variable_declarator" ) || ts_node_is_null( nm ) || ts_node_is_null( val ) || nodeTextOf( nm, src ) != name ) + { + return true; + } + if( kindIs( ts_node_type( val ), "class" ) || kindIs( ts_node_type( val ), "call_expression" ) ) + { + hit = nm; + } + return false; + } ); + return hit; +} + +// does declaration `d` declare `name` as a class (or, when `scopeOwner`, as a class or namespace that a qualifier can +// name)? A C/C++ class must carry a body: `struct Mid;` declares a name whose definition lives elsewhere. Returns the name +// node, or a null node. +inline TSNode baseDeclNames( TSNode d, std::string_view name, bool scopeOwner, std::string_view src ) noexcept +{ + if( ts_node_is_null( d ) ) + { + return TSNode{}; + } + const char* t = ts_node_type( d ); + const BaseScopeKind kind = baseScopeKindOf( t ); + if( kind == BaseScopeKind::Class || ( scopeOwner && kind == BaseScopeKind::Namespace ) ) + { + const bool forward = ( kindIs( t, "class_specifier" ) || kindIs( t, "struct_specifier" ) ) && ts_node_is_null( fieldChild( d, NodeField::Body ) ); + const TSNode nm = fieldChild( d, NodeField::Name ); + return ( !forward && !ts_node_is_null( nm ) && nodeTextOf( nm, src ) == name ) ? nm : TSNode{}; + } + if( !scopeOwner && ( kindIs( t, "lexical_declaration" ) || kindIs( t, "variable_declaration" ) ) ) + { + return jsClassDeclaratorNamed( d, name, src ); + } + return TSNode{}; +} + +// One scope's lookup of `name`: the declaration among `scope`'s statements (skipping `self`, the class whose header +// writes the base). `ambiguous` is set when two bodied declarations answer (a Python redefinition, two C++ definitions), +// `cut` when the scope holds more than kBaseScopeScanCap statements. Returns the declaration node (name in `nameOut`). +struct BaseScopeHit +{ + TSNode decl{}; + TSNode name{}; + bool ambiguous = false; + bool cut = false; +}; + +// One scope's declarations, by name, in statement order — built ONCE per scope node per file (BaseScopeIndex), so the base +// lookup of every class in a 16 000-class module reads a table instead of rescanning the module (quadratic: the scale +// probe in the lane report measured 0.3 s → 12 s on 8 000 chained classes before the index). `kind` None = a JS/TS +// declarator whose value is a class expression or a call (a factory the M2 gate decides). +struct BaseScopeEntry +{ + TSNode stmt{}; + TSNode decl{}; + TSNode name{}; + BaseScopeKind kind = BaseScopeKind::None; +}; + +struct BaseScopeTable +{ + HashMap> byName; +}; + +// the per-FILE memo: scope node id → its table. Lives in captureTagsFacts' frame (BaseScopeIndexGuard), so every key and +// every string_view it holds points into the one tree and source being read, and nothing outlives them. +struct BaseScopeIndex +{ + HashMap tables; + + const BaseScopeTable& tableFor( TSNode scope, std::string_view src ) + { + const auto [ it, fresh ] = tables.try_emplace( scope.id ); + if( fresh ) + { + fill( it->second, scope, src ); + } + return it->second; + } + +private: + static void fill( BaseScopeTable& table, TSNode scope, std::string_view src ) + { + ChildCursor cursor( scope ); + forEachNamedChild( scope, cursor.cur, [ & ]( TSNode stmt ) + { + const TSNode d = baseDeclOf( stmt ); + const char* t = ts_node_is_null( d ) ? "" : ts_node_type( d ); + const BaseScopeKind kind = baseScopeKindOf( t ); + if( kind != BaseScopeKind::None ) + { + const bool forward = ( kindIs( t, "class_specifier" ) || kindIs( t, "struct_specifier" ) ) && ts_node_is_null( fieldChild( d, NodeField::Body ) ); + const TSNode nm = fieldChild( d, NodeField::Name ); + if( !forward && !ts_node_is_null( nm ) ) + { + table.byName[ nodeTextOf( nm, src ) ].push_back( BaseScopeEntry{ stmt, d, nm, kind } ); + } + } + else if( kindIs( t, "lexical_declaration" ) || kindIs( t, "variable_declaration" ) ) + { + addJsDeclarators( table, stmt, d, src ); + } + return true; + } ); + } + static void addJsDeclarators( BaseScopeTable& table, TSNode stmt, TSNode d, std::string_view src ) + { + ChildCursor cursor( d ); + forEachNamedChild( d, cursor.cur, [ & ]( TSNode decl ) + { + const TSNode nm = fieldChild( decl, NodeField::Name ); + const TSNode val = fieldChild( decl, NodeField::Value ); + if( kindIs( ts_node_type( decl ), "variable_declarator" ) && !ts_node_is_null( nm ) && !ts_node_is_null( val ) + && ( kindIs( ts_node_type( val ), "class" ) || kindIs( ts_node_type( val ), "call_expression" ) ) ) + { + table.byName[ nodeTextOf( nm, src ) ].push_back( BaseScopeEntry{ stmt, d, nm, BaseScopeKind::None } ); + } + return true; + } ); + } +}; + +// the index captureTagsFacts arms for the file it is reading (null outside it: lookupInScope then scans, capped) +inline thread_local BaseScopeIndex* tBaseScopeIndex = nullptr; + +struct BaseScopeIndexGuard +{ + BaseScopeIndex index; + BaseScopeIndex* previous; + BaseScopeIndexGuard() noexcept : previous( tBaseScopeIndex ) { tBaseScopeIndex = &index; } + ~BaseScopeIndexGuard() { tBaseScopeIndex = previous; } + BaseScopeIndexGuard( const BaseScopeIndexGuard& ) = delete; + BaseScopeIndexGuard& operator=( const BaseScopeIndexGuard& ) = delete; +}; + +// lookupInScope through the file's index: the same answer, read off the table (statement order kept) +inline BaseScopeHit lookupInScopeIndexed( BaseScopeIndex& index, TSNode scope, std::string_view name, bool scopeOwner, TSNode self, std::string_view src ) +{ + BaseScopeHit hit; + const BaseScopeTable& table = index.tableFor( scope, src ); + const auto it = table.byName.find( name ); + if( it == table.byName.end() ) + { + return hit; + } + for( const BaseScopeEntry& e : it->second ) + { + const bool ns = e.kind == BaseScopeKind::Namespace; + if( ts_node_eq( e.decl, self ) || ts_node_eq( e.stmt, self ) || ( ns && !scopeOwner ) || ( e.kind == BaseScopeKind::None && scopeOwner ) ) + { + continue; + } + if( !ts_node_is_null( hit.name ) && !( scopeOwner && ns ) ) + { + hit.ambiguous = true; // a namespace reopened in one scope is ONE scope (handled by the caller); a class twice is two + break; + } + if( ts_node_is_null( hit.name ) ) + { + hit.decl = e.decl; + hit.name = e.name; + } + } + return hit; +} + +inline BaseScopeHit lookupInScope( TSNode scope, std::string_view name, bool scopeOwner, TSNode self, std::string_view src ) // allocates (the file's scope index) +{ + if( tBaseScopeIndex != nullptr ) + { + return lookupInScopeIndexed( *tBaseScopeIndex, scope, name, scopeOwner, self, src ); + } + BaseScopeHit hit; + std::uint32_t seen = 0; + ChildCursor cursor( scope ); + forEachNamedChild( scope, cursor.cur, [ & ]( TSNode stmt ) + { + if( ++seen > kBaseScopeScanCap ) + { + hit.cut = true; + return false; + } + const TSNode d = baseDeclOf( stmt ); + if( ts_node_is_null( d ) || ts_node_eq( d, self ) || ts_node_eq( stmt, self ) ) + { + return true; + } + const TSNode nm = baseDeclNames( d, name, scopeOwner, src ); + if( ts_node_is_null( nm ) ) + { + return true; + } + if( !ts_node_is_null( hit.name ) && !( scopeOwner && isNamespaceDeclKind( ts_node_type( d ) ) ) ) + { + hit.ambiguous = true; // a namespace reopened in one scope is ONE scope (handled by the caller); a class twice is two + return false; + } + if( ts_node_is_null( hit.name ) ) + { + hit.decl = d; + hit.name = nm; + } + return true; + } ); + return hit; +} + +// the written segments of a base type node — `Outer.Changed` → {Outer, Changed}, `b::Mid` → {b, Mid}, `Base` → +// {Base}, `::Mid` → {Mid} with `absolute` — read off the node the grammar already classified as a type name +inline std::vector baseSegments( std::string_view text, bool& absolute ) +{ + std::vector segs; + if( const std::size_t lt = text.find( '<' ); lt != std::string_view::npos && lt > 0 ) + { + text = text.substr( 0, lt ); // a type-argument list is no segment (finalSegment's rule) + } + absolute = text.starts_with( "::" ); + for( std::size_t start = 0, i = 0; start <= text.size(); start = i + 1 ) + { + i = std::min( text.find_first_of( ":.\\", start ), text.size() ); // C++/Rust `::`, Python/Java/JS/C# `.`, PHP `\\` + const std::string_view seg = text.substr( start, i - start ); + const std::size_t first = seg.find_first_not_of( " \t" ); + if( first != std::string_view::npos ) + { + segs.push_back( seg.substr( first, seg.find_last_not_of( " \t" ) - first + 1 ) ); // `Base ` leaves `Base ` + } + } + return segs; +} + +// the body of a scope owner a qualifier named (a class body, a namespace body); null when it has none +inline TSNode scopeBodyOf( TSNode owner ) noexcept +{ + return fieldChild( owner, NodeField::Body ); +} + +// `name` in the OTHER blocks of the namespace `ns` (a node of kind `nsKind`) opens beside itself in its enclosing scope: +// `namespace a { … } namespace a { struct T : Mid {} }` (C++, C#'s `namespace A { }`, TS's `namespace a { }`; a PHP +// `namespace A;` statement has no block). Two answers, a cut or an ambiguous block set the hit's flags. +inline BaseScopeHit lookupInReopenedNamespace( TSNode ns, std::string_view name, bool scopeOwner, TSNode self, std::string_view src ) // allocates (the file's scope index) +{ + BaseScopeHit hit; + const TSNode nsName = fieldChild( ns, NodeField::Name ); + const TSNode outer = ts_node_parent( ns ); + if( ts_node_is_null( nsName ) || ts_node_is_null( outer ) ) + { + return hit; + } + const char* nsKind = ts_node_type( ns ); + const std::string_view nsText = nodeTextOf( nsName, src ); + std::uint32_t seen = 0; + ChildCursor cursor( outer ); + forEachNamedChild( outer, cursor.cur, [ & ]( TSNode sib ) + { + hit.cut = ++seen > kBaseScopeScanCap; + const TSNode body = scopeBodyOf( sib ); + if( hit.cut || ts_node_eq( sib, ns ) || std::strcmp( ts_node_type( sib ), nsKind ) != 0 || ts_node_is_null( body ) + || nodeTextOf( fieldChild( sib, NodeField::Name ), src ) != nsText ) + { + return !hit.cut; + } + const BaseScopeHit h = lookupInScope( body, name, scopeOwner, self, src ); + hit.ambiguous = h.cut || h.ambiguous || ( !ts_node_is_null( h.name ) && !ts_node_is_null( hit.name ) ); + if( !ts_node_is_null( h.name ) && !hit.ambiguous ) + { + hit.decl = h.decl; + hit.name = h.name; + } + return !hit.ambiguous; + } ); + return hit; +} + +// The innermost declaration of `name` visible from `site` (owned by `self`): every enclosing node's statements are read, +// innermost first, the way the language's lexical lookup reads them, a reopened namespace as one scope +// (lookupInReopenedNamespace). `scopeOwner`: the name is a qualifier segment, so a namespace answers too. A cut or +// ambiguous scope stops the walk with its flag set. +inline BaseScopeHit findOutward( TSNode site, TSNode self, std::string_view name, bool scopeOwner, std::string_view src ) // allocates (the file's scope index) +{ + for( TSNode p = ts_node_parent( site ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + { + BaseScopeHit hit = lookupInScope( p, name, scopeOwner, self, src ); + if( ts_node_is_null( hit.name ) && !hit.cut && !hit.ambiguous && isNamespaceDeclKind( ts_node_type( p ) ) ) + { + hit = lookupInReopenedNamespace( p, name, scopeOwner, self, src ); + } + if( hit.cut || hit.ambiguous || !ts_node_is_null( hit.name ) ) + { + return hit; + } + } + return BaseScopeHit{}; +} + +// the tree root above `n` (the global scope a C++ `::Mid` names) +inline TSNode rootOf( TSNode n ) noexcept +{ + for( TSNode p = ts_node_parent( n ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + { + n = p; + } + return n; +} + +// Resolve `segs` from the class header at `site` (owned by `self`) and return the binding's encoding for RawRef::baseBind: +// the FIRST segment through findOutward (only the global scope for an `absolute` `::X`), each further segment inside the +// body the previous one named. +inline std::uint32_t localBaseBinding( TSNode site, TSNode self, const std::vector& segs, bool absolute, + std::string_view src ) // allocates (the file's scope index) +{ + if( segs.empty() ) + { + return kBaseBindNone; + } + const bool qualified = segs.size() > 1; + const BaseScopeHit first = absolute ? lookupInScope( rootOf( site ), segs.front(), qualified, self, src ) + : findOutward( site, self, segs.front(), qualified, src ); + if( first.cut || first.ambiguous ) + { + return kBaseBindUnknown; + } + TSNode found = first.decl; + TSNode foundName = first.name; + if( ts_node_is_null( found ) ) + { + return kBaseBindNone; + } + for( std::size_t i = 1; i < segs.size(); ++i ) + { + const TSNode body = scopeBodyOf( found ); + const BaseScopeHit hit = ts_node_is_null( body ) ? BaseScopeHit{} : lookupInScope( body, segs[ i ], i + 1 < segs.size(), self, src ); + if( hit.cut || hit.ambiguous ) + { + return kBaseBindUnknown; + } + if( ts_node_is_null( hit.name ) ) + { + return kBaseBindNone; // the qualifier names an in-file scope that does not declare it: nothing proven here + } + found = hit.decl; + foundName = hit.name; + } + return ts_node_start_byte( foundName ) + 1u; // never 0 (kBaseBindNone) — and below ~0u: a TSNode byte is a u32 < src.size() +} + // Emit one inherit RawRef (derived → base) for a base-type node. startByte sits inside the class header // (the type node's own start), so the byte-span enclosing attribution binds fromSymbol = the derived class. -inline void emitBaseRef( TSNode typeNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs ) +// `self` is the declaration whose header writes the base (its own name never answers the lookup). lane lego-transitive +// fix 1: the reference also carries the written qualifier (memberRoot on an inherit ref: `Outer` for `Outer.Changed`) and +// the same-file declaration the name binds to (baseBind, localBaseBinding above). Ruby keeps its own constant lookup +// (resolve.h RubyBaseScope), so a Ruby base records neither. +inline void emitBaseRef( TSNode typeNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs, TSNode self = TSNode{} ) { const uint32_t a = ts_node_start_byte( typeNode ), b = ts_node_end_byte( typeNode ); if( a >= b || b > src.size() ) { return; } - RawRef r = rawRefAt( typeNode, fileId, lang, RefRole::Extends, finalSegment( src.substr( a, b - a ) ) ); // a base-class / interface use-site + const std::string_view text = src.substr( a, b - a ); + RawRef r = rawRefAt( typeNode, fileId, lang, RefRole::Extends, finalSegment( text ) ); // a base-class / interface use-site r.isInherit = true; + if( lang != Lang::Ruby && lang != Lang::Rust ) + { + bool absolute = false; + const std::vector segs = baseSegments( text, absolute ); + r.baseBind = localBaseBinding( typeNode, self, segs, absolute, src ); + if( segs.size() > 1 ) + { + const std::size_t last = text.rfind( segs.back() ); + std::string_view qual = text.substr( 0, last == std::string_view::npos ? 0 : last ); + while( !qual.empty() && ( qual.back() == ':' || qual.back() == '.' || qual.back() == '\\' || qual.back() == ' ' ) ) { qual.remove_suffix( 1 ); } + r.memberRoot = std::string( qual ); + } + } refs.push_back( std::move( r ) ); } @@ -445,6 +888,54 @@ void captureMacroBodyCalls( TSNode defineNode, std::uint32_t fileId, Lang lang, } } +// lane lego-transitive fix 1: a base clause child that is an EXPRESSION, read whole — true when it was one (emitted or +// deliberately not). The one-level descent captureBases does for a wrapper read each identifier INSIDE an expression as +// a base of its own: Python's `Outer.Changed` minted `Outer` and `Changed`, `Generic[Key]` minted the type argument `Key`, +// `metaclass=ABCMeta` the keyword `metaclass` and its value; JavaScript's `extends ns.Base` minted `ns`, `extends +// mixin( B )` minted `mixin`. Now a dotted base is ONE base named by its last attribute (the qualifier rides on the ref +// for graph.h), a subscript's base is its value (type arguments are not bases), and a keyword argument, a call or a splat +// is a computed base this tool cannot name. Every Python child is an expression (its bases have no wrapper). +inline bool expressionBase( TSNode bn, const char* bt, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs, TSNode classNode ) +{ + if( lang == Lang::Ruby ) + { + return true; // Ruby has no wrapper: anything but a constant in its `superclass` clause is computed (see captureBases) + } + if( lang == Lang::Python ) + { + const TSNode named = kindIs( bt, "subscript" ) ? fieldChild( bn, NodeField::Value ) : bn; + const char* nt = ts_node_is_null( named ) ? "" : ts_node_type( named ); + if( kindIs( nt, "attribute" ) || kindIs( nt, "identifier" ) ) + { + emitBaseRef( named, fileId, lang, src, refs, classNode ); + } + return true; + } + if( lang != Lang::JavaScript && lang != Lang::TypeScript ) + { + return false; + } + if( kindIs( bt, "member_expression" ) ) + { + emitBaseRef( bn, fileId, lang, src, refs, classNode ); + return true; + } + return kindIs( bt, "call_expression" ); +} + +// one child of a base-clause WRAPPER (extends_clause / implements_clause / type_list): a type node (and TS's +// `implements ns.IFoo`, a nested_type_identifier) is a base; anything else is read as an expression base +inline void wrappedBase( TSNode wn, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs, TSNode classNode ) +{ + const char* wt = ts_node_type( wn ); + if( isBaseTypeNode( wt ) || kindIs( wt, "nested_type_identifier" ) ) + { + emitBaseRef( wn, fileId, lang, src, refs, classNode ); + return; + } + expressionBase( wn, wt, fileId, lang, src, refs, classNode ); +} + // Capture base classes for the inheritance/Lego view: walk a class node's base clause and emit an // inherit RawRef per base (derived → base). startByte sits inside the class header, so the enclosing // attribution assigns fromSymbol = the derived class. Explicit-syntax langs: C++/TS/JS/Java/Python/Swift/ @@ -477,6 +968,7 @@ void captureMacroBodyCalls( TSNode defineNode, std::uint32_t fileId, Lang lang, // child, collecting type nodes at both depths (Rust is a separate pass — impl Trait for T is a sibling). void captureBases( TSNode classNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs ) { + PROFILE_SCOPE_DESCRIBE( "ingest/relations: base classes of one class" ); // O(children) at all three levels. These child lists LOOK grammar-bounded — a class node's clauses, a // clause's base types — and the earlier class-3 reasoning said so, but EXTRAS refute it: a comment run // between two base types is spliced straight into the clause's own child array (src/infra/tschildren.h), @@ -508,13 +1000,14 @@ void captureBases( TSNode classNode, std::uint32_t fileId, Lang lang, std::strin const char* bt = ts_node_type( bn ); if( isBaseTypeNodeIn( bt, lang ) ) // DIRECT: type node right under the clause { - emitBaseRef( bn, fileId, lang, src, refs ); + emitBaseRef( bn, fileId, lang, src, refs, classNode ); return true; } // Ruby has no wrapper: its `superclass` clause holds ONE expression, and anything but a constant is // COMPUTED (`class Dynamic < Struct.new( :a )`). Descending into that (call) lands on its receiver // `Struct` and minted an edge to it — the receiver is not the base (test/rubyinheritcheck.sh floor (a)). - if( lang == Lang::Ruby ) + // Python's and JavaScript's base EXPRESSIONS are read whole the same way (expressionBase). + if( expressionBase( bn, bt, fileId, lang, src, refs, classNode ) ) { return true; } @@ -523,10 +1016,7 @@ void captureBases( TSNode classNode, std::uint32_t fileId, Lang lang, std::strin ChildCursor wrapCursor( bn ); forEachChild( bn, wrapCursor.cur, [ & ]( TSNode wn ) { - if( isBaseTypeNode( ts_node_type( wn ) ) ) - { - emitBaseRef( wn, fileId, lang, src, refs ); - } + wrappedBase( wn, fileId, lang, src, refs, classNode ); return true; } ); return true; @@ -667,6 +1157,20 @@ inline bool insideFunctionBody( TSNode n ) noexcept return false; } +// The whole written text of a type written `std::<…>` for a standard container whose element access yields a template +// argument (externalnames.h cppStdElementArgIndex: `std::map`, `std::vector`); "" for +// every other type, a global `::std::…` included (writtenTypeNamespace reads two segments). graph.h stdElementType +// reads the argument out of it (parser version 165). +inline std::string_view stdElementContainerText( TSNode typeNode, std::string_view src ) noexcept +{ + const TSNode tmpl = ( !ts_node_is_null( typeNode ) && writtenTypeNamespace( typeNode, src ) == "std" ) ? fieldChild( typeNode, NodeField::Name ) : TSNode{}; + if( ts_node_is_null( tmpl ) || !kindIs( ts_node_type( tmpl ), "template_type" ) ) + { + return {}; + } + return externalnames::cppStdElementArgIndex( nodeFieldText( tmpl, NodeField::Name, src ) ) < 0 ? std::string_view{} : nodeTextOf( typeNode, src ); +} + // one alias record: `aliasNode` names `target`, the class `spec` spells (see captureTypeAlias for the record's fields) inline void emitTypeAlias( TypeAliasCtx& cx, TSNode aliasNode, TSNode spec, std::string_view target ) { @@ -685,6 +1189,7 @@ inline void emitTypeAlias( TypeAliasCtx& cx, TSNode aliasNode, TSNode spec, std: r.name = target; r.recvVar = alias; r.qualifier = writtenTypeNamespace( spec, cx.src ); + r.memberPath = stdElementContainerText( spec, cx.src ); // `typedef std::map Bindings;`: the text its element type is read from cx.out.push_back( std::move( r ) ); } @@ -694,7 +1199,8 @@ inline void emitTypeAlias( TypeAliasCtx& cx, TSNode aliasNode, TSNode spec, std: // IRBuilderBase::CreateCall (test/fieldnarrowcheck.sh arm t). Rides the compose record shape — cached field for field, so the blob // format is unchanged — marked composeRel "alias" with an EMPTY fieldName, the one shape no member record has: `recvVar` = the // alias, `name` = the target's class name, `qualifier` = the namespace the target was written in (a `std` target is refused by -// its reader). Only a PLAIN alias of a named class records: a pointer/reference/array/function declarator, a target that names no +// its reader), `memberPath` = the target's whole text when it is a std element container (stdElementContainerText, parser +// version 166; graph.h StdMemberGate's element receivers read it, nothing else does). Only a PLAIN alias of a named class records: a pointer/reference/array/function declarator, a target that names no // class (aliasTargetName), a self-alias and an alias local to a function body (it types no member, and the name graph has no // scopes to keep it local) record nothing. inline void captureTypeAlias( TypeAliasCtx& cx, TSNode n, const char* t ) @@ -789,6 +1295,7 @@ inline WrittenPointee stdSmartPointee( TSNode typeNode, std::string_view src ) n // `w_.m()` does not (kParserVer 113). Every other qualified template (`std::vector v_;`) still records nothing. void captureFields( TSNode classNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs ) { + PROFILE_SCOPE_DESCRIBE( "ingest/relations: fields of one class" ); if( lang != Lang::Cpp ) { return; // C++ only for S5-E; extend for Python/TS later @@ -860,6 +1367,31 @@ void captureFields( TSNode classNode, std::uint32_t fileId, Lang lang, std::stri // and on rocksdb and llvm-project that lost six correct narrows and refused no wrong one (2026-09-17). typeName = std::string( pointee.name ); } + else if( const std::string_view containerText = stdElementContainerText( typeNode, src ); !containerText.empty() ) + { + // `std::map items_;` — an ELEMENT record (parser version 165), for StdMemberGate's element + // receivers (`items_[ k ].empty()`, graph.h stdReceiverOf) alone: composeRel "elem" with an EMPTY fieldName, so + // Rule 2b's field tables never read the container as the field's type (see the vector note above), qualifier + // `std`, so the HAS-A edges draw none; recvVar = the field, name = the container, memberPath = its whole text. + // A plain value member only. No other record ever came from such a field (the walk below finds no + // type_identifier directly under a qualified template), so continuing keeps every other capture as it was. + const TSNode elemDecl = fieldChild( fdecl, NodeField::Declarator ); + if( !ts_node_is_null( elemDecl ) && kindIs( ts_node_type( elemDecl ), "field_identifier" ) ) + { + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( fdecl ); + r.lang = lang; + r.isCompose = true; + r.composeRel = "elem"; + r.name = std::string( nodeFieldText( fieldChild( typeNode, NodeField::Name ), NodeField::Name, src ) ); + r.qualifier = "std"; + r.recvVar = std::string( nodeTextOf( elemDecl, src ) ); + r.memberPath = std::string( containerText ); + refs.push_back( std::move( r ) ); + } + continue; + } else { // Not a plain type_identifier type — could be template, qualified, etc. @@ -1099,8 +1631,10 @@ inline std::string phpUseTarget( TSNode useNode, std::string_view src ) // TS/JS `require("./x")` and dynamic `import("./x")` → the written specifier, or empty when this call // expression is not a module load. Its own function for the same reason csharpUsingTarget and phpUseTarget -// are: one AST shape per language stays a one-line call inside directiveTargetOf, whose grain is one -// branch per grammar spelling rather than one branch plus one nested scan. +// are: one AST shape per language stays a one-line call at the use site rather than one branch plus one nested +// scan. #358: its readers are now the DepDialect::Web normaliser (src/ingest_importcap.h — the captured +// `require`/`import` call) and captureJsImportFacts (src/ingest_jsimports.h — the binding a destructured +// require makes); directiveTargetOf no longer reads it, because the walk no longer visits TS/JS. // // THREE conditions, all required, and each is what keeps an ordinary call out of the dependency graph: // * the callee is the BARE identifier `require` or `import` — a member expression @@ -1569,7 +2103,9 @@ 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. +// else — a method body, another block, a file's top level — emits none. 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. // Is `call` a statement of an ActiveSupport::Concern `included do … end` block written at a module's body level? When a // class includes the concern the block runs in that class, so a mixin there joins the INCLUDER's ancestors; read as an // ancestor of the concern itself, it reaches the includer through the concern — activerecord's AttributeMethods @@ -1867,7 +2403,8 @@ inline bool isPreprocConditional( const char* type ) noexcept // --expand's sibling lift, the SameInclude call-resolution tier, and --impact's import tier) was reading // an empty table and reporting the emptiness as a horizontal, cycle-free architecture. The four entries // below are exactly the statement forms a top-level `require` is written in; the call node itself is read -// by directiveTargetOf, which is what keeps every OTHER call expression out. +// by jsModuleLoadTarget (read by the Web normaliser in src/ingest_importcap.h since #358), which is what keeps +// every OTHER call expression out. // // EVERY ENTRY HAS A FIXTURE ARM in test/nestedimportfix — an entry with no arm is an untested claim, and // every parent chain below was read off a real parse with `--match`, never predicted from the grammar. @@ -1994,7 +2531,10 @@ inline bool isFunctionLike( Lang lang, const char* type ) noexcept return false; } -// TS/JS: every container a `require("./x")` / `import("./x")` call can legitimately sit under. +// TS/JS: every container a `require("./x")` / `import("./x")` call can legitimately sit under. #358: no walk +// reads this table for TS/JS any more — it is now the REACH RULE src/ingest_importcap.h re-derives from a +// captured `@import.path` node's ancestry (a specifier under a node that is not listed here was never an edge, +// and still is not), and the `insideFn` source for the lazy bit (kJsFunctionContainers, below). // // The first five are the statement forms that wrap a TOP-LEVEL require, kParserVer 71's set — read off // real parses, not predicted: @@ -2028,7 +2568,7 @@ inline bool isFunctionLike( Lang lang, const char* type ) noexcept // function: (_) @f)'` returns a real `function:` field of text "import" here, so the earlier miss was the // missing container, never a grammar shape jsModuleLoadTarget could not read). // EVERY ENTRY still needs the SAME three jsModuleLoadTarget guards (bare require/import callee, one arg, a -// string literal) — this table only widens WHERE the walk looks, never what counts as a hit. +// string literal) — this table only widens WHERE a specifier may sit, never what counts as a hit. inline constexpr std::array kJsImportContainers = { "lexical_declaration", "variable_declaration", "variable_declarator", "expression_statement", "assignment_expression", "statement_block", "return_statement", "labeled_statement", "export_statement", @@ -2094,25 +2634,29 @@ constexpr std::uint16_t kMaxImportContainerDepth = 256; // READS them stay separately readable — the walk is one shape, this is one branch per grammar spelling. // // Node types confirmed per grammar: Python import_statement/import_from_statement; Go/Swift -// import_declaration; Rust use_declaration + mod_item; C# using_directive; TS/JS call_expression for the -// CommonJS `require("./x")` and dynamic `import("./x")` spellings; plus the kParserVer-81 four (bash +// import_declaration; Rust use_declaration + mod_item; C# using_directive; plus the kParserVer-81 four (bash // `command`, lua `function_call`, ruby `call`, elixir `call`). LEVER-B B0: non-C imports capture the // CLEAN written specifier via grammar child fields (module path / quoted specifier / use argument), not a // sliced clause — the sound resolver input. // -// #358: the C-family rows are GONE from this list. C++ preproc_include (path field, "" local vs <> +// #358: the C-family rows are GONE from this list, and so is Go's (the Go `import_declaration` clause is read by +// the DepDialect::Go normaliser; only Swift still reaches the `import_declaration` branch below). C++ preproc_include (path field, "" local vs <> // external) and C++ preproc_call with directive `#import` are captured as `@import.path` by the grammar's // own query and read by the DepDialect::CFamily normaliser (src/ingest_importcap.h), so this function is -// never asked about a C-family node. What is left is every language whose extraction needs the walk's own -// frame state — which is also the list of languages that have NOT moved over yet. +// never asked about a C-family node. The TS/JS rows (import_statement `source:`, the export_statement +// re-export, the CommonJS / dynamic call_expression) went the same way: `@import.path` in +// queries/{typescript,tsx,javascript}/tags.scm, DepDialect::Web normaliser. What is left is every language +// whose extraction needs the walk's own frame state — which is also the list of languages that have NOT +// moved over yet. // -// `lang` exists for exactly one branch: `call_expression` is a node type in most of our grammars, and a -// C++ or Rust function that happens to be named `require` must never manufacture a dependency edge. The -// language gate makes that impossible by construction rather than by relying on where the walk goes. +// `lang` exists for the language-gated branches: `call_expression` / `call` / `command` are node types in +// most of our grammars, and a C++ or Rust function that happens to be named `require` must never +// manufacture a dependency edge. The language gate makes that impossible by construction rather than by +// relying on where the walk goes. // -// `isLazy` (kParserVer 72, TS/JS; Ruby since parser version 82/83): true when `insideFn` says this call sits -// inside a closure container — see kJsFunctionContainers, the Ruby closure kinds, and captureIncludes' -// `insideFn` propagation below — or when the directive is a Ruby `autoload`. Allocates a std::string → +// `isLazy` (Ruby since parser version 82/83; TS/JS since kParserVer 72, now computed in ingest_importcap.h): +// true when `insideFn` says this call sits inside a closure container — see the Ruby closure kinds and +// captureIncludes' `insideFn` propagation below — or when the directive is a Ruby `autoload`. Allocates a std::string → // not noexcept. `isSymbolic` (parser version 82, Ruby only): the target is a CONSTANT resolved through the // corpus's own class/module index, never a path — see model.h Include::isSymbolic. // @@ -2122,8 +2666,38 @@ constexpr std::uint16_t kMaxImportContainerDepth = 256; // gone rather than documented as always-false. struct DirectiveTarget { std::string target; bool isLazy; bool isSymbolic; bool isReceiver; }; -// `insideFn` exists for exactly the same one branch `lang` does: whether the call_expression being read -// sits inside a TS/JS function body, per captureIncludes' walk — meaningless (and ignored) everywhere else. +// The written CLAUSE of an `import_declaration` — the declaration's own text with its leading keyword +// dropped (everything after the first space), at most 96 bytes, trailing `;` / space / newline / CR +// trimmed. This is the Include target Go has always carried (`"fmt"`, `f "fmt"`, `. "x"`, `_ "x"`, or a +// whole `( … )` group collapsed to ONE record), and Swift's `import Foundation`. Shared by the Swift walk +// branch below and by the DepDialect::Go normaliser (src/ingest_importcap.h) so the two cannot drift; the +// quirks are PRE-EXISTING and preserved byte-for-byte on purpose (#358 is a pure refactor): the keyword is +// dropped at a SPACE, so `import"os"` keeps the word `import`; a group is cut at 96 bytes, mid +// character if it lands there; a tab is not trimmed. Empty for an empty or out-of-range span. +std::string importClauseTarget( TSNode n, std::string_view src ) +{ + std::string target; + const std::uint32_t a = ts_node_start_byte( n ), b = ts_node_end_byte( n ); + if( a < b && b <= src.size() ) + { + std::string_view s = src.substr( a, b - a ); + const std::size_t sp = s.find( ' ' ); // drop the leading keyword + if( sp != std::string_view::npos ) + { + s = s.substr( sp + 1 ); + } + target.assign( s.data(), s.size() < 96 ? s.size() : 96 ); + while( !target.empty() && ( target.back() == ';' || target.back() == ' ' || target.back() == '\n' || target.back() == '\r' ) ) + { + target.pop_back(); + } + } + return target; +} + +// `insideFn` is the walk's sticky "inside a closure" bit, now read by Ruby alone (a constant receiver written +// inside a method / lambda / block is lazy). TS/JS used to be the other reader: its require()/import() branch +// moved to `@import.path` (#358), whose lazy bit is re-derived from ancestry in src/ingest_importcap.h. DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src, Lang lang, bool insideFn ) { std::string target; @@ -2131,36 +2705,19 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src bool isSymbolic = false; bool isReceiver = false; - if( kindIs( t, "import_statement" ) ) // Python `import a` / TS `import … from 'x'` + if( kindIs( t, "import_statement" ) ) // Python `import a` { // Prefer the grammar's specifier field over slicing the whole statement (LEVER-B B0: the resolver - // needs the REAL written specifier, not the clause). Empirically confirmed node shapes: + // needs the REAL written specifier, not the clause). Empirically confirmed node shape: // Python: import_statement name:(dotted_name|aliased_import) → the dotted module `pkg.mod`. - // TS/JS: import_statement source:(string) → the quoted specifier `'./x'`. - // A grammar that lacks the field resolves it to id 0, and fieldChild returns null for it, so a single - // capture covers both grammars without a per-language branch. - if( const TSNode src_ = fieldChild( n, NodeField::Source ); !ts_node_is_null( src_ ) ) - { - target = importSpecifierText( src_, src ); // TS/JS: strip the surrounding quotes - } - else if( const TSNode nm = fieldChild( n, NodeField::Name ); !ts_node_is_null( nm ) ) + // #358: the TS/JS `import … from 'x'` spelling of this node (and the `export … from 'x'` re-export that + // used to follow it) is GONE from this function — it is `@import.path` in queries/{typescript,tsx,javascript} + // /tags.scm now, read by the DepDialect::Web normaliser (src/ingest_importcap.h). + if( const TSNode nm = fieldChild( n, NodeField::Name ); !ts_node_is_null( nm ) ) { target = importSpecifierText( nm, src ); // Python: the dotted module head } } - else if( kindIs( t, "export_statement" ) && ( lang == Lang::TypeScript || lang == Lang::JavaScript ) ) - { - // kParserVer 124 (#220 part 2): a RE-EXPORT — `export { x } from './y'`, `export * from './y'`, - // `export * as ns from './y'`, `export type { T } from './y'` — loads its module exactly as an import does, - // and a barrel file is made of nothing else, so an import graph without them misses every cycle through a - // barrel. The grammar gives the specifier the same `source:` field import_statement has; every other - // export_statement (`export function …`, `export { x }`, `export default …`) has none and reads empty, and - // the walk still descends into it (it is a kJsImportContainers entry) for the requires inside. - if( const TSNode src_ = fieldChild( n, NodeField::Source ); !ts_node_is_null( src_ ) ) - { - target = importSpecifierText( src_, src ); - } - } else if( kindIs( t, "import_from_statement" ) ) // Python `from pkg.mod import Z` { // module_name:(dotted_name) → `pkg.mod`; module_name:(relative_import) → `.rel` / `..up` (leading @@ -2170,12 +2727,6 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src target = importSpecifierText( mn, src ); } } - else if( kindIs( t, "call_expression" ) - && ( lang == Lang::TypeScript || lang == Lang::JavaScript ) ) // TS/JS `require("./x")` / `import("./x")` - { - target = jsModuleLoadTarget( n, src ); - isLazy = insideFn && !target.empty(); // kParserVer 72: a hit found inside a function body is LAZY - } else if( kindIs( t, "command" ) && lang == Lang::Bash ) // Bash `source x.sh` / `. x.sh` { target = bashSourceTarget( n, src ); @@ -2255,25 +2806,14 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src } } } - else if( kindIs( t, "import_declaration" ) ) // Go / Swift — captured but NOT precise-resolved + else if( kindIs( t, "import_declaration" ) ) // Swift — captured but NOT precise-resolved { - // Go (needs go.mod module-root) and Swift (whole-module, no path) are DEFERRED — the precise - // resolver leaves them unresolved. Keep the best-effort target for --uses / --deps back-compat. - const uint32_t a = ts_node_start_byte( n ), b = ts_node_end_byte( n ); - if( a < b && b <= src.size() ) - { - std::string_view s = src.substr( a, b - a ); - const std::size_t sp = s.find( ' ' ); // drop the leading keyword - if( sp != std::string_view::npos ) - { - s = s.substr( sp + 1 ); - } - target.assign( s.data(), s.size() < 96 ? s.size() : 96 ); - while( !target.empty() && ( target.back() == ';' || target.back() == ' ' || target.back() == '\n' || target.back() == '\r' ) ) - { - target.pop_back(); - } - } + // #358: Go moved onto `@import.path` (queries/go/tags.scm + the DepDialect::Go normaliser in + // src/ingest_importcap.h), so this branch is reached by Swift ALONE now. The clause reader is the + // one both languages shared and is kept as ONE function (importClauseTarget) rather than copied: + // Swift (whole-module, no path) is DEFERRED in the precise resolver, and its slice has its own + // landing. Keep the best-effort target for --uses / --deps back-compat. + target = importClauseTarget( n, src ); } else if( kindIs( t, "using_directive" ) ) { // C# `using Foo.Bar;` / `using static Foo;` / `using X = Foo.Bar;` @@ -2293,14 +2833,12 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src // Capture #include / import directives (physical dependencies) by walking the file's top-level nodes — // and the bodies of anything that WRAPS a directive, which tree-sitter does not flatten: preprocessor -// conditionals in the C family and C#, and ordinary language constructs in Python / Rust / C# / TS / JS -// (see isImportContainer). Each node is read by directiveTargetOf above. -// kParserVer 72 cost note: TS/JS's kJsImportContainers now includes call_expression/object/arguments — -// containers a require() sits under with no other purpose — so the walk over a TS/JS file approaches full -// AST size rather than "top-level statements only". Bounded per-node (each node is visited once) and still -// depth-capped by kMaxImportContainerDepth; no perf gate exists in this tree to budget against (see -// CLAUDE.md's "best tool first, then fast" note), and CommonJS's own weight — an entire module system's -// worth of edges was previously invisible (kParserVer 71's LB-H measurement) — is the justification. +// conditionals in C# (the C family and TS/JS read `@import.path` instead, #358), and ordinary language +// constructs in Python / Rust / C# (see isImportContainer). Each node is read by directiveTargetOf above. +// kParserVer 72 cost note (HISTORICAL for TS/JS since #358, which retired this walk for them — the cost is gone +// with it): TS/JS's kJsImportContainers included call_expression/object/arguments — containers a require() +// sits under with no other purpose — so the walk over a TS/JS file approached full AST size rather than +// "top-level statements only". // ABS-3: each directive ALSO emits an import-role RawRef (name = the importable final segment) so the // use-site index reports import sites. The ref is file-scope (fromSymbol=kNoNode) — that is correct for // a directive at any container depth, and it NEVER enters the call graph (role != Call → skipped in @@ -2441,10 +2979,11 @@ void captureIncludes( TSNode root, Lang lang, std::uint32_t fileId, std::string_ // order, which keeps `incs`/`refs` in SOURCE order: the determinism contract is byte-identity, and an // order that depended on the walk shape would break it. Only ALLOWLISTED containers are entered, so a // language whose imports are top-level by rule (Go, Java) still costs exactly the old scan. - // `insideFn` (kParserVer 72, TS/JS only): true once the walk has descended through a function-body - // container (kJsFunctionContainers) — sticky for every descendant, never cleared, exactly like `depth` + // `insideFn` (kParserVer 72, TS/JS; Ruby's closure kinds since 83): true once the walk has descended through a + // function-body container (isFunctionLike) — sticky for every descendant, never cleared, exactly like `depth` // is monotonic. It rides the frame rather than being recomputed from ancestry because the walk never - // keeps the ancestor chain around: this is the one bit of it a lazy-require call needs. + // keeps the ancestor chain around: this is the one bit of it a lazy-require call needs. #358: TS/JS no longer + // walks (its lazy bit is re-derived from ancestry in ingest_importcap.h), so Ruby is its only reader here. // `openIdx` (parser version 83, Ruby only): the index in `constOpens` of the innermost class/module open the // frame sits inside — kNoOpenIdx at file level. It rides the frame for the same reason `insideFn` does (the // walk keeps no ancestor chain) and exists for the RECEIVER DEDUPE: a constant receiver is recorded once per diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 6ea1140e7..2a847aacc 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1150,6 +1150,90 @@ bool prepareParserFor( TSParser* parser, const LangEntry& le ) return true; } +// ── cold-ingest profile scopes by LANGUAGE FAMILY (cli-perf-068 STEP 0) ────────────────────────────────────────── +// A PROFILE_SCOPE names one code site, so the parse pool's per-file phases (parse, side captures, tags captures) are +// opened through underExtractScope, which picks one of eight family-named sites per phase: the self-profile then splits +// a cold index's parse and extraction cost by language family (the LLVM memory run reads these). In a build without +// RIPWIRE_PROFILE the macro is nothing and this is a switch that calls `work` — same work, same order, same answer. +enum class ExtractFamily : std::uint8_t { CFamily, Python, JsTs, JvmDotNet, GoRustSwiftDart, Scripting, Bash, DocsData, Count }; +enum class ExtractPhase : std::uint8_t { Parse, SideFacts, TagsFacts, Count }; + +// One row per Lang, in enumerator order (model.h): the size assert below fails when a language is appended without a row. +inline constexpr ExtractFamily kExtractFamilyByLang[] = { + ExtractFamily::CFamily, // Cpp + ExtractFamily::Python, // Python + ExtractFamily::JsTs, // TypeScript + ExtractFamily::GoRustSwiftDart, // Go + ExtractFamily::GoRustSwiftDart, // Rust + ExtractFamily::GoRustSwiftDart, // Swift + ExtractFamily::CFamily, // ObjC + ExtractFamily::DocsData, // Markdown + ExtractFamily::JsTs, // JavaScript + ExtractFamily::Bash, // Bash + ExtractFamily::JvmDotNet, // Java + ExtractFamily::Scripting, // Ruby + ExtractFamily::DocsData, // Unknown + ExtractFamily::DocsData, // Json + ExtractFamily::JvmDotNet, // CSharp + ExtractFamily::CFamily, // C + ExtractFamily::DocsData, // Toml + ExtractFamily::DocsData, // Yaml + ExtractFamily::Scripting, // Php + ExtractFamily::Scripting, // Lua + ExtractFamily::Scripting, // Elixir + ExtractFamily::GoRustSwiftDart, // Dart + ExtractFamily::JvmDotNet, // Kotlin + ExtractFamily::Scripting, // GDScript +}; +static_assert( std::size( kExtractFamilyByLang ) == kLangCount, "one profile family row per Lang, in enumerator order" ); +static_assert( kExtractFamilyByLang[ std::size_t( Lang::Kotlin ) ] == ExtractFamily::JvmDotNet && kExtractFamilyByLang[ std::size_t( Lang::C ) ] == ExtractFamily::CFamily, + "the table follows Lang's enumerator order" ); + +constexpr ExtractFamily extractFamilyOf( Lang lang ) noexcept +{ + const std::size_t langIdx = static_cast( lang ); + return langIdx < kLangCount ? kExtractFamilyByLang[ langIdx ] : ExtractFamily::DocsData; +} + +inline constexpr const char* kExtractScopeNames[ std::size_t( ExtractPhase::Count ) ][ std::size_t( ExtractFamily::Count ) ] = { + { "ingest/extract[c-family]: parse one file", "ingest/extract[python]: parse one file", "ingest/extract[js-ts]: parse one file", + "ingest/extract[jvm-dotnet]: parse one file", "ingest/extract[go-rust-swift-dart]: parse one file", "ingest/extract[scripting]: parse one file", + "ingest/extract[bash]: parse one file", "ingest/extract[docs-data]: parse one file" }, + { "ingest/extract[c-family]: side captures (relations, binds, includes, values)", "ingest/extract[python]: side captures (relations, binds, includes, values)", + "ingest/extract[js-ts]: side captures (relations, binds, includes, values)", "ingest/extract[jvm-dotnet]: side captures (relations, binds, includes, values)", + "ingest/extract[go-rust-swift-dart]: side captures (relations, binds, includes, values)", "ingest/extract[scripting]: side captures (relations, binds, includes, values)", + "ingest/extract[bash]: side captures (relations, binds, includes, values)", "ingest/extract[docs-data]: side captures (relations, binds, includes, values)" }, + { "ingest/extract[c-family]: tags captures (defs, refs, metrics)", "ingest/extract[python]: tags captures (defs, refs, metrics)", + "ingest/extract[js-ts]: tags captures (defs, refs, metrics)", "ingest/extract[jvm-dotnet]: tags captures (defs, refs, metrics)", + "ingest/extract[go-rust-swift-dart]: tags captures (defs, refs, metrics)", "ingest/extract[scripting]: tags captures (defs, refs, metrics)", + "ingest/extract[bash]: tags captures (defs, refs, metrics)", "ingest/extract[docs-data]: tags captures (defs, refs, metrics)" }, +}; + +template +decltype( auto ) inExtractScope( Work&& work ) +{ + PROFILE_SCOPE_DESCRIBE( kExtractScopeNames[ std::size_t( Phase ) ][ std::size_t( Family ) ] ); + return work(); +} + +template +decltype( auto ) underExtractScope( Lang lang, Work&& work ) +{ + switch( extractFamilyOf( lang ) ) + { + case ExtractFamily::CFamily: return inExtractScope( work ); + case ExtractFamily::Python: return inExtractScope( work ); + case ExtractFamily::JsTs: return inExtractScope( work ); + case ExtractFamily::JvmDotNet: return inExtractScope( work ); + case ExtractFamily::GoRustSwiftDart: return inExtractScope( work ); + case ExtractFamily::Scripting: return inExtractScope( work ); + case ExtractFamily::Bash: return inExtractScope( work ); + case ExtractFamily::DocsData: break; + case ExtractFamily::Count: break; + } + return inExtractScope( work ); +} + TSTree* parseTree( TSParser* parser, std::string_view src ) { TSTree* tree = nullptr; @@ -1546,11 +1630,12 @@ void captureSideFacts( const LangEntry& le, std::uint32_t fileId, std::string_vi #endif // Elixir directives share the lexical tags context below (ElixirContext), and as of #358 so do the - // C-family ones: their `@import.path` captures are normalised and emitted by captureTagsFacts, and - // captureIncludes no longer reads a preproc_include / preproc_call at all. The walk is pure cost - // for those languages — it descends only allowlisted containers, and a C-family include lives at - // file scope or inside a preprocessor guard, which the tags query already reaches unanchored. - if( le.lang != Lang::Elixir && dependencyDialect( le.lang ) != DepDialect::CFamily ) + // C-family ones and TypeScript/JavaScript's: their `@import.path` captures are normalised and emitted + // by captureTagsFacts, and captureIncludes no longer reads a preproc_include / preproc_call / + // import_statement / export_statement / require() call at all. The walk is pure cost for those + // languages — it descends only allowlisted containers, and the tags query already reaches the same + // nodes unanchored (importsFromCapture names the dialects; ingest_importcap.h keeps the reach rule). + if( le.lang != Lang::Elixir && !importsFromCapture( dependencyDialect( le.lang ) ) ) { captureIncludes( root, le.lang, fileId, src, incs, refs, binds, constOpens, shortfall ); } @@ -1725,6 +1810,14 @@ inline bool isCFamilyTypeSpecifier( Lang lang, TSNode node ) noexcept return kindIs( t, "enum_specifier" ) || kindIs( t, "struct_specifier" ) || kindIs( t, "union_specifier" ) || kindIs( t, "class_specifier" ); } +// The two postures of one tags pass. Full: every fact the index keeps. DefinitionsOnly (definitionLinesInBlobs, --whereis on +// other refs): the SAME definitions — every step that decides a definition's existence, name, line, kind or scope runs +// unchanged — without the work only references, imports and metrics read: the @reference arm, @import emission, +// complexityOf, the function-scope span, the def arm's IS-A / HAS-A / macro-body reference minting and Ruby's +// reference and bind passes. Measured on 104 C++ header blobs: those were ~60% of this function's samples (cc_walk alone +// ~30%, the reference arm's qualifier / receiver parent walks ~30%). +enum class TagsCapture : std::uint8_t { Full, DefinitionsOnly }; + /// Append definitions and references captured by the language query, with language-specific filtering. /// Captured spans refer to src and root; a null cursor appends nothing. Existing output rows are retained. /// `ppDead` is this file's decided-dead byte ranges (preprocDeadRangesFor, computed ONCE per file by the @@ -1732,13 +1825,15 @@ inline bool isCFamilyTypeSpecifier( Lang lang, TSNode node ) noexcept /// file's tags pass runs later, at the prewarm flush). void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t fileId, std::string_view src, TSNode root, std::vector& defs, std::vector& refs, std::vector& binds, std::vector& includes, - const std::vector& ppDead, ExtractShortfall& shortfall ) + const std::vector& ppDead, ExtractShortfall& shortfall, TagsCapture mode = TagsCapture::Full ) { + const bool full = mode == TagsCapture::Full; if( cursor == nullptr ) { return; } + BaseScopeIndexGuard baseScopes; // lane lego-transitive fix 1 (mutable: lookups fill it): this file's base-lookup scope tables (ingest_relations.h) TSQuery* query = compiledQueryFor( le ); // shared immutable query, compiled once per grammar (pre-warmed) — do NOT delete if( query == nullptr ) { @@ -1860,7 +1955,10 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // order while costing an allocation and a drain block. case CapRole::Import: { - emitCapturedImport( cap.node, fileId, le.lang, src, includes, refs, shortfall ); + if( full ) + { + emitCapturedImport( cap.node, fileId, le.lang, src, includes, refs, shortfall ); + } } break; } @@ -2101,7 +2199,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // A literal-bound name measures its LITERAL; every other def measures defNode, as before. const bool literalBound = !ts_node_is_null( own.literal ); const TSNode metricNode = literalBound ? own.literal : defNode; - const auto [ cxVal, ccxVal, nestVal, localsVal, ppAltVal, humpsVal, deepVal, evVal, evWhyVal ] = fnOrMethod ? complexityOf( spanThroughBody ? body : metricNode, src, le.lang ) : Complexity{ 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, {} }; + const auto [ cxVal, ccxVal, nestVal, localsVal, ppAltVal, humpsVal, deepVal, evVal, evWhyVal ] = fnOrMethod && full ? complexityOf( spanThroughBody ? body : metricNode, src, le.lang ) : Complexity{ 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, {} }; d.cx = cxVal; d.ccx = ccxVal; d.locals = localsVal; // Phase 1: floor count, C/C++ only (model.h localsCountedLang) — 0 elsewhere, never emitted there @@ -2176,7 +2274,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t d.internalLinkage = internalLinkageBit( le.lang, defNode, src ); // A FUNCTION bound inside another function's body records that function's span (ingest_names.h // enclosingFunctionScope): graph.h reachableByName ranks it below every def a call outside it can name. - if( kind == SymKind::Function && !bindsOutsideItsFunction( roleNode, le.lang ) ) + if( full && kind == SymKind::Function && !bindsOutsideItsFunction( roleNode, le.lang ) ) { // Descend to the OUTER of the two def nodes: a C-family def's defNode climbed from its declarator (the role // node) to the function_definition, which must not read as the function enclosing itself; a multi-name @@ -2261,9 +2359,17 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // holds an error (fileHasError, one O(1) flag test per file) — see parseRecoveredBits. d.recovered = fileHasError ? parseRecoveredBits( defNode, kind, le.lang, d.scope.empty() ) : std::uint8_t( 0 ); defs.push_back( std::move( d ) ); + if( !full ) + { + continue; // TagsCapture::DefinitionsOnly: the reference edges below feed no definition + } if( kind == SymKind::Class || isStructOrNamedType( kind ) || kind == SymKind::Interface ) { captureBases( defNode, fileId, le.lang, src, refs ); // IS-A: inheritance edges (derived → base) + if( defCapSv == "definition.classfactory" ) // lane lego-transitive (M2): the factory's receiver is the base + { + emitBaseRef( jsClassFactoryReceiver( nameNode, src ), fileId, le.lang, src, refs, defNode ); + } captureFields( defNode, fileId, le.lang, src, refs ); // HAS-A: member-variable type edges (S5-E) } else if( kind == SymKind::Macro ) @@ -2271,7 +2377,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t captureMacroBodyCalls( roleNode, fileId, le.lang, src, refs ); // macro-edges: the graph connects THROUGH the macro } } - else if( isRef ) + else if( isRef && full ) { // H4: a C++ cast keyword is not a call — see isCppCastKeyword. Valid input, skipped, no alert. if( le.lang == Lang::Cpp && isCppCastKeyword( nameTxt ) ) @@ -2426,7 +2532,11 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // `include` in `class << self` — into binds, beside the inherit reference each mixin already is. if( le.lang == Lang::Ruby ) { - captureRubyAttrDefs( root, fileId, src, defs ); + captureRubyDefs( root, fileId, src, defs ); // #339: the Definitions-only walker (attr defs + rendered db/schema.rb columns) + if( !full ) + { + return; // TagsCapture::DefinitionsOnly: the passes below write references and binds only + } captureRubyBareCalls( root, fileId, src, refs ); captureRubyFactories( root, fileId, src, binds ); captureRubyHelperMethods( root, fileId, src, binds ); diff --git a/src/landingplan.h b/src/landingplan.h index a69fbbda3..0069333de 100644 --- a/src/landingplan.h +++ b/src/landingplan.h @@ -73,6 +73,7 @@ struct PlanResult bool nonGitRoot = false; // --stray-content's own refusal reasons, passed through bool tooManyRefs = false; // verbatim — this verb adds no refusal of its own bool filterMatchedNothing = false; // H7: the filter selected no ref NAME — a refusal, never refs="0" + bool blobsUnread = false; // K75: the sweep's blob batch was unread — passed through, a refusal crossref::StrayResult stray; // the full sweep (headSha/refsScanned/mergedRefs/refs) std::vector scouted; // indices into stray.refs: the landing set fed to merge-scout std::vector bounded; // indices into stray.refs: unmerged, but cut by the size bound @@ -126,6 +127,7 @@ inline PlanResult computePlan( const std::string& root, std::string_view filter, result.nonGitRoot = result.stray.nonGitRoot; result.tooManyRefs = result.stray.tooManyRefs; result.filterMatchedNothing = result.stray.filterMatchedNothing; + result.blobsUnread = result.stray.blobsUnread; return result; } diff --git a/src/lsp.h b/src/lsp.h index f5a2fcdad..634e40c1b 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -99,7 +99,7 @@ inline LspFrame lspReadMessage( std::FILE* in ) std::string low = head; std::transform( low.begin(), low.end(), low.begin(), - [ ]( unsigned char c ) { return static_cast( std::tolower( c ) ); } ); + [ ]( char c ) { return static_cast( std::tolower( static_cast( c ) ) ); } ); // char in, explicit narrowing: an unsigned char parameter is G1's implicit sign-change on a byte >= 0x80 const std::size_t k = low.find( "content-length:" ); if( k == std::string::npos ) { f.eof = true; f.malformed = true; return f; } std::size_t len = 0; @@ -681,7 +681,7 @@ inline std::string lspWorkspaceSymbol( const rw::IngestResult& ing, const std::s const auto lowerCopy = [ ]( std::string s ) { std::transform( s.begin(), s.end(), s.begin(), - [ ]( unsigned char c ) { return static_cast( std::tolower( c ) ); } ); + [ ]( char c ) { return static_cast( std::tolower( static_cast( c ) ) ); } ); // char in, explicit narrowing: an unsigned char parameter is G1's implicit sign-change on a byte >= 0x80 return s; }; const std::string qLower = lowerCopy( std::string( query ) ); diff --git a/src/main.cpp b/src/main.cpp index ee4a09577..55bab907c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -89,6 +89,7 @@ static_assert( rw::kTestGateCcxBarMirror == rw::quality::kCcxBar, "situ.h kTestG #include "didyoumean.h" // §P12.1 / §B6 M8: the ONE near-miss suggester, now shared with the MCP refusal table #include "selectorrefuse.h" // §B4.2: the ONE file:name selector not-found refusal — all six SYM-taking verbs #include "gitmine.h" +#include "historyread.h" // K51: the disclosure of a git history walk that could not be read (churn=/amp=) #include "ownersview.h" // §P6.4: countUniformOwnership/ownershipRowsToPrint — shared with mcpverbs.h's `owners` verb #include "mention.h" // B8: query-mention anchoring — files/modules/symbols NAMED in the --for text #include "siblift.h" // r4 EXPERIMENT: env-gated same-directory sibling lift (inert by default) @@ -1080,6 +1081,12 @@ inline std::FILE* openTokenBudgetBuffer( rw::MemoryStream& stream, TokenBudgetSt // here the same way the layer will compact it and repriced by the same rule (compactlegend.h compactRepricedTokens), // so the number decided on is the number the root prints. A body the dialect cannot shape keeps its own price — an // empty one (the map streamed unbuffered) and a --json one (no posture) included. +// map-inventory-d7: the number decided on is READ from the compacted body — the root's est_tokens=, or the map header's +// field when --stable keeps the root free of it — never recomputed from compacted.size(). That recount priced the bytes +// AFTER the layer had rewritten the est digits themselves (a 5-digit full price repriced to 4 digits is 2 bytes shorter +// in the root and the header), so on that boundary the gate said one token less than the header it was asserting +// (tokenbudgetcheck #9: src --top-k=233 on 7608d1f6, header 9965, gate 9964). The recount stays only as the fallback for +// a compacted body that prints no price at all. static std::size_t compactPostureMapPrice( const rw::Config& cfg, std::string_view body, std::size_t fullEstTokens ) { if( cfg.legend != "compact" || fullEstTokens == 0 ) @@ -1091,6 +1098,29 @@ static std::size_t compactPostureMapPrice( const rw::Config& cfg, std::string_vi { return fullEstTokens; } + const std::string_view doc = compacted; + const rw::CompactRootInfo root = rw::findCompactRoot( doc ); + if( const std::size_t printed = root.tag.empty() ? 0 : rw::rootUnsignedAttr( doc.substr( root.openBegin, root.openEnd - root.openBegin ), "est_tokens" ); + printed > 0 ) + { + return printed; + } + const std::string_view header = rw::compactMapHeader( doc ); + if( const std::size_t at = header.find( " est_tokens=" ); at != std::string_view::npos ) + { + std::size_t v = 0; + bool any = false; + for( std::size_t i = at + 12; i < header.size() && header[ i ] >= '0' && header[ i ] <= '9'; ++i ) + { + v = v * 10 + std::size_t( header[ i ] - '0' ); + any = true; + } + if( any && v > 0 ) + { + return v; + } + } + // no printed price to read (a body whose root and header carry none): the recount is the only number there is return static_cast( rw::compactRepricedTokens( static_cast( fullEstTokens ), body.size(), compacted.size() ) ); } @@ -1205,6 +1235,28 @@ inline std::string churnDecayWindowLabel( std::string_view minedSpan ) return label; } +// The crawl-scope flags a pasted continuation must replay so it indexes the SAME files this run did: --exclude, +// --max-file-size, --no-ignore, --ignore-tests (the enumeration below says why these four and no others). Each is +// returned with its leading space; empty at the defaults. Shared by scopedMapNextInvocation and leanMapPointer. +inline std::string crawlScopeFlags( const rw::Config& cfg ) +{ + std::string inv; + for( const std::string& x : cfg.excludes ) + { + inv += " " + rw::nextFlag( "--exclude=", x ); + } + // The ceiling is replayed as the BYTE COUNT it resolved to, not as the caller's "2K": parseByteSize accepts + // plain digits, and the number is what shaped the crawl. Omitted at the default, so the common hint is + // unchanged to the byte. + if( cfg.maxFileBytes != rw::kDefaultMaxFileBytes ) + { + inv += " --max-file-size=" + std::to_string( cfg.maxFileBytes ); + } + if( cfg.noIgnore ) { inv += " --no-ignore"; } + if( cfg.ignoreTests ) { inv += " --ignore-tests"; } + return inv; +} + // C1-b (2026-09-12): --in=DIR's page of rows, from the SAME mining pass as the global block. The prefix is // matched on the ROOT-RELATIVE spelling the map prints (serialize.h pathRel: rootRelativeUri against the crawl root as // typed), so a scoped row is byte-identical to its global twin — a sub-root-relative spelling missed every held-out gold @@ -1268,19 +1320,7 @@ inline std::string scopedMapNextInvocation( const rw::Config& cfg, std::string_v { inv += " " + rw::nextFlag( "--since=", cfg.since ); } - for( const std::string& x : cfg.excludes ) - { - inv += " " + rw::nextFlag( "--exclude=", x ); - } - // The ceiling is replayed as the BYTE COUNT it resolved to, not as the caller's "2K": parseByteSize accepts - // plain digits, and the number is what shaped the crawl. Omitted at the default, so the common hint is - // unchanged to the byte. - if( cfg.maxFileBytes != rw::kDefaultMaxFileBytes ) - { - inv += " --max-file-size=" + std::to_string( cfg.maxFileBytes ); - } - if( cfg.noIgnore ) { inv += " --no-ignore"; } - if( cfg.ignoreTests ) { inv += " --ignore-tests"; } + inv += crawlScopeFlags( cfg ); if( withIn ) { inv += " " + rw::nextFlag( "--in=", scopeDir ); @@ -1525,6 +1565,83 @@ inline constexpr const char* kMapRidesAlongFmt = " note=\"the ranked top-{} map below rides along with the requested payload; --top-k=0 for the payload " "alone (--top-k=1 for a minimal map)\""; +// ── expand-lean-k64: --expand/--outline whose requested definitions were ALL served ride no map ───────────────────── +// A multi-definition name used to ship the generic top-200 map beside bodies that already answer it (0 of 185 rows named +// the symbol). When every requested definition is served there is nothing left to disambiguate: top-k defaults to 0, the +// root says so (topk_default="0", the exact-name default's own disclosure) and carries ONE recoverable pointer to the map +// it did not ship (map_next=, a shell line: the map is the default verb of `ripwire ROOT`). The bodies' headers already +// carry p= l= t= for every definition. When some were NOT served (cut by the byte budget, unreadable), the root names +// exactly those — unserved_total= (the true count) and unserved_next= (the --expand/--outline call serving them, quoted by +// the next= house rule) — never the generic map. kMaxUnservedSelectors is a blow-up guard (the kMaxExpandSibs precedent), +// not a target: past it unserved_next= serves the FIRST kMaxUnservedSelectors and unserved_listed= says how many it lists; +// the rest are recovered by the same call with a larger --pack-budget-bytes (the legend says so). Language-neutral: the +// decision reads the served-id record, not any language's syntax. An EXPLICIT --top-k keeps today's shape. +inline constexpr std::size_t kMaxUnservedSelectors = 16; + +// `ripwire ROOT…`: EVERY root this call crawled (a multi-root call's map is the map of all of them — an empty mapRootArg +// there used to fall back to ".", the parent tree), each quoted by the next= house rule, plus the crawl-scope flags that +// decided which files were indexed, so the pasted call maps the same corpus. Presentation and payload flags stay out. +inline std::string leanMapPointer( const rw::Config& cfg ) +{ + std::string inv = "ripwire"; + for( const std::string_view root : cfg.roots ) + { + inv += " " + rw::nextFlag( "", root ); + } + if( cfg.roots.empty() ) + { + inv += " ."; + } + return inv + crawlScopeFlags( cfg ); +} + +struct UnservedDefs +{ + std::size_t requested = 0; // valid, non-module-scope requests + std::size_t unserved = 0; // of those, the ones with no body/outline in the answer + std::string attrs; // ` unserved_total="N" [unserved_listed="K"] unserved_next="--expand=P:L:N,…"` — empty when unserved == 0 +}; + +inline UnservedDefs unservedDefsOf( const rw::IngestResult& ing, const std::vector& requested, std::vector served, + std::string_view verbFlag, std::string_view rootArg ) +{ + UnservedDefs u; + std::sort( served.begin(), served.end() ); + const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); + std::string selectors; + std::size_t listed = 0; + for( rw::NodeId id : requested ) + { + if( id >= ing.symbols.size() || ing.symbols[ id ].kind == rw::SymKind::ModuleScope ) + { + continue; + } + ++u.requested; + if( std::binary_search( served.begin(), served.end(), id ) ) + { + continue; + } + ++u.unserved; + if( listed >= kMaxUnservedSelectors ) + { + continue; + } + const rw::Symbol& sym = ing.symbols[ id ]; + const std::string_view path = rootArg.empty() ? std::string_view( ing.files[ sym.fileId ] ) : rw::sarif::rootRelativeUri( ing.files[ sym.fileId ], rootPrefix ); + selectors += ( listed == 0 ? "" : "," ); + selectors += std::string( path ) + ":" + std::to_string( sym.line ) + ":" + sym.name; + ++listed; + } + if( u.unserved > 0 ) + { + // unserved_listed= only when the guard cut the list: unserved_next= then serves the first `listed`, not all of them. + u.attrs = " unserved_total=\"" + std::to_string( u.unserved ) + "\"" + + ( listed < u.unserved ? " unserved_listed=\"" + std::to_string( listed ) + "\"" : std::string() ) + + rw::nextAttrXml( rw::nextFlag( std::string( verbFlag ) + "=", selectors ), "unserved_next" ); + } + return u; +} + // ── ONE PRICE FOR BOTH SERVING CANDIDATES (CodeRabbit, PR #215, second round on this comparison) ───── // THE DEFECT was not a missing addend, it was two counters. The bundle candidate was priced to the byte // (envelope, root attributes, the unproven residue, the map, the rendered , the closing tag) by the @@ -1872,6 +1989,7 @@ int runDefaultMap( const MainDispatch& d ) mapAnn.viaLegendStripped = cfg.legend == "compact" && !cfg.json; // FE-B: the rewrite would strip the map's via comment mapAnn.notesDegraded = d.notesDegraded; // L3 follow-up (CodeRabbit 4053600616): onto every this run emits mapAnn.codeFirstRows = isDefaultMapScope; // the code-first row pick + its data_sections_cut= / next= (serialize.h) + mapAnn.inventoryTier = isDefaultMapScope && !cfg.noInventory; // D7: the tier (mapInventoryWanted also drops it under --max-tokens) // C1-b (2026-09-12): --in=DIR — the scoped block and the map stub, filled by assignment like seed. The two next= strings // outlive every serialize() call below (mapAnn holds views into them). The scoped next= is the SAME run at the next // offset, page size carried when the caller set one; the stub's next= is the same run without in= (the map it stubbed). @@ -1921,7 +2039,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 ); + 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 ); const rw::MemoryStreamBytes measured = probe.finish(); return measured.isWhole ? std::string( measured.bytes ) : std::string(); }; @@ -1934,7 +2052,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 ); + 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 ); const rw::MemoryStreamBytes measured = probe.finish(); if( !measured.isWhole ) { @@ -1978,7 +2096,7 @@ int runDefaultMap( const MainDispatch& d ) } serializeJson( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, - g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, mapProvPtr, mapAnn, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly ); + g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, mapProvPtr, mapAnn, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.outNameOnly.empty() ? nullptr : &g.outNameOnly, g.stdMemberDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); if( !measured.isWhole ) { @@ -2319,21 +2437,64 @@ int runDefaultMap( const MainDispatch& d ) // measurement). Built only when a body is expanded; the same vector rides the two emissions below, so the // charged render and the emitted one cannot differ. const std::vector calleeOrder = expandNodes.empty() ? std::vector{} : rw::calleeNameSpecificity( ing ); + EmittedBodies expandRecord; // expand-lean-k64: which requested bodies the render above actually emitted if( !expandNodes.empty() ) { bodiesSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, - expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/nullptr, + { expandRecord = EmittedBodies{}; // a re-render starts from empty: the record is what THIS render emitted + packBodies( f, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/&expandRecord, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); }, // V1: octocode F2 sibs=/inc= rw::kBytesPerTokenBody ); } + std::vector outlineShown; // expand-lean-k64: the outlines the render below emitted if( !outlineNodes.empty() ) { outlineSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packOutline( f, ing, outlineNodes, cfg.packBudgetBytes, cfg.compress, redactPtr, mapRootArg ); }, + { outlineShown.clear(); + packOutline( f, ing, outlineNodes, cfg.packBudgetBytes, cfg.compress, redactPtr, mapRootArg, &outlineShown ); }, rw::kBytesPerTokenBody ); } + // expand-lean-k64 (see kMaxUnservedSelectors above): a payload verb whose requested definitions were ALL served rides no + // map. Decided HERE, after the payload rendered, because "all served" is a property of the render; nothing between the + // exact-name default above and this point reads mapTopK. Same exclusions as the exact-name default (composed verbs claim + // the map for their own purpose; --max-tokens sizes the map; --json has its own twin), plus an explicit --top-k. + // Scope: every --expand the exact-name default above did not take (one ambiguous name, several tokens, several tokens + // naming one definition) and ONE --outline token (any count — a single-definition --outline rode the same map). + // --outline with several tokens keeps the ordinary default. Consequence: wherever the M6 mode choice runs, the map is + // already gone, so its bundleDoc.mapBytes term below is 0 from the CLI (kept: the price must stay honest if a future + // posture rides a map there). + std::string ctxLeanAttr = exactNameExpandDefault ? std::string( " topk_default=\"0\"" ) : std::string(); + std::string ctxUnservedAttr; + const bool leanEligible = !exactNameExpandDefault && !cfg.topKExplicit && !cfg.packSignatures && cfg.packTopN == 0 && cfg.query.empty() + && !cfg.adaptive && cfg.maxTokens == 0 && !cfg.json + && ( ( !cfg.expand.empty() && cfg.outline.empty() && !expandNodes.empty() && bodiesSection.isRendered ) + || ( cfg.outline.size() == 1 && cfg.expand.empty() && !outlineNodes.empty() && outlineSection.isRendered ) ); + if( leanEligible ) + { + std::vector served; + if( cfg.expand.empty() ) + { + served = outlineShown; + } + else + { + for( const EmittedBody& b : expandRecord.kept ) + { + served.push_back( b.id ); + } + } + const UnservedDefs un = unservedDefsOf( ing, cfg.expand.empty() ? outlineNodes : expandNodes, std::move( served ), + cfg.expand.empty() ? "--outline" : "--expand", mapRootArg ); + if( un.requested > 0 ) + { + mapTopK = 0; + ctxLeanAttr = " topk_default=\"0\"" + nextAttrXml( leanMapPointer( cfg ), "map_next" ); + ctxUnservedAttr = un.attrs; + } + } + // The --expand fallback: estimateExpandBodyTokens is no longer the primary estimate (measured bytes are), // but it remains the honest answer on the degrade path — better than dropping the block out of the // charge entirely, which is the defect this whole item is about. @@ -2378,7 +2539,7 @@ int runDefaultMap( const MainDispatch& d ) // the INITIAL value so it survives even when expandAutoServeScope below does not run (a range slice, or // a pre-render degrade) — every path exactNameExpandDefault can reach ends up with SOME to // decorate, since hasExtension is provably true whenever --expand is non-empty. - std::string ctxOpenStr = exactNameExpandDefault ? "" : ""; + std::string ctxOpenStr = ""; // R-E fix (2026-08-19): the payload document root DISCLOSES the root its p= are relative to, on the two // shapes where the ride-along map's is NOT there to do it — no map at all (mapTopK==0, which // the exact-name --expand default always picks) and whole-file mode. The first R-E landing made @@ -2448,11 +2609,11 @@ int runDefaultMap( const MainDispatch& d ) // because every byte on that path is raw code text — the same rate and the same pricedRootAttr fixpoint // the whole-file emission below applies. topk_default="0" and the unproven residue ride BOTH openers, so // the first is charged to both documents and the second is passed as the shared root bytes. - const std::size_t topkDefaultBytes = exactNameExpandDefault ? ( sizeof( " topk_default=\"0\"" ) - 1 ) : 0; + const std::size_t topkDefaultBytes = ctxLeanAttr.size(); ExpandServeDocument bundleDoc; bundleDoc.payloadBytes = bodiesSection.xml.size(); bundleDoc.mapBytes = mapTopK > 0 ? measureEmittedMapBytes( mapTopK, payloadTokens ) : 0; - bundleDoc.rootAttrBytes = ctxRootBytesWhenNoMap + topkDefaultBytes + noteBytes; // root=/est_tokens= only where no map carries them; note= only where a map does + bundleDoc.rootAttrBytes = ctxRootBytesWhenNoMap + topkDefaultBytes + ctxUnservedAttr.size() + noteBytes; // root=/est_tokens= only where no map carries them; note= only where a map does ExpandServeDocument fileDoc; fileDoc.payloadBytes = wholeFile.xml.size(); // as EMITTED, not the raw file bytes fileDoc.rootAttrBytes = ctxRootAttr.size() + topkDefaultBytes + ctxNotesDegradedBytes; // whole-file mode always carries root= (no rides with it) and, when degraded, the marker too @@ -2488,9 +2649,9 @@ int runDefaultMap( const MainDispatch& d ) // the ROOT ATTRIBUTES ride too: the compact legend reads root=/est_tokens=/topk_default= etc. present-only, so a // candidate without them would price a legend shorter than the one delivered. The est_tokens= values are the // candidates' own prices (the layer reprices them, and a placeholder of another digit count would move the delta). - const std::string topk = exactNameExpandDefault ? " topk_default=\"0\"" : ""; + const std::string topk = ctxLeanAttr; const std::string fileEst = std::to_string( static_cast( double( wholeFile.xml.size() + kExpandWholeFileLegend.size() + ctxRootAttr.size() ) / rw::kBytesPerTokenBody ) + 1 ); - const std::string bundleRoot = ""; + const std::string bundleRoot = ""; const std::string fileRoot = ""; // in the order the bundle is EMITTED — the bodies first, the ride-along map after them: the layer reads its // head terms off the root's first child, so a map-first candidate priced a different legend (rv-r1-L1-2: 107 B) @@ -2505,13 +2666,14 @@ int runDefaultMap( const MainDispatch& d ) ASSUME( ctxOpenStr.rfind( "" — insert // right after " page from the bundle + // the pack-task row below must keep serving. + { "candidate-page", []( const rw::Config& c ) noexcept { return ( !c.forTask.empty() || c.packTaskFlag ) && c.tokenBudget != 0 && ( c.pageLimit > 0 || c.pageOffset > 0 ); }, CompactKeyMark::RootAttr, "above_cliff" }, { "pack-task", []( const rw::Config& c ) noexcept { return c.packTaskFlag || !c.packTask.empty(); }, CompactKeyMark::RootAttr, "budget_tokens" }, { "from-trace", []( const rw::Config& c ) noexcept { return !c.fromTrace.empty() || !c.runTrace.empty(); }, CompactKeyMark::RootAttr, "task" }, { "expand", []( const rw::Config& c ) noexcept { return !c.expand.empty(); }, CompactKeyMark::None, {} }, @@ -3847,7 +4013,11 @@ static bool nativeCompactLegendVerb( const rw::Config& c ) noexcept // L1 fix round (rv-r1-L1 LOW-1): --batch outranks --for in dispatch, so `--for=X --batch=F` answers the batch envelope — // an answer this layer shapes. Only a run --for actually answers is skipped. const bool batchAnswers = !c.batchFile.empty(); // the batch envelope answers, and this layer shapes it - return !batchAnswers && !c.forTask.empty(); + // the budgeted-bundle CANDIDATE PAGE (#294/PR #362) is NOT the --for bundle this skip protects: + // its legend is pure prose (no data comment), so the layer's rewrite is safe — and required, or + // --legend=compact is accepted and ignored on the page (the #362 review's item 5). + const bool forCandidatePage = !c.forTask.empty() && c.tokenBudget != 0 && ( c.pageLimit > 0 || c.pageOffset > 0 ); + return !batchAnswers && !c.forTask.empty() && !forCandidatePage; } // L1 fix round (rv-r1-L1 LOW-3): a DEFAULTED posture captured every run through a tmpfile, a 1.28 MB `--lint --sarif` @@ -5086,6 +5256,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) DISCLOSE( Diagnostics::answerRefused, "main: over the memory guard's hard limit between workspace roots — exit 5, one stderr line" ); rw::emitTo( stderr, "ripwire: {}\n", rw::memguard::hardStopLine( "ingest of workspace root " + std::to_string( parts.size() ) + " of " + std::to_string( ws.size() ) + " (" + r.label + ")" ) ); + rw::abandonGitWalks(); // the refusal is out: do not wait for the history walks' futures (gitmine.h) return 5; } } @@ -5110,10 +5281,12 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) rw::memguard::answerStops(); // the refusal answers for any stop the ingest recorded DISCLOSE( Diagnostics::answerRefused, "main: over the memory guard's hard limit after the ingest — exit 5, one stderr line" ); rw::emitTo( stderr, "ripwire: {}\n", rw::memguard::hardStopLine( "ingest" ) ); + rw::abandonGitWalks(); // the refusal is out: do not wait for the history walks' futures (gitmine.h) return 5; } if( const int rc = memoryStopExit( ing, cfg, verbPrec.winner ); rc != 0 ) { + rw::abandonGitWalks(); // same: historyWalks joins on this return, and the answer it fed is refused return rc; } if( cfg.ignoreTests ) @@ -5182,6 +5355,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) { DISCLOSE( Diagnostics::answerRefused, "main: over the memory guard's hard limit after the graph build — exit 5, one stderr line" ); rw::emitTo( stderr, "ripwire: {}\n", rw::memguard::hardStopLine( "graph build" ) ); + rw::abandonGitWalks(); // the refusal is out: do not wait for the history walks' futures (gitmine.h) return 5; } @@ -5252,6 +5426,23 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) // sets concatenate (commits are disjoint across repos) and churn accumulates per file. ASSUME( historyWalks.size() == ( multiRoot ? ws.size() : 1u ), "this block runs under startHistoryWalks' condition (--help-task has already answered), so every root's walk was started" ); + // K51: a walk git could not finish arrives flagged (it is never cached either); the document says so (historyread.h). + HistoryReadReport& historyReport = historyRead(); + const auto joinWalk = [ & ]( std::size_t r ) + { + quality::HistoryWalk walk = historyWalks[ r ].get(); + if( walk.raw.unread && walk.raw.notRun ) + { + DISCLOSE( historyReport, HistoryReadReport::DisclosureWhy::WalkNotRun, + "main: git did not run for the history walk — churn= is absent and amp= counts callers only, and the document says so" ); + } + else if( walk.raw.unread ) + { + DISCLOSE( historyReport, HistoryReadReport::DisclosureWhy::WalkStopped, + "main: the git history walk stopped part-way — churn= and amp= count only the commits read (floors), and the document says so" ); + } + return walk; + }; std::vector> commits; if( multiRoot ) { @@ -5261,7 +5452,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) // Y2: the memoized form — skips the 431 ms `git log --name-only` walk on a // warm (repo, HEAD sha, window, boundary-sha) hit; see quality.h's qchurn family. std::vector> part = - quality::resolveHistoryWalk( historyWalks[ r ].get(), ing, + quality::resolveHistoryWalk( joinWalk( r ), ing, cfg.forTask.empty() ? 0u : 12u, cfg.forTask.empty() ? nullptr : &rootChurn, r ); for( std::vector& c : part ) @@ -5284,7 +5475,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) else { // Y2: memoized — see the multi-root branch above. - commits = quality::resolveHistoryWalk( historyWalks[ 0 ].get(), ing, + commits = quality::resolveHistoryWalk( joinWalk( 0 ), ing, cfg.forTask.empty() ? 0u : 12u, cfg.forTask.empty() ? nullptr : &forChurn ); } @@ -5360,6 +5551,9 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) ampPtr, cboPtr, testedPtr, lcom4Ptr, impurePtr, forChurn, redactCounts, redactPtr, notesPtr, notesDegraded, grepPhases.valid ? &grepPhases : nullptr, needsValueUses }; + // Every verb from here on answers from the built graph: its query and its emit are this scope (the self-profile's + // "after the graph" share; the verbs' own scopes nest inside it). + PROFILE_SCOPE_DESCRIBE( "main: query + emit (the verb, after the graph)" ); if( std::optional handled = runForLens( dsp ) ) { return *handled; diff --git a/src/mapinventory.h b/src/mapinventory.h new file mode 100644 index 000000000..b80eee816 --- /dev/null +++ b/src/mapinventory.h @@ -0,0 +1,386 @@ +#pragma once +// mapinventory.h — the default map's INVENTORY TIER (): every indexed file the ranked rows did not show is +// named (code) or counted (tests, docs, examples, config), and the program entry points are named. +// +// WHY. The ranked map prints the top-k symbols (200 by default), which covers a few dozen files of a few hundred. On +// orient questions ("map this codebase: subsystems, key files, entry points") that cut WAS the miss: subsystem files +// ranked just below it (tmux input.c #208, key-bindings.c #202 of 6361) were absent with nothing saying they existed. +// Entry points are structurally worse: `main` has no in-repo caller, so PageRank ranks it at the bottom (htop main +// #2548 of 3206) and no re-rank can surface it without demoting something else. An inventory is complete and +// disclosed where a re-rank would only move the miss. +// +// WHAT. computeMapInventory is the ONE selection both dialects render (serialize.h: the XML element and the +// JSON "inv" object), so they cannot disagree on what was listed. It reads only the index and the call graph; it +// never touches the rank vector or the ranked rows (the tier is appended after them). +// • entries — a def named `main` by each language's OWN entry convention, never by the name alone: +// – a Function `main` with no in-repo caller in a language whose entry IS such a function (C, C++, ObjC, Go, +// Rust, Kotlin, Dart); +// – a Method `main`/`Main` with no in-repo caller only where the entry is a static method (Java, C#); +// – a Function `main` in a module-scope language (Python, JS, TS, Ruby, Bash, PHP, Lua) only when its OWN +// file's module scope calls it and nothing else does (Python's `if __name__ == "__main__": main()`, Bash's +// `main "$@"`, a script's top-level `main()`): an uncalled `main` there is a library function or dead code; +// – a Python `__main__.py` module scope. +// A method named main anywhere else (a Python/JS/Ruby class member) is never an entry. Any other language: no +// claim (a missed entry is disclosed by the legend; a false one would not be). Outside test paths and the +// rolled-up directories below. Not detected (stated in the legend): entries declared in a manifest +// (package.json main/bin/exports, console_scripts, Cargo [[bin]]) or by an attribute (Swift @main, Kotlin +// @JvmStatic) — the index does not read those as entry declarations. +// • ls rows — per directory, the unshown files: code files NAMED (basename), every other file COUNTED. A file under +// a test/bench/fixture/example/doc directory rolls up to that directory (counted, never named): those trees are +// large (textual's docs/examples, tmux's regress) and are not where an orient answer's key files live. +#include +#include +#include +#include +#include + +#include "model.h" +#include "filter.h" // isTestSymbol — the shared test-path convention +#include "resolve.h" // includerDir — the shared directory-of-a-path splitter +#include "infra/namesplit.h" // namesplit::afterLast — the shared basename splitter +#include "nextverb.h" // nextFlag — the one shell-safe next= argv composer + +namespace rw +{ + +struct MapInventoryDir +{ + std::string dir; // root-relative directory ("." for the root); a rolled-up subtree's top dir + std::uint32_t total = 0; // unshown files under it + std::vector named; // the code files among them (file ids, path order) +}; + +struct MapInventory +{ + bool active = false; // false ⇒ the tier is not emitted at all (byte-identical map) + std::size_t listed = 0; // files the ranked rows showed (the map's groups) + std::size_t unlisted = 0; // every other indexed file; listed + unlisted = files= + std::vector entries; // path order, then symbol id; at most kInventoryEntryCap + std::size_t entryTotal = 0; // every entry found (entries.size() < entryTotal ⇒ cut, disclosed) + std::vector dirs; // path order + std::size_t namesTotal = 0; // every unshown code file a directory row could name + bool namesCapped = false; // the name ceiling left some of them counted, not named (disclosed) + // Recovery of a cut (PROCESS rule 5: every cut recoverable via next=): the argv fragment, on the same root, that + // lists what the guard left out. Empty unless that guard fired. + std::string entriesNext; // --graph-query over every def that could be a cut entry (a superset) + std::string namesNext; // --tree over every indexed file (a superset of the count-only names) +}; + +// RUNAWAY GUARDS (PROCESS rule 5a), far above any measured answer: the eleven orient repos of the round-1 table name at +// most 199 files (htop) and list at most 5 entries (gotestsum); golang.org/x/tools, a Go tool monorepo, has 60 real +// entries and golang.org/x/text 31, so the entry cap sits well above both. A monorepo of thousands of unshown code +// files would otherwise print every name (~13 B each). Past the ceiling, directories in path order keep n= and drop f= +// (names_capped="1" names_total=N names_next=); past the entry cap, entries_capped="1" entries_total=N entries_next=. +// Each next= is an argv on the same root that lists what was cut (a superset of it), so neither cut is a dead end. +inline constexpr std::size_t kInventoryNameCeiling = 2000; +inline constexpr std::size_t kInventoryEntryCap = 256; + +// Directory components whose subtree rolls up count-only. pathTierOf's test/bench/fixture conventions plus the +// presentational ones (examples, docs, regress, samples) — local to the inventory on purpose: widening pathTierOf +// itself would re-tier --grep/--callers ordering on every verb. +inline constexpr std::string_view kInventoryRollupDirs[] = { + "test/", "tests/", "__tests__/", "spec/", "specs/", "e2e/", "bench/", "benches/", "benchmark/", "benchmarks/", + "fixture/", "fixtures/", "testdata/", "example/", "examples/", "_example/", "_examples/", "doc/", "docs/", + "regress/", "sample/", "samples/", +}; +// Deliberately NOT rolled up: integration/ and testing/. Both are real package names (hono's src/helper/testing is a +// library helper; an integration/ package is common), so their files stay named; a main under them is still judged +// by the entry rule. e2e/ is an end-to-end test tree by name and rolls up. Go's _examples/ convention (a leading +// underscore keeps it out of the module build) and benchmark/ (grpc-go's benchmark mains) roll up like examples/. + +// The rolled-up directory a root-relative path falls under: the prefix through the FIRST matching component, or empty. +inline std::string_view inventoryRollupDirOf( std::string_view rel ) noexcept +{ + std::size_t best = std::string_view::npos; + std::size_t bestLen = 0; + for( std::string_view seg : kInventoryRollupDirs ) + { + std::size_t pos = 0; + while( ( pos = rel.find( seg, pos ) ) != std::string_view::npos ) + { + if( pos == 0 || rel[ pos - 1 ] == '/' ) + { + if( pos < best ) { best = pos; bestLen = seg.size(); } + break; + } + ++pos; + } + } + if( best == std::string_view::npos ) + { + return {}; + } + return rel.substr( 0, best + bestLen - 1 ); // without the trailing '/' +} + +// A code symbol: anything a reader can open as a definition — not a data Section (doc heading, JSON/YAML key) and +// not the synthetic module-scope row every script file carries. +inline bool isInventoryCodeSymbol( const Symbol& s ) noexcept +{ + return s.kind != SymKind::Section && s.kind != SymKind::ModuleScope; +} + +// How a language spells its program entry (the header's entry rule). EntryFn: a caller-less Function `main` IS the +// entry. EntryStaticMethod: a caller-less Method `main`/`Main` is (Java/C# static main). ModuleScopeCall: a Function +// `main` is an entry only when its own file's module scope calls it. None: no claim. +enum class InventoryEntryStyle : std::uint8_t { None, EntryFn, EntryStaticMethod, ModuleScopeCall }; + +// The languages of each style, as data (a reader checks one list, not a switch). +inline constexpr Lang kInventoryEntryFnLangs[] = { Lang::C, Lang::Cpp, Lang::ObjC, Lang::Go, Lang::Rust, Lang::Kotlin, Lang::Dart }; +inline constexpr Lang kInventoryStaticMethodLangs[] = { Lang::Java, Lang::CSharp }; +inline constexpr Lang kInventoryModuleScopeLangs[] = { Lang::Python, Lang::JavaScript, Lang::TypeScript, Lang::Ruby, Lang::Bash, + Lang::Php, Lang::Lua }; + +inline constexpr InventoryEntryStyle inventoryEntryStyle( Lang l ) noexcept +{ + const auto in = [ l ]( const auto& langs ) noexcept { return std::find( std::begin( langs ), std::end( langs ), l ) != std::end( langs ); }; + return in( kInventoryEntryFnLangs ) ? InventoryEntryStyle::EntryFn + : in( kInventoryStaticMethodLangs ) ? InventoryEntryStyle::EntryStaticMethod + : in( kInventoryModuleScopeLangs ) ? InventoryEntryStyle::ModuleScopeCall + : InventoryEntryStyle::None; +} + +// A def whose name and kind fit its language's entry convention (callers are judged separately). +inline bool isInventoryEntryShape( const Symbol& s ) noexcept +{ + switch( inventoryEntryStyle( s.lang ) ) + { + case InventoryEntryStyle::EntryFn: return s.kind == SymKind::Function && s.name == "main"; + // a script language indexes a class member as a Function with its class as scope (Python `def main(self)`): + // only a TOP-LEVEL main is the script's entry + case InventoryEntryStyle::ModuleScopeCall: return s.kind == SymKind::Function && s.name == "main" && s.scope.empty(); + case InventoryEntryStyle::EntryStaticMethod: return s.kind == SymKind::Method && ( s.name == "main" || s.name == "Main" ); + case InventoryEntryStyle::None: break; + } + return false; +} + +inline bool isInventoryMainModule( const IngestResult& ing, const Symbol& s ) noexcept +{ + return s.kind == SymKind::ModuleScope && s.fileId < ing.files.size() + && namesplit::afterLast( rootRelPath( ing, s.fileId ), "/" ) == "__main__.py"; +} + +// The recovery argv for entries past the cap: a --graph-query naming every def the cut entries could be (each name a +// cut entry carries, plus the __main__.py module scopes when one was cut), --limit sized to every symbol it can match +// so its own window never cuts. A superset — it also lists non-entries of those names (tests, methods, callees). +inline std::string inventoryEntriesNext( const IngestResult& ing, const MapInventory& inv, std::size_t from ) +{ + bool on[ 3 ] = { false, false, false }; // a cut def named main, one named Main, a cut __main__.py scope + for( std::size_t i = from; i < inv.entries.size(); ++i ) + { + const Symbol& s = ing.symbols[ inv.entries[ i ] ]; + const bool scope = s.kind == SymKind::ModuleScope; + on[ 0 ] = on[ 0 ] || ( !scope && s.name == "main" ); + on[ 1 ] = on[ 1 ] || ( !scope && s.name == "Main" ); + on[ 2 ] = on[ 2 ] || scope; + } + static constexpr std::string_view kParts[ 3 ] = { "name(\"main\")", "name(\"Main\")", + "and(file(all,\"(^|/)__main__\\.py$\"),kind(all,modscope))" }; + std::string joined; + int n = 0; + for( int i = 0; i < 3; ++i ) + { + if( on[ i ] ) { joined += n++ ? "," : ""; joined += kParts[ i ]; } + } + std::size_t limit = 0; // every symbol the expression can match, so the query's own window never cuts + for( const Symbol& s : ing.symbols ) + { + limit += ( on[ 0 ] && s.name == "main" ) || ( on[ 1 ] && s.name == "Main" ) || ( on[ 2 ] && isInventoryMainModule( ing, s ) ); + } + return nextFlag( "--graph-query=", n > 1 ? "or(" + joined + ")" : joined ) + " --limit=" + std::to_string( limit ); +} + +// The program entries by each language's convention (the header's rule), outside the rolled-up directories and test +// code; and the __main__.py module scopes. Path order, at most kInventoryEntryCap kept (entryTotal counts them all; +// entriesNext lists the rest). +template +inline void collectInventoryEntries( MapInventory& inv, const IngestResult& ing, const std::vector& outOff, + const std::vector& outTargets, const PathRel& pathRel ) +{ + const std::size_t F = ing.files.size(); + const std::size_t S = ing.symbols.size(); + // test PATHS need no clause here: isTestSymbol (below) covers a candidate def, and every test directory isTestPath + // knows (test/, tests/, __tests__/) is a rolled-up directory, which covers a __main__.py scope + const auto entryEligibleFile = [ & ]( std::uint32_t f ) noexcept { return inventoryRollupDirOf( rootRelPath( ing, f ) ).empty(); }; + std::vector candidate( S, 0 ); + std::vector scopeCalled( S, 0 ); // its own file's module scope calls it + for( NodeId id = 0; id < S; ++id ) + { + const Symbol& s = ing.symbols[ id ]; + candidate[ id ] = isInventoryEntryShape( s ) && s.fileId < F && !isTestSymbol( ing, id ) && entryEligibleFile( s.fileId ); + } + // a candidate with an in-repo caller other than its own file's module scope is a library function, not an entry + for( NodeId caller = 0; caller < S; ++caller ) + { + const Symbol& c = ing.symbols[ caller ]; + for( std::uint32_t e = outOff[ caller ]; e < outOff[ caller + 1 ]; ++e ) + { + const NodeId t = outTargets[ e ]; + if( t >= S || !candidate[ t ] ) + { + continue; + } + if( c.kind == SymKind::ModuleScope && c.fileId == ing.symbols[ t ].fileId ) + { + scopeCalled[ t ] = 1; + } + else + { + candidate[ t ] = 0; + } + } + } + for( NodeId id = 0; id < S; ++id ) + { + const Symbol& s = ing.symbols[ id ]; + // a module-scope language's main is an entry only when that scope runs it; elsewhere the caller-less def is + const bool conventionHolds = inventoryEntryStyle( s.lang ) != InventoryEntryStyle::ModuleScopeCall || scopeCalled[ id ]; + const bool mainModule = isInventoryMainModule( ing, s ) && entryEligibleFile( s.fileId ); + if( ( candidate[ id ] && conventionHolds ) || mainModule ) + { + inv.entries.push_back( id ); + } + } + std::stable_sort( inv.entries.begin(), inv.entries.end(), [ & ]( NodeId a, NodeId b ) + { return pathRel( ing.symbols[ a ].fileId ) < pathRel( ing.symbols[ b ].fileId ); } ); + inv.entryTotal = inv.entries.size(); + if( inv.entries.size() > kInventoryEntryCap ) + { + inv.entriesNext = inventoryEntriesNext( ing, inv, kInventoryEntryCap ); + inv.entries.resize( kInventoryEntryCap ); + } +} + +// One row per directory over the unshown files, path order. A file under a rolled-up directory groups under that +// directory and is counted; elsewhere a code file (codeSyms > 0, not test-named) is named, every other file counted. +template +inline void collectInventoryDirs( MapInventory& inv, const IngestResult& ing, const std::vector& shownFile, + const std::vector& codeSyms, const PathRel& pathRel ) +{ + struct Keyed { std::string key; bool rolled; std::uint32_t f; }; + std::vector keyed; + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + if( shownFile[ f ] ) + { + continue; + } + // classify on the ROOT-RELATIVE path (a checkout living under a tests/ dir is not a test tree — #228), group + // and print in the display form every uses + const std::string_view rel = rootRelPath( ing, f ); + const std::string_view disp = pathRel( f ); + const std::string_view rolled = inventoryRollupDirOf( rel ); + if( rolled.empty() ) + { + const std::string_view dir = includerDir( disp ); // the shared directory splitter; the root's files group under "." + keyed.push_back( Keyed{ std::string( dir.empty() ? std::string_view( "." ) : dir ), false, f } ); + continue; + } + const std::size_t prefix = disp.ends_with( rel ) ? disp.size() - rel.size() : 0; + keyed.push_back( Keyed{ std::string( disp.substr( 0, prefix ) ) + std::string( rolled ), true, f } ); + } + std::stable_sort( keyed.begin(), keyed.end(), [ & ]( const Keyed& a, const Keyed& b ) + { return a.key != b.key ? a.key < b.key : pathRel( a.f ) < pathRel( b.f ); } ); + for( const Keyed& k : keyed ) + { + if( inv.dirs.empty() || inv.dirs.back().dir != k.key ) + { + inv.dirs.push_back( MapInventoryDir{ k.key, 0, {} } ); + } + MapInventoryDir& d = inv.dirs.back(); + ++d.total; + if( !k.rolled && codeSyms[ k.f ] > 0 && !isTestPath( rootRelPath( ing, k.f ) ) ) + { + d.named.push_back( k.f ); + } + } +} + +// The name ceiling: whole directories, path order — a directory is named completely or not at all, so f= is never a +// silent part-list; past the ceiling every later directory is count-only too (one contiguous cut). +inline void applyInventoryNameCeiling( MapInventory& inv ) noexcept +{ + for( const MapInventoryDir& d : inv.dirs ) + { + inv.namesTotal += d.named.size(); + } + std::size_t namedSoFar = 0; + for( MapInventoryDir& d : inv.dirs ) + { + if( namedSoFar + d.named.size() > kInventoryNameCeiling ) + { + inv.namesCapped = inv.namesCapped || !d.named.empty(); + d.named.clear(); + namedSoFar = kInventoryNameCeiling; + continue; + } + namedSoFar += d.named.size(); + } + ENSURES( namedSoFar <= kInventoryNameCeiling, "the printed names stay within the ceiling" ); +} + +// `shownFile[f]` != 0 ⇔ the ranked rows printed a group for file f. `pathRel` maps a file id to its display path +// (the same root-relative form every uses). +template +inline MapInventory computeMapInventory( const IngestResult& ing, const std::vector& outOff, + const std::vector& outTargets, const std::vector& shownFile, + const PathRel& pathRel ) +{ + const std::size_t F = ing.files.size(); + EXPECTS( shownFile.size() == F, "one shown bit per indexed file" ); + EXPECTS( outOff.size() >= ing.symbols.size() + 1 || ing.symbols.empty(), "outOff is the CSR offset array over the symbols" ); + MapInventory inv; + + // code symbols per file: a file with none is counted, never named + std::vector codeSyms( F, 0 ); + for( const Symbol& s : ing.symbols ) + { + if( s.fileId < F && isInventoryCodeSymbol( s ) ) + { + ++codeSyms[ s.fileId ]; + } + } + // Present-only: the tier rides a map whose ranked rows LEFT OUT a file with code in it. A map that shows every + // such file has nothing to inventory (its unshown files are empty or data-only) and stays byte-identical. + bool anyCut = false; + for( std::size_t f = 0; f < F; ++f ) + { + if( !shownFile[ f ] ) { ++inv.unlisted; } else { ++inv.listed; } + anyCut = anyCut || ( !shownFile[ f ] && codeSyms[ f ] > 0 ); + } + if( !anyCut ) + { + return MapInventory{}; + } + inv.active = true; + collectInventoryEntries( inv, ing, outOff, outTargets, pathRel ); + collectInventoryDirs( inv, ing, shownFile, codeSyms, pathRel ); + applyInventoryNameCeiling( inv ); + if( inv.namesCapped ) + { + // --tree lists every indexed file with a symbol, ranked, paging by its own next=; --limit= files= keeps it to + // one page. A superset of the count-only names (it repeats the ranked and the named files). + inv.namesNext = "--tree --limit=" + std::to_string( F ); + } + ENSURES( inv.listed + inv.unlisted == F, "every indexed file is listed or inventoried" ); + return inv; +} + +// The legend clause (full dialect). No double hyphen: it rides inside an XML comment (G4). +inline constexpr std::string_view kMapInventoryLegend = + ""; + +} // namespace rw diff --git a/src/mcp.h b/src/mcp.h index bd1fac0c8..481af44a2 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -31,7 +31,7 @@ #include #include // std::find — the --mcp-tools duplicate check and the batch-served lookup #include // std::popcount — the --mcp-tools profile check -#include "infra/tablelookup.h" // findByField — mcpToolIndex, the same row lookup wrap and ingest use +#include "infra/tablelookup.h" // findIndexByField — mcpToolIndex, the same row lookup wrap and ingest use #include // ::realpath — the workspace-pin canonicalization (mcpCanonRoot) #include // PATH_MAX #include "infra/os.h" // rw::os::getcwd — R2a: the launch-cwd assumed root (resolved once at startup) @@ -141,8 +141,10 @@ inline constexpr McpToolMask kMcpAllToolsMask = ( McpToolMask{ 1 } << kMcpVerbCo // kMcpVerbTable's row for `name`, or kMcpVerbCount when the name is not an advertised tool. constexpr std::size_t mcpToolIndex( std::string_view name ) noexcept { - const McpVerbInfo* const row = findByField( kMcpVerbTable, &McpVerbInfo::name, name ); - return row == nullptr ? kMcpVerbCount : static_cast( row - kMcpVerbTable ); + // The INDEX-shaped lookup, not findByField's row pointer: g++ 15 rejected the pointer form's + // row-address arithmetic in constant evaluation under -fsanitize=address (the static_asserts + // below) — the same GCC class findIndexByField exists for (#347). Same values on every compiler. + return findIndexByField( kMcpVerbTable, &McpVerbInfo::name, name ); } // The named profiles. `members` is a comma list of tool names; "" means every tool. @@ -1374,7 +1376,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo + mcprefuse::toolMetadataFor( "edit_check", pathIsRequired ) + "}," // The cross-branch + dark-content verbs. Read-only git plumbing, no index // coupling for the first two (they read OTHER refs' blobs, which the index never ingested). - + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"whereis\",\"description\":\"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-addressed), so N branches cost about one tree. kind=def on a REF row is a LEXICAL heuristic (ref blobs are raw text, never ingested) — for HEAD's parsed answer use find_symbol/fetch_body. symbol = the name, or an @FILE:LINE seed; kind = optional ref-name substring filter, echoed as filter=; limit/offset page the hits (first 60). at= is HEAD's sha; +dirty = changed paths read from the working tree (worktree=). Single-root; read-only.\"," + + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"whereis\",\"description\":\"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-addressed), so N branches cost about one tree. kind=def is a parser's label on any row: HEAD's from the index, another ref's from parsing its blob; kind=text = a def-like line not parsed. symbol = the name, or an @FILE:LINE seed; kind = optional ref-name substring filter, echoed as filter=; limit/offset page the hits (first 60). at= is HEAD's sha; +dirty = changed paths read from the working tree (worktree=). Single-root; read-only.\"," + mcprefuse::toolMetadataFor( "whereis", pathIsRequired ) + "}," ) + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"stray_content\",\"description\":\"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the work — the case `git cherry` structurally cannot see; merged branches are omitted and counted; v=unknown is a branch this scan could NOT analyse at all (no merge-base, unrelated history), not a fourth kind of divergence. Every file row carries its raw del/redone/sim evidence. Line-granular, not semantic. kind = optional ref-name substring filter, echoed as filter=; limit/offset page the refs. Single-root; read-only.\"," + mcprefuse::toolMetadataFor( "stray_content", pathIsRequired ) + "}," ) + @@ -1498,6 +1500,13 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // whereis arm, a non-string `listing:5` decoded to "" after that gate had run, its shape refusal was dropped, and the // closed-set refusal then told the caller it sent '' (CodeRabbit 5468003465; test/crossrefcheck.sh LEAN (L10b)). const std::string listingArg = strArg( "listing" ); + // whereis: full_text (the CLI --whereis-full-text twin) prints every t= line whole and lifts nothing else. A BOOLEAN, + // read here with the other typed arguments so a wrong-shaped value refuses through the shared gate (checklist 26). + const bool whereisFullText = boolArg( "full_text" ).value; + // grep: code|any (the CLI --grep-in twin). The `listing` sibling (CodeRabbit 5468003465, checklist 26): read inside the + // grep arm, a non-string `in:5` decoded to "" after the shape gate had run, its refusal was dropped, and the arm then + // answered the default tier as if nothing had been sent. test/greptiercheck.sh (9e). + const std::string grepInArg = strArg( "in" ); const std::string from = strArg( "from" ); // path verb: source symbol const std::string to = strArg( "to" ); // path verb: destination symbol const std::string trace = strArg( "trace" ); // L4 from_trace: the raw trace TEXT @@ -2206,7 +2215,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // registers the field and the batch surface already refuses loudly. Both dialects now // read the value through grepInModeFromArg, so they cannot disagree about the set. rw::GrepIn grepInMode = rw::GrepIn::Code; - const std::string inRefusal = rw::grepInModeFromArg( strArg( "in" ), grepInMode ); + const std::string inRefusal = rw::grepInModeFromArg( grepInArg, grepInMode ); if( !inRefusal.empty() ) { resp = errResultMsg( -32602, inRefusal ); @@ -2288,10 +2297,9 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // --token-budget beside --limit — the page has no byte ceiling to shape against. resp = pagedResult( [ & ]( McpPageArgs pg ) { - if( ( pg.limit > 0 || pg.offset > 0 ) && budgetArg.isPresent ) - { - return errResultMsg( -32602, "for: limit/offset select the file page, which has no token budget to shape against — drop budget_tokens, or drop limit/offset for the budgeted bundle" ); - } + // PAGING-POC follow-up (issue #294): budget + window no longer refuses — + // forTaskText serves the budgeted-bundle CANDIDATE PAGE for it (the CLI twin's + // own document); the budgetless FILE PAGE still answers a windowless-budget call. const std::optional answer = forTaskText( path, task, redactPtr, budgetArg.isPresent ? std::size_t( budgetArg.value ) : 0, noRoute, pg, sections ); // L2: "" (default) = stub; the validated closed set otherwise @@ -2307,8 +2315,11 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // D8 fix: legoText now only returns "" on genuine not-found — a resolved type with zero // implementors comes back as real content (implementors="0" + contract), so this message // is no longer conflating two different failures. - const std::string t = legoText( path, type, redactPtr ); - resp = t.empty() ? errResultMsg( -32602, notFoundKind( "type", type ) ) : textResult( t ); + resp = pagedResult( [ & ]( McpPageArgs pg ) // lane lego-transitive: limit/offset window the deeper implementors + { + const std::string t = legoText( path, type, redactPtr, pg ); + return t.empty() ? errResultMsg( -32602, notFoundKind( "type", type ) ) : textResult( t ); + } ); } else if( name == "whereis" && !path.empty() && !symbol.empty() ) { @@ -2323,11 +2334,18 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo resp = !listingRefusal.empty() ? errResultMsg( -32602, listingRefusal ) : pagedResult( [ & ]( McpPageArgs pg ) { bool seedFault = false; - const std::string t = whereisText( path, symbol, kind, crossref::kWhereisHits, pg, &seedFault, listing ); + bool blobFault = false; + const std::string t = whereisText( path, symbol, kind, crossref::kWhereisHits, pg, &seedFault, listing, &blobFault, + whereisFullText ); if( seedFault ) { return errResultMsg( -32602, mcprefuse::notFound( getIndex( path ).ing, "symbol", symbol ) ); } + if( blobFault ) + { + DISCLOSE( Diagnostics::answerRefused, "whereis (MCP): the blob stream did not serve every blob — a -32603 error names it, no hit count" ); + return errResultMsg( -32603, crossref::blobsUnreadCause( crossref::UnreadBlobBatch::Trees ) + " — no hit count rather than one nothing measured" ); + } return t.empty() ? errResult( -32602, "not a git repository (or no HEAD commit) — no refs to search" ) : textResult( t ); } ); } @@ -2336,7 +2354,13 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // `kind` doubles as the optional ref-name substring filter (no dedicated arg needed). resp = pagedResult( [ & ]( McpPageArgs pg ) // M13 { - const std::string t = strayContentText( path, kind, crossref::kStrayFilesPerRef, pg ); + bool blobFault = false; + const std::string t = strayContentText( path, kind, crossref::kStrayFilesPerRef, pg, &blobFault ); + if( blobFault ) + { + DISCLOSE( Diagnostics::answerRefused, "stray_content (MCP): the blob batch did not serve every blob - a -32603 error names it, no verdicts" ); + return errResultMsg( -32603, crossref::blobsUnreadCause( crossref::UnreadBlobBatch::Branches ) ); + } return t.empty() ? errResult( -32602, "not a git repository (or no HEAD commit) — no refs to compare" ) : textResult( t ); } ); } @@ -2566,6 +2590,20 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // `pack_task` is the dispatch-only alias (same branch, not separately advertised in tools/list above). else if( ( name == "explore" || name == "pack_task" ) && !path.empty() && !task.empty() ) { + // PAGING-POC (issue #294, ask (b)): explore's limit/offset — the same bounded pair the + // `for` twin takes (mcpPageArgs → pageWindow), refused-together with partition: a + // partitioned bundle has N+1 slices and no single page to walk. + const McpPageParse explorePage = mcpPageArgs( args ); + if( !explorePage.refusal.empty() ) + { + resp = errResultMsg( -32602, explorePage.refusal ); + } + else if( ( explorePage.page.limit > 0 || explorePage.page.offset > 0 ) && partitionCount > 0 ) + { + resp = errResult( -32602, "partition and limit/offset are mutually exclusive — a partitioned bundle has N+1 slices and no single page to walk" ); + } + else + { // W3FIX H4: `partition` used to be a bare `std::uint32_t( partitionArg )` cast — N3's // radius bug, one field over — so partition:4294967299 ran a 3-way fan-out and // partition:2^32 served a single bundle, both silently. The band is a DECLARED domain now @@ -2576,7 +2614,9 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo static_assert( kMcpRecallTopKMax == 1000, "the top_k refusal names the band 1..1000 in mcprefusal.h's kMcpValueFields and in the " "tools/list memory_recall stanza — move all three together" ); - resp = textResult( packTaskText( path, task, budgetTokens, redactPtr, partitionCount, noRoute ) ); + resp = textResult( packTaskText( path, task, budgetTokens, redactPtr, + PackTaskForm{ partitionCount, noRoute, explorePage.page } ) ); + } } // L4: `from_trace` — maps a pasted stack-trace/sanitizer/compiler-error TEXT onto indexed symbols // (fromTraceBundleText, tracelocus.h) — the SAME assembler --from-trace's CLI path calls. diff --git a/src/mcprefusal.h b/src/mcprefusal.h index 075069dbd..82c6c0127 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -372,6 +372,8 @@ inline constexpr McpValueSpec kMcpValueFields[] = { // answered and given no way to ask for the other one. Advertised as a real boolean, not a string, so a // quoted "true" refuses instead of being guessed at (mcpBoolArg). { "no_route", "a BOOLEAN: true forces plain subtoken+body BM25 (omit it for the default router)", "no_route=true", "boolean" }, + // whereis-lean (review C1): the CLI --whereis-full-text twin, the replay a names. Terse: it rides tools/list. + { "full_text", "a BOOLEAN: true prints every t= line whole", "full_text=true", "boolean" }, // ── the ENVELOPE, outside `params` (§B6 M6/M7) ── // These four were read through the bare findString/findObject path, which collapses "absent" onto // "present but not the shape I read" — so `"method":5` became `-32700 "parse error"` (a JSON that parsed @@ -1039,7 +1041,7 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { { "situational_awareness", "path diff files limit offset" }, { "mentions", "path paths symbol limit offset" }, { "for", "path paths task budget_tokens no_route limit offset sections" }, // L-W: limit/offset = the file page; L2: sections = the CLI --sections= twin - { "lego", "path paths type legend" }, + { "lego", "path paths type limit offset legend" }, { "owners", "path symbol limit offset legend" }, { "fetch_body", "path handle start_line end_line" }, { "batch", "path queries legend" }, @@ -1059,13 +1061,16 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { { "affected", "path paths files legend" }, { "path_between", "path paths from to legend" }, { "connect", "path paths symbols radius legend" }, - { "explore", "path paths task budget_tokens partition legend no_route" }, + // PAGING-POC (issue #294, ask (b)): limit/offset are DECLARED because explore now HONORS them — the + // same file-grain widening page the `for` twin serves (one inputSchema row drives this table, the + // tools/list stanza and mcpw3fixcheck's enumeration together). + { "explore", "path paths task budget_tokens partition legend no_route limit offset" }, { "from_trace", "path paths trace budget_tokens legend" }, // 2026-09-10: limit/offset are DECLARED here because the verb now HONORS them (mcpPageArgs -> the // unflagged-row window in editcheck.h), the same rule the `impact`/`uses` rows above state. They // page the CONTEXT rows only; the flagged callers this verb exists to name are never windowed. { "edit_check", "path paths symbol new_body limit offset legend" }, - { "whereis", "path symbol kind limit offset listing legend" }, + { "whereis", "path symbol kind limit offset listing legend full_text" }, { "stray_content", "path kind limit offset legend" }, { "flags", "path kind symbol limit offset legend" }, { "doc_drift", "path kind limit offset legend" }, diff --git a/src/mcpverbs.h b/src/mcpverbs.h index dfdfa611b..f3e720b51 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -449,9 +449,9 @@ inline std::string analyzeToString( const std::string& root, int topK, bool stab // W2-F: the map's convergence disclosure. The CLI map carries pr_iters= and // this one must too — "the clause landed at 3 of its 5 echo sites" is the // §B4 family, and mcpclidiffcheck is the gate that keeps the two surfaces one. - /*ann=*/rw::MapAnnotations{ .prDisclosure = ix.prDisclosure, .codeFirstRows = ix.isCleanWorkingSet }, + /*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.outNameOnly.empty() ? nullptr : &ix.g.outNameOnly, ix.g.stdMemberDeclinedCalls ); } ); } // `rank_by` verb (lane/t10-mcp-coverage): the MCP twin of --rank-by=pagerank|authority|hub|rrf — the SAME @@ -517,9 +517,9 @@ inline std::string rankByText( const std::string& root, std::string_view mode, i ix.g.bindLabel.empty() ? nullptr : &ix.g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, /*extraPayloadTokens=*/0, - /*ann=*/rw::MapAnnotations{ .rankByLabel = rankByLabel, .prDisclosure = disclosure, .codeFirstRows = rankByLabel == nullptr }, // pagerank: the map scope's code-first pick + /*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.outNameOnly.empty() ? nullptr : &ix.g.outNameOnly, ix.g.stdMemberDeclinedCalls ); } ); } // ─── the cross-branch + dark-content MCP twins (`whereis`, `stray_content`, `flags`) ─── @@ -560,7 +560,8 @@ inline std::string rankByText( const std::string& root, std::string_view mode, i // than answering a question the caller did not ask. inline std::string whereisText( const std::string& root, const std::string& symbol, const std::string& filter, std::size_t maxHits, McpPageArgs page = {}, bool* seedFault = nullptr, - crossref::WhereisListing listing = crossref::WhereisListing::ShorterOfDefsAll ) + crossref::WhereisListing listing = crossref::WhereisListing::ShorterOfDefsAll, + bool* blobFault = nullptr, bool fullText = false ) { std::string sel = symbol; std::string seedSpec; @@ -581,7 +582,13 @@ inline std::string whereisText( const std::string& root, const std::string& symb { return {}; } - res.seedSpec = std::move( seedSpec ); + if( res.blobsUnread ) // the CLI twin's refusal: a hit count over blobs never read is not an answer + { + if( blobFault != nullptr ) { *blobFault = true; } + return {}; + } + res.seedSpec = std::move( seedSpec ); + res.textWhole = fullText; // full_text, the CLI --whereis-full-text: every t= whole, nothing else lifted // The tree zero stays an answer; the near-miss only says WHICH zero it is. Computed only on the zero, // so a real hit list costs nothing and is byte-identical to before. // Review M3/M7, the CLI twin's rule: the dotted note only for a method the index defines; on a zero over a dirty @@ -602,11 +609,12 @@ inline std::string whereisText( const std::string& root, const std::string& symb // M13: --stray-content is in cli.h's honorsPaging set — writeStrayContentPage is the entry point the // CLI already calls with cfg.pageLimit/cfg.pageOffset, so the twin passes the same pair rather than 0,0. inline std::string strayContentText( const std::string& root, const std::string& filter, std::size_t maxFiles, - McpPageArgs page = {} ) + McpPageArgs page = {}, bool* blobFault = nullptr ) { const crossref::StrayResult res = crossref::computeStrayContent( root, filter ); if( !res.ok ) { + if( res.blobsUnread && blobFault != nullptr ) { *blobFault = true; } // K75: the host answers an error naming the cause, not "not a git repository" return {}; } return captureXml( [ & ]( std::FILE* f ) { crossref::writeStrayContentPage( f, res, maxFiles, page.limit, page.offset ); } ); @@ -722,6 +730,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& const HopTestedPartition chTested = computeHopTestedPartition( ing, g, referencingOnly ? calledBy : calls ); 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 { anyVia = anyVia || via; @@ -746,7 +755,8 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& for( std::size_t i = w.begin; i < w.end; ++i ) { if( i != w.begin ) { a += ","; } - a += symObj( ids[i], rowNameOnly( g, chRows.matches, ids[i], callersSide ) ); + // 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 += "]"; return a; @@ -759,6 +769,10 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& const PageWindow pwSecond = pageWindow( calledBy.size(), rowCap, page.offset ); const std::size_t rowTotal = referencingOnly ? calledBy.size() : calls.size(); const bool discloseCap = ( pwPrimary.end - pwPrimary.begin ) < rowTotal; + // CALLSITE-AT: the calledBy window's rows with their call sites — editcheck.h rowCallSites, the CLI --callers pass and path spelling. + const PageWindow& calledByWin = referencingOnly ? pwPrimary : pwSecond; + calledBySites = rowCallSites( ing, chRows.matches, std::span( calledBy ).subspan( calledByWin.begin, calledByWin.end - calledByWin.begin ), + VrRender{ sqSingleRoot, sqRootPrefix } ); char pab[ kPageDisclosureCap ]; std::string out = "{"; @@ -782,6 +796,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + ",\"hop_untested\":" + std::to_string( chTested.untested ) + declinedCallsKeyJson( chRows.declinedCalls ) // the CLI root's declined_calls=, for the direction count= describes + 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) if( !referencingOnly && chRows.bodylessDefs > 0 ) { @@ -815,6 +830,7 @@ 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( 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 // through (the --callees side, find_symbol only). Absent when empty; value_refs is the callers-side count, beside @@ -1002,6 +1018,10 @@ inline std::string grepTierKeys( const GrepTierReport& tier, bool floorAlreadyEm if( std::strcmp( tier.emittedTier, "code" ) != 0 ) { keys += std::string( ",\"tier\":\"" ) + tier.emittedTier + "\""; + if( tier.servedString > 0 ) + { + keys += ",\"string_hits\":" + std::to_string( tier.servedString ); // the CLI grepTierAttrs() twin (idea #5) + } // M17: the CLI grepTierAttrs() twin — same condition, same name. A confidence qualifier is exactly // the class of fact a dialect may not drop (mcpclidiffcheck's LENS2), and an MCP-only agent has no // CLI to re-ask from before trusting the label. @@ -1112,16 +1132,16 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat { const McpIndex& ix = getIndex( root ); const IngestResult& ing = ix.ing; - constexpr int kRowCap = 100; + GrepTierReport tierReport; // R-H span tiers: the SAME filter, in the SAME position (after collection), as the CLI verb applies — // search.h owns the policy precisely so these two surfaces cannot answer differently. `grepInMode` is // the MCP `in` argument (the CLI --grep-in twin): the escape hatch has to exist here too, because an // MCP-only agent that reads suppressed_comment= has no CLI to re-ask from. Counters ride the payload // below under the CLI's own key names. - GrepTierReport tierReport; const GrepCollection collected = grepApplySpanTiers( ing, grepCollect( ing, pattern, /*regex=*/false, /*noPrefilter=*/false ), - grepInMode, tierReport ); - const PageWindow grepPage = pageWindow( collected.raw.size(), effectiveRowCap( page.limit, kRowCap ), page.offset ); + grepInMode, tierReport, /*useMemo=*/true, pattern ); + const int grepRowCap = tierReport.servedString > 0 ? kGrepLiftRowCap : 100; // the CLI's default window (idea #5; ranked lifts only) + const PageWindow grepPage = pageWindow( collected.raw.size(), effectiveRowCap( page.limit, grepRowCap ), page.offset ); const std::size_t rowCount = grepPage.end - grepPage.begin; const std::vector hits = grepEnrich( ing, std::span( collected.raw ).subspan( grepPage.begin, rowCount ), 0, 0 ); @@ -1254,7 +1274,7 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat // block below is: existing key-order-sensitive gates read up through "hits" first. // H4: the SAME window the indexed list obeyed, over this list's own length (the CLI emitter's auxPage // twin) — `limit` reached only the hits array before, so a three-row page shipped 29 unindexed rows. - out += grepAuxJson( aux.hits, pageWindow( aux.hits.size(), effectiveRowCap( page.limit, kRowCap ), page.offset ), + out += grepAuxJson( aux.hits, pageWindow( aux.hits.size(), effectiveRowCap( page.limit, grepRowCap ), page.offset ), 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. @@ -2068,41 +2088,38 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr // late-insertion technique for kOverCeilingLegend) knows whether to add it. bool mcpSectionsWillStub = false; { - const bool mcpWantLego = sectionsWant( sections, "lego" ); - const bool mcpWantCompose = sectionsWant( sections, "compose" ); - const bool mcpLegoCandidate = !legoStr.empty() && !mcpWantLego; - const bool mcpComposeCandidate = !composeStr.empty() && !mcpWantCompose; - if( mcpLegoCandidate || mcpComposeCandidate ) - { - std::string sectionsNextMcp = nextFlag( "--for=", task ); - sectionsNextMcp += ' '; - sectionsNextMcp += nextFlag( "--sections=", "lego,compose" ); - const SectionStubPricing legoPricing = mcpLegoCandidate - ? priceSectionStub( "lego", legoPreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, legoStr.size() ) - : SectionStubPricing{}; - const SectionStubPricing composePricing = mcpComposeCandidate - ? priceSectionStub( "compose", composePreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, composeStr.size() ) - : SectionStubPricing{}; - const bool mcpLegoWillStub = mcpLegoCandidate && legoPricing.collapse; - const bool mcpComposeWillStub = mcpComposeCandidate && composePricing.collapse; - // postcondition of the rule itself, checked BEFORE the swap below discards the original size: - // whichever section actually collapses is, by construction, smaller than what it replaced. - ENSURES( !mcpLegoWillStub || legoPricing.stubXml.size() < legoStr.size(), - "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); - ENSURES( !mcpComposeWillStub || composePricing.stubXml.size() < composeStr.size(), - "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); - if( mcpLegoWillStub ) - { - legoStr = legoPricing.stubXml; - } - if( mcpComposeWillStub ) - { - composeStr = composePricing.stubXml; - } - // R2-L2p: same truth table as the CLI's `if( legoWillStub || composeWillStub ) sectionsStubNote = …` - // (verbs_for.h ~2858): the legend rides iff at least one section actually collapsed. - mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; - } + // 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; } 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 ); @@ -2145,6 +2162,14 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr return McpForAssembly{ std::move( out ), mcpSigsCut }; } +// #362 review round 4, follow-up (quality bar): the twins' B2 rank-flags fragment, named once - +// the inline conditional appeared in both twins' page calls and the second read pushed +// packTaskText past its complexity bar. +inline std::string_view mcpPageRankFlags( bool noRoute ) +{ + return noRoute ? std::string_view( " --no-route" ) : std::string_view(); +} + inline std::optional forTaskText( const std::string& root, const std::string& task, RedactCounts* redact = nullptr, std::size_t budgetTokens = 0, bool noRoute = false, McpPageArgs page = {}, // L-W: limit/offset select the FILE PAGE (forpage.h), the CLI --for --limit twin @@ -2299,6 +2324,25 @@ inline std::optional forTaskText( const std::string& root, const st const std::string_view mcpRootArg = ing.realPaths.empty() ? std::string_view( root ) : std::string_view(); if( page.limit > 0 || page.offset > 0 ) { + // PAGING-POC follow-up (issue #294): budget + window is NOT this file page — it is the + // BUDGETED-BUNDLE CANDIDATE PAGE, the same document the CLI --for serves (forpage.h's + // forCandidatePageDoc, one implementation for every dialect). Serving the budgetless file + // page here would ignore budget_tokens — the accept-and-ignore class (mcpverbscheck 6e). + if( budgetTokens > 0 ) + { + const AdaptiveCut cut = adaptiveCut( lensRank, 5, std::size_t( forTopN ), /*scanFullDistribution=*/true ); + // #362 review round 4 (B1): the twins' inline pipeline always scored the full distribution + // (pruneTopK=0) and the CLI pages now force fullDistribution under a window, so the five page + // paths rank ONE list — the page carries the same pasteable CLI next= as the CLI pages, with + // no_route echoed (B2) so the pasted continuation re-ranks identically. The compact dialect + // emitted in-doc (schema= present) so the ref posture reduces it like any other spec-keyed + // answer — the outer layer sees AlreadyCompact and passes it through. + return forCandidatePageDoc( ing, lensRank, cut, ForCandidatePageReq{ task, "--for=", + mcpPageRankFlags( noRoute ), + /*pasteHandle=*/true, /*compactLegend=*/true, + routeNoteOf( rc, shape, noRoute ), mcpRootArg, + redact, gitstamp::stampAt( root ), budgetTokens, page.limit, page.offset } ); + } const ForFilePage filePage = computeForFilePage( ing, lensRank, mcpEvidence ); // PR #215 review item 4: this page composed "routed: " + rc.reason by hand and so answered in a spelling // row 6 retired everywhere else — a parity break with the CLI page AND with this server's own bundle two @@ -2528,10 +2572,17 @@ inline std::optional forTaskText( const std::string& root, const st const std::size_t mcpAtLegendExemptBytes = !flRootArg.empty() && !mcpForAtAttrStr.empty() ? rw::kForAtStampProse.size() : 0; // r2-LO, above // e=: a disclosure on the same contract (the CLI twin exempts the same clause) const std::size_t mcpEndLineExemptBytes = mcpEndLineLegend.size(); + // lego-charge: / cost what they are SERVED as (the CLI twin's rule, rw::forSectionsSigChargePlan): + // a collapsing section its stub; under an explicit budget_tokens, a section `sections` opts into its full render. + const ForStubbableSection mcpLegoChargeIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = true, + .renderedBytes = legoStr.size() }; + const ForStubbableSection mcpComposeChargeIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = true, + .renderedBytes = composeStr.size() }; + const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, budgetTokens > 0, task, mcpLegoChargeIn, mcpComposeChargeIn ); const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes - mcpEndLineExemptBytes - rw::forZeroNoteBytes( headerStr ) // lean-answers: the zero reading never costs a row - + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" + + mcpSigChargePlan.servedBytes( mcpLegoChargeIn, mcpComposeChargeIn ) + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" // knob-honesty-068: the CLI twin's continuation (serialize.h SigsCutContinuation). This surface is signatures-only, // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO CLI argv here — an @@ -2607,7 +2658,7 @@ inline std::optional forTaskText( const std::string& root, const st // check and returned "" for BOTH cases, which is exactly the conflated message the caller downstream had // to guess at ("type not found or has no implementors"); removing that check lets the two cases produce // genuinely different, unambiguous outcomes. -inline std::string legoText( const std::string& root, const std::string& type, RedactCounts* redact ) +inline std::string legoText( const std::string& root, const std::string& type, RedactCounts* redact, McpPageArgs page = {} ) { const McpIndex& ix = getIndex( root ); const IngestResult& ing = ix.ing; @@ -2626,12 +2677,14 @@ inline std::string legoText( const std::string& root, const std::string& type, R // H5: the same legend the CLI --lego prints, and (issue #66) the same adjacent clause defining the // graph_unindexed= the root below carries — CLI and MCP are one wording by construction. H1: the unproven_defs= // clause rides as its own comment beside the closed literal, exactly as on the CLI. - rw::emitTo( mem, "{}{}{}{}", kLegoLegend, graphUnindexedLegendComment( rw::graphGaugeClauses( ix.g ) ).c_str(), + rw::emitTo( mem, "{}{}{}{}{}", kLegoLegend, graphUnindexedLegendComment( rw::graphGaugeClauses( ix.g ) ).c_str(), unprovenDefsVerbComment( UnprovenDefsVerb::Lego, unprovenDefs > 0, "", kImpactLegendOpen, kPageRaiseCapClause, reach.empty() ? "" : kImpactDepthLegend, // 0.6.5: exactly when d=/by_depth= ride, as on the CLI @@ -2940,10 +2996,11 @@ inline std::optional impactText( const std::string& root, const std 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 - declinedCallsLegendWithGate( declinedCalls > 0, g.gateDeclinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI + declinedCallsLegendWithGates( declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI 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 ) ).c_str(), // ... and the depth disclosure, 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 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, @@ -2966,6 +3023,7 @@ inline std::optional impactText( const std::string& root, const std 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) + 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 ] ); @@ -4345,9 +4403,24 @@ inline std::pair qualityBaselineJson( const std::strin // what it is FOR; a separate verb would duplicate the whole task/budget contract for one integer. 0 (or any // value outside 2..16, which is silently clamped OFF rather than erroring an otherwise valid explore call) // ⇒ the plain single-bundle form, byte-identical to before. +// (#362 review, item 2): the form options in ONE struct — packTaskText's parameter list was the +// quality gate's finding (6 -> 8 as the paging round grew it; the form bundles the family: the +// partition fan-out, the route posture, and the page window, all of them optional knobs of the +// same bundle). Aggregate-init order is the declaration order. +struct PackTaskForm +{ + std::uint32_t partitionCount = 0; // --partition=N over MCP (0 = one un-split bundle) + bool noRoute = false; // the CLI --no-route twin + McpPageArgs page; // limit/offset: the candidate page under a budget, the file page without +}; + inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, - RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noRoute = false ) + RedactCounts* redact = nullptr, const PackTaskForm& form = {} ) { + const std::uint32_t partitionCount = form.partitionCount; + const bool noRoute = form.noRoute; + const int pageLimit = form.page.limit; + const int pageOffset = form.page.offset; const McpIndex& ix = getIndex( root ); const IngestResult& ing = ix.ing; const Graph& g = ix.g; @@ -4441,6 +4514,32 @@ inline std::string packTaskText( const std::string& root, const std::string& tas const notes::NoteIndex noteIndex = notes::loadNoteIndex( root ); const notes::NoteIndex* const notesPtr = noteIndex.empty() ? nullptr : ¬eIndex; + // PAGING-POC (#294 + the #362 review round, item 6): the page branch sits AFTER every ranking + // boost (the early position returned before applyDocMentionBoost above, and the page ranked a + // different candidate set than the un-paged answer this same function serves — total= 1155 vs + // 901 on the same task; the parity arm in test/mcpverbscheck.sh (6e) pins the two together). + // budget + window is the CANDIDATE PAGE; a window with no budget is the FILE-GRAIN WIDENING + // PAGE (computeForFilePage/renderForFilePageXml, forpage.h) — the for twin's own page, byte-identical. + if( pageLimit > 0 || pageOffset > 0 ) + { + const std::string_view mcpRootArg = ing.realPaths.empty() ? std::string_view( root ) : std::string_view(); + if( budgetTokens > 0 ) + { + const AdaptiveCut cut = adaptiveCut( lr.rank, 5, std::size_t( kForLensDefaultTopN ), /*scanFullDistribution=*/true ); + // #362 review round 4 (B1/B2): one list across all five page paths — the same pasteable + // next= as the CLI pages, no_route echoed so the pasted continuation re-ranks identically. + return forCandidatePageDoc( ing, lr.rank, cut, ForCandidatePageReq{ task, "--pack-task=", + mcpPageRankFlags( noRoute ), + /*pasteHandle=*/true, /*compactLegend=*/true, + lr.routeNote, mcpRootArg, + redact, gitstamp::stampAt( root ), budgetTokens, pageLimit, pageOffset } ); + } + const ForFilePage filePage = computeForFilePage( ing, lr.rank, lr.evidence ); + const std::string pageRootOpen = ctxRootOpen( task, lr.routeNote, mcpRootArg ); + return renderForFilePageXml( ing, filePage, ForPageRenderParts{ task, pageRootOpen, forCoveragePct( lr.evidence, topLensId( lr.rank ) ), + pageLimit, pageOffset, mcpRootArg, /*compactLegend=*/false } ); + } + PackTaskInputs in; in.budgetTokens = budgetTokens; in.fanIn = &fanIn; @@ -4563,7 +4662,7 @@ inline EditCheckReply editCheckText( const std::string& root, const std::string& } return EditCheckReply{ editCheckBundleText( ing, g, root, kDefaultMaxFileBytes, {}, groups[0].lowestNode, - /*ni=*/nullptr, /*preview=*/false, pg.limit, pg.offset, unprovenDefs ), {} }; + /*ni=*/nullptr, /*preview=*/false, EditCheckPage{ pg.limit, pg.offset }, unprovenDefs ), {} }; } // ─── `slice` verb (lane/tc-sliceat): the ARISE def-use slice over MCP, mirroring the CLI --slice ──────── @@ -5278,7 +5377,7 @@ inline std::string batchPageRefusal( std::string_view verb, const McpPageParse& { return {}; } - return ( verb == "grep" || verb == "impact" || verb == "uses" ) ? page.refusal : std::string{}; // LB-G: uses pages too + return ( verb == "grep" || verb == "impact" || verb == "uses" || verb == "lego" ) ? page.refusal : std::string{}; // LB-G: uses pages too; lego (deeper rows) too } // The served set as ONE list (registry + the two aliases), for the membership test, the near-miss pool and @@ -5666,7 +5765,7 @@ inline BatchSub runBatchSub( const std::string& root, const std::string& obj, in { return bad( missingField( "lego" ) ); } - r.payload = legoText( root, type, redactPtr ); // D8 fix: "" now means genuine not-found only (zero implementors is real content) + r.payload = legoText( root, type, redactPtr, pageParse.page ); // D8 fix: "" now means genuine not-found only; paged like the CLI if( r.payload.empty() ) { return bad( mcprefuse::notFound( getIndex( root ).ing, "type", type ) ); @@ -5863,7 +5962,8 @@ inline bool mcpVerbDeclaresLegend( std::string_view verb ) noexcept inline std::string_view mcpCompactLegendHint( std::string_view verb ) noexcept { if( verb == "analyze" || verb == "rank_by" ) { return "map"; } // lane/t10-mcp-coverage: rank_by's XML root is the SAME shape analyze serves - if( verb == "explore" || verb == "pack_task" ) { return "pack-task"; } + if( verb == "explore" || verb == "pack_task" ) { return "pack-task"; } // their CANDIDATE PAGE (root ) adjusts to candidate-page inside the layer + if( verb == "for" ) { return "candidate-page"; } // the for twin's candidate page (root ); the un-paged answer has no spec row here and passes through, as it did under the empty hint if( verb == "from_trace" ) { return "from-trace"; } if( verb == "lego" ) { return "lego"; } if( verb == "exemplar" ) { return "exemplar"; } diff --git a/src/model.h b/src/model.h index 8a94f8948..b885e8b7d 100644 --- a/src/model.h +++ b/src/model.h @@ -94,7 +94,9 @@ inline const char* symTag( SymKind k ) noexcept case SymKind::Struct: return "struct"; case SymKind::Interface: return "iface"; case SymKind::Var: return "var"; - case SymKind::Section: return "sec"; // markdown heading (doc structure; isolated in the graph) + case SymKind::Section: return "sec"; // data-kind defs: doc headings, JSON/YAML keys, and Ruby db/schema.rb + // columns — the Ruby ones are definitions only, NO call edges + // (graph.h buildGraph's byName skips them; see isMeasurableKind) case SymKind::Macro: return "macro"; // #define (disclosed-degraded: replacement text, not a parsed body) case SymKind::Field: return "field"; // member variable (id=path::Owner::field; use-sites via --uses=Owner.field) case SymKind::Other: return "other"; @@ -743,6 +745,9 @@ struct LocalNameFact }; // An unresolved reference (call/usage). Resolved into an edge in the graph stage. +// The prefix of a C++ call's memberPath that marks an ELEMENT-ACCESS receiver (Reference::memberPath, its C++ reading). +inline constexpr std::string_view kCppElementMark = "[]"; + struct Reference { NodeId fromSymbol = kNoNode; // enclosing symbol (caller); kNoNode if file-scope @@ -802,6 +807,12 @@ struct Reference // memberCtor is the class a CONSTRUCTED receiver names, final segment — "Reply" for `new Reply( r ).send()`, Go // "Merger" for `(&Merger{}).Len()` — with memberRoot "" (a construction has no root identifier). Both "" for every // other shape and every language that records no member-call shape. + // C++ (parser version 166): memberCall stays false, and a member call whose receiver is ONE element access of a one-hop + // container (`m[ k ].empty()`, `v.at( i ).size()`, `rule->bindings_[ k ].empty()`) carries kCppElementMark + the container's + // shape here ("m", "this.items_", "rule.bindings_"; ingest_binds.h cppElementContainer) — read only by graph.h's + // StdMemberGate receiver exception (stdReceiverOf); chainOf reads memberPath only beside memberCall. A C/C++ type alias + // record and a composeRel "elem" field record carry the std container's whole written text here (ingest_relations.h + // stdElementContainerText). std::string memberPath; std::string memberCtor; // FE-B on #373: a Ruby receiver the code BUILDS (`User.new`, a finder, a FactoryBot build, a let or local holding one) @@ -809,6 +820,13 @@ struct Reference // memberFactory), memberVia = the method that built it. recvVar is then empty. Read through rubyTypedRecvOf. std::string memberVia; bool memberFactory = false; + // lane lego-transitive fix 1 (test/legotransitivecheck.sh NS arms), an INHERIT ref only (isInherit): the same-file declaration + // the base name binds to by the language's lexical lookup, as 1 + the byte its name starts at (ingest_relations.h + // localBaseBinding; 0 = no enclosing scope declares it, 0xFFFFFFFF = a scope too large to read, nothing proven). On an + // inherit ref memberRoot holds the base's WRITTEN qualifier (`Outer` for `Outer.Changed`, `b` for `b::Mid`, "" bare) — + // memberCall is false there, so no member-call reader sees it. graph.h's implementors builder reads both (baseEvidence). + // Sits in memberFactory's alignment padding: sizeof(Reference) is unchanged. + std::uint32_t baseBind = 0; }; // A Ruby call whose RECEIVER the code builds (parser version 132, test/rubytypedrecvcheck.sh): `c = Client.new` then `c.get`, diff --git a/src/pageview.h b/src/pageview.h index 53554454a..50df4eee3 100644 --- a/src/pageview.h +++ b/src/pageview.h @@ -168,6 +168,12 @@ inline constexpr int kUseSiteRowCap = 100; // — the two surfaces disagreeing about the size of one answer. Named here for the same reason as the two // above: a cap that lives at its call site is a cap the sibling surface forgets to have. inline constexpr int kCochangePartnerCap = 30; +// CALLSITE-AT (2026-10-09): the call sites ONE caller row lists in sites_at= (the callers verb, safe-delete, and the MCP +// calledBy rows). A runaway guard, not a size target: over the 60 most-called functions of a C repo (2,939 caller rows) +// the per-row count is p50 1 / p90 3 / p99 16, and the rows above it are test files calling one helper dozens of times +// at file scope (216 at most). A cut row says so with sites_total= (the uncut count), and the uses verb on the same +// selector — the callers root's next= — lists every site. Not raised by --limit, which sizes the ROW window. +inline constexpr std::size_t kCallSitesPerRowCap = 16; // ── LB-H (r10 GitNexus round) — the display cap on --impact's SECONDARY import tier ────────────────────── // Deliberately the SAME 40 as the symbol rows above rather than a third number: it counts a comparable diff --git a/src/pathgaps.h b/src/pathgaps.h index 9b66f209c..5c682abb9 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -326,7 +326,7 @@ inline PathGapsXml pathUnreachedXml( const IngestResult& ing, const PathSearchGa return out; } const std::size_t shown = std::min( gaps.syms.size(), kPathGapRows ); - std::string next = "--expand="; + std::string nextSelectors; // the comma-joined file:name list; nextFlag quotes it when a shell would split or expand it for( std::size_t i = 0; i < shown; ++i ) { const Symbol& s = ing.symbols[ gaps.syms[i].id ]; @@ -334,12 +334,12 @@ inline PathGapsXml pathUnreachedXml( const IngestResult& ing, const PathSearchGa out.rows += ""; // file:name, the selector spelling --expand resolves to this one definition (the same p= the row prints) - next += ( i == 0 ? "" : "," ) + std::string( rp ) + ":" + s.name; + nextSelectors += ( i == 0 ? "" : "," ) + std::string( rp ) + ":" + s.name; } out.rootAttrs = " searched=\"" + std::to_string( gaps.searched ) + "\" gaps=\"" + pathGapCountsValue( gaps.totals ) + "\" gap_syms=\"" + std::to_string( gaps.syms.size() ) + "\"" + ( gaps.syms.size() > shown ? " gap_syms_capped=\"1\"" : "" ) + " hint=\"no path through resolved call edges, but the search is incomplete: it met calls or hand-offs it could not follow in full (gaps=) — read the gap rows, or try " - + ex( say.connect ) + " (undirected: finds a shared caller)" + std::string( say.tail ) + "\"" + nextAttrXml( next ); + + ex( say.connect ) + " (undirected: finds a shared caller)" + std::string( say.tail ) + "\"" + nextAttrXml( nextFlag( "--expand=", nextSelectors ) ); return out; } diff --git a/src/quality.h b/src/quality.h index 3b8b98450..21ba0e1fd 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1227,23 +1227,9 @@ inline std::vector> pythonDispatchRequests( inline std::vector pythonDispatchClasses( const Graph& g, NodeId base ) { std::vector classes{ base }; - HashMap visited; - visited.reserve( 32 ); - visited.emplace( base, true ); - for( std::size_t n = 0; n < classes.size(); ++n ) + for( const ImplementorRow& row : implementorClosure( g.implementors, base ) ) // cycles and diamonds visit a class once { - const NodeId id = classes[n]; - if( id >= g.implementors.size() ) - { - continue; - } - for( NodeId child : g.implementors[id] ) - { - if( visited.emplace( child, true ).second ) - { - classes.push_back( child ); // cycles and diamonds visit a class only once - } - } + classes.push_back( row.id ); } return classes; } @@ -2280,8 +2266,18 @@ 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 = 29; // MUST equal ingest.cpp's kCacheVersion (gated); 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 156; // MUST equal ingest.cpp's kParserVer (gated) +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 = 167; // MUST equal ingest.cpp's kParserVer (gated) + // 167 = 2026-10-11 (train 27 release: lane fix27-cpp-subscript's 166 renumbered above the lane build; + // see the kParserVer note) + // 164 = 2026-10-11 (train 27: lego-transitive's 162 renumbered above every live build, up to 162 / rich tag 163; + // see the kParserVer note) + // 162 = 2026-10-10 lane lego-transitive fix 1 (inherit-ref base binding; above 160's + // rich tag 161; the train renumbers) + // 160 = 2026-10-10 lane lego-transitive (JS/TS class factory; above 156's + // rich tag 157 and train 26c's 158/159; the train renumbers) + // 158 = 2026-10-09 (train 26c: PR #339 Rails schema columns; above 156 / rich tag 157; see the + // kParserVer note) // 156 = 2026-10-08 (train 26b: above every lane build, up to 154 / rich tag 155; see the // kParserVer note) // 148 = 2026-10-04 (train 26a: PR #373 merged; above the branch's 145 and the 145–147 @@ -2659,11 +2655,18 @@ inline std::string headSnapExclHex( const std::vector& excludes, st // only WRITTEN into its shard the next time it is (re)computed. A blob absent from both locations is a clean // miss, created fresh, in its shard. `evictOldCacheFamily` below sweeps both layouts, so an old flat blob // still ages out on schedule even if its key is never rewritten. +// A 64-bit key spelled as its 16 lowercase hex digits — the ack ledger's key (ackMapKey), the shard below, and the +// ingest's per-root sidecar names. +inline std::string hex16( std::uint64_t key ) +{ + char hex[ 20 ]; + rw::formatTo( hex, sizeof( hex ), "{:016x}", static_cast( key ) ); + return hex; +} + inline std::string blobShardHex( std::string_view filename ) { - char hex[ 3 ]; - rw::formatTo( hex, sizeof( hex ), "{:02x}", static_cast( fnv1a64( filename ) & 0xff ) ); - return std::string( hex ); + return hex16( fnv1a64( filename ) ).substr( 14 ); // the low byte, two hex digits: "{:02x}" of ( hash & 0xff ) } inline std::string resolveCacheBlobPath( const std::string& dir, const std::string& filename ) @@ -3515,7 +3518,15 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // shipped): defectSitesOf passes the tree's macro names to the C++ scanner, and the scanners skip more calls (an // argument that may be a macro, a directive inside the call, #if 0, a brace-spelling escape) and guard more // vacuous-assert absences — fewer records, the same blob shape; the producer identity keeps the builds apart. -constexpr std::uint32_t kQSnapCacheScheme = 19; +// v20 (2026-10-11, train 27) — no blob shape change; the dead SET's inputs moved. pythonDispatchedMethodIds walks +// pythonDispatchClasses over g.implementors, and lane lego-transitive changed which Python classes extend which: a base +// clause is read as Python evaluates it (`class A( Outer.B )` extends B only, `Generic[K]` extends Generic and not K, +// `metaclass=` and computed bases name none), so the set of methods an inherited receiver call keeps alive can differ +// for an UNCHANGED sha. kParserVer moved with that extraction (164, its mirror with it), and the producer identity +// 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; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) @@ -4470,7 +4481,11 @@ computeWindowRefBodyHashes( const std::string& root, std::uint32_t days, // would keep serving the old stream to a fixed binary: the exact silent-zero the fix removes, restored by // cache. Bumping the scheme makes every pre-existing blob a clean miss (deserialize checks it), not a wrong // answer. This is the qchurn cache's OWN version — not kParserVer, which keys extraction and is untouched. -constexpr std::uint32_t kQChurnCacheScheme = 2; +// SCHEME 3 (2026-10-10, train 26c, K51): a binary before K51 stored a walk git could not finish (an empty stream, or the +// newest prefix of the window) as if it were the window, and the key cannot tell that blob from a real read. K51 never +// stores one, but a blob already on disk would keep serving its silent zero or prefix, with no history_unread=, until HEAD +// moved. The bump makes every pre-K51 blob a clean miss; the cost is one cold walk per repository. +constexpr std::uint32_t kQChurnCacheScheme = 3; constexpr char kQChurnMagic[4] = { 'Q', 'C', 'H', 'N' }; inline std::string serializeRawCommitStream( const RawCommitStream& raw, const std::string& keyMat ) @@ -4580,7 +4595,7 @@ inline bool deserializeRawCommitStream( const std::string& blob, const std::stri // corrupt/foreign blob — is a clean miss that falls through to a full recompute, never a wrong answer). No git repo // degrades to an empty stream; no resolvable HEAD to the uncached walk (which itself degrades to an empty stream — // gitLogNameOnlyRaw's contract). Thread-safe: every memo it reaches (the HEAD anchor, the toplevel) takes its own -// mutex, and the blob write is an atomic rename. +// mutex, and the blob write is an atomic rename. K51: a walk git could not complete comes back with `unread` set and is never stored. inline RawCommitStream gitRawCommitStreamCached( const std::string& root, const char* coSince ) { if( !hasEnclosingGitRepo( root ) ) @@ -4590,7 +4605,20 @@ inline RawCommitStream gitRawCommitStreamCached( const std::string& root, const const std::string headSha = gitHeadSha( root ); if( headSha.empty() ) { - return gitLogNameOnlyRaw( root, coSince ); + // No resolvable HEAD: an unborn branch has no history to read, and git log's failure there (git's own fatal exit, + // 128) is that fact, not an I/O error — so that walk is "read, empty" (K51 floor: a HEAD that is corrupt rather than + // unborn exits 128 too and reads the same here). Any OTHER failure means git did not answer at all — not on PATH + // (the shell's 127), not executable, no popen — and gitHeadSha's "" was that, not an unborn branch: it stays unread. + RawCommitStream noHead = gitLogNameOnlyRaw( root, coSince ); + if( noHead.unread && noHead.exitCode == 128 ) + { + noHead.unread = false; + } + else if( noHead.unread ) + { + noHead.notRun = true; + } + return noHead; } const std::string repoHex = cacheRootKeyHex( root ); @@ -4609,8 +4637,16 @@ inline RawCommitStream gitRawCommitStreamCached( const std::string& root, const } raw = gitLogNameOnlyRaw( root, coSince ); // cold — the 431 ms walk + if( raw.unread || gitWalksAbandoned() ) + { + // K51: a walk that could not be read is NOT stored. Stored, its empty stream would answer "read, no commits" under this + // (repo, HEAD, window) key for as long as HEAD stayed put; unstored, the next call walks again and the caller discloses. + // The flag rides the returned stream to main.cpp's amp=/churn= block, which DISCLOSEs it into the document (historyread.h). + // An abandoned read (a memory-guard refusal stopped the walk) is a prefix too, even when git exited 0: never cached. + return raw; + } atomicWriteFile( cachePath, serializeRawCommitStream( raw, keyMat ) ); // best-effort; a failed - // write just recomputes next time + // write just recomputes next time return raw; } @@ -6470,9 +6506,7 @@ inline std::optional rescoreAckRecord( const AckRecord& r, std::optional( key ) ); - return kind + " " + hex; + return kind + " " + hex16( key ); } // ─── THE LEGACY-ACK BACKFILL — what a row written before provenance existed CAN still get back ───────── @@ -7474,6 +7508,10 @@ inline std::size_t healDefectSites( Snapshot& base, const IdentityAliases& al ) { std::size_t healed = 0; std::vector addSites; + // ADD, NEVER OVERWRITE, per `to` anchor: two alias sources that converge on one current anchor (a scheme alias and a + // rename alias, CodeRabbit on #383) must not both copy their sites under it — the baseline check above only sees + // anchors the baseline already held, not ones this loop filled. The first alias in `from` order fills; the rest skip. + std::vector filledTo; for( const auto& [ from, to ] : al.toCurrent ) { const auto lo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ from, 0 } ); @@ -7482,6 +7520,15 @@ inline std::size_t healDefectSites( Snapshot& base, const IdentityAliases& al ) { continue; } + if( lo == base.defectSites.end() || lo->anchor != from ) + { + continue; // nothing to copy: an alias with no recorded sites must not claim the anchor + } + if( std::find( filledTo.begin(), filledTo.end(), to ) != filledTo.end() ) + { + continue; + } + filledTo.push_back( to ); for( auto it = lo; it != base.defectSites.end() && it->anchor == from; ++it ) { addSites.push_back( { to, it->site } ); diff --git a/src/search.h b/src/search.h index 4fa3d4654..d2785ea9f 100644 --- a/src/search.h +++ b/src/search.h @@ -2065,7 +2065,8 @@ inline GrepCollection grepApplyBooleanTerms( const IngestResult& ing, GrepCollec // THE COST MODEL (E5, PLAN_WAVE2_REPORTS_2026-08-17/exp-e5.md): tiering needs the hit files PARSED, and // astQuery/astQueryGrouped always parse the whole corpus — the wrong cost model for a verb that has already // narrowed to a handful of files. E5 measured the alternative (~42 ns/byte; 0.217 ms median per file on a -// 2400-file tree) and funded ingest.h's narrow spanTiersOfFiles() entry, which this is the only caller of. +// 2400-file tree) and funded ingest.h's narrow spanTiersOfFiles() entry, which this and grepFirstRefSite (ref=, at most +// kGrepRefSitesScanned candidate sites) are the callers of. // // THE BAIL-OUT (E5 design condition 3) IS A BYTE BUDGET, NOT A TIME BUDGET, and that substitution is the // one place this diverges from E5's wording. E5 asked for "a per-call parse-time budget"; a wall-clock @@ -2093,7 +2094,9 @@ struct GrepTierReport std::uint32_t hitFileCount = 0; // hit files the classification had to cover: tier_files=, the TOTAL beside // tier_parsed= once a budget stops it (the cut says how much it left) std::uint32_t unclassifiedHits = 0; // hits in files past the budget, or with no grammar — NEVER suppressed - const char* emittedTier = "code"; // "code" | "comment" | "string" | "comment+string" — §F4's served tier + const char* emittedTier = "code"; // "code" | "comment" | "string" | "comment+string" | "code+string" — §F4's served tier + std::uint32_t servedString = 0; // a RANKED code+string answer only (GrepStringLift::Ranked): how many of the + // served hits are string-tier (string_hits=); 0 on main's plain literal lift const char* budgetHit = nullptr; // nullptr | "files" | "bytes" — E5's disclosed bail-out bool didRun = false; // false under --grep-in=any: no tier vocabulary may be emitted @@ -2124,6 +2127,50 @@ inline bool isBareWordCodePath( std::string_view p ) return false; } +// idea #5 (round-2 tmux-05, `--grep=split-window`): a literal NO identifier can spell — it holds '-', '/', '.' or a +// space — or a SCREAMING_CASE one (a macro, an env var, a protocol constant) is asked for as TEXT, and the place +// that text lives in source is a string: the command table's `.name = "split-window"`, not the 59 shell lines that +// type the command. Such a literal, or any literal whose code tier is thin (fewer than kGrepLiftCodeFloor hits), +// serves its string tier WITH the code tier. Identifier-shaped literals with a full code tier keep today's answer. +// Fix round 1 (rv-lean-trio item 2): the SHAPE is a text heuristic, so it never outvotes the parser — the lift fires only +// while the literal's CODE hits in program code (grepSourceTierHits: source AND test files; not a doc, not a +// shell/YAML/TOML/JSON file, where a bare word parses as code although it is a command word typed) number under +// kGrepLiftCodeFloor. Test files count because a call there is parsed code, a USE a "list every use" question needs: +// textual `self.app` / `DEFAULT_CSS` (source) and hono `c.json` (~490 code hits, nearly all in tests) keep their code rows; +// tmux `split-window` has 2 in .c files (its other code hits are .sh lines that type it) and still lifts. +// Fix round 2 (rv-lean-trio2 item 1): that ONE count decides the shape lift AND the ranked posture (the order, the +// kGrepLiftRowCap window, string_hits=, ref=) of either lift, and a hit there that no parse classified (past the tier +// budget, or no grammar) counts as code — a stopped classification can never make a code tier read thin. Main's +// literal-question lift on a full count (textual `app.query`: its code hits are test-file uses) is served as main serves +// it: collection order, the 100-row default (GrepStringLift::Served). +inline constexpr std::uint32_t kGrepLiftCodeFloor = 8; +inline constexpr int kGrepLiftRowCap = 10; // a RANKED code+string answer's default window (CLI and MCP alike) + +// grepApplySpanTiers' per-hit tier byte for a hit no parse classified: the first value past the last SpanTier (see there). +inline constexpr std::uint8_t kGrepUnclassifiedTier = std::uint8_t( kSpanTierCount ); + +inline bool grepLiteralWantsStrings( std::string_view p ) +{ + if( p.empty() ) + { + return false; + } + std::size_t letters = 0; + bool lower = false; + for( const char c : p ) + { + if( c == '-' || c == '/' || c == '.' || c == ' ' ) + { + return true; + } + const bool isUpper = c >= 'A' && c <= 'Z'; + const bool isLower = c >= 'a' && c <= 'z'; + letters += ( isUpper || isLower ) ? 1 : 0; + lower = lower || isLower; + } + return letters >= 2 && !lower; // SCREAMING_CASE: two or more letters, none lower-case +} + // The span-tier LIFT (grepApplySpanTiers): does no CLASSIFIED code-tier hit sit in source code, while a // classified string-tier hit sits in source code? "Source code" = filter.h's PathTier::Source and not a bare-word // file above, for BOTH halves. Unclassified hits never vote, the same rule the tier election itself follows. @@ -2155,6 +2202,133 @@ inline bool grepLiteralLiftsStrings( const IngestResult& ing, const std::vector< return sourceString; } +// The order a code+string answer is served in (idea #5): source files whose BASENAME holds the literal first (the +// command's own file), then the rest of source code — files with a code-tier hit before files that hold the literal +// only as a string — then everything else in the collection's order: shell/YAML/TOML/JSON files, tests and docs, which +// are where a literal is TYPED rather than defined. `fileHasCode` (by file id) marks the code-tier holders. A stable partition by +// file, so one file's hits stay contiguous and in line order, and a pure function of the collected set (§A1: it +// runs before the page window, so every page walks the same list). `literal` is empty for a regex. +inline void grepRankLiftedStrings( const IngestResult& ing, std::vector& raw, std::string_view literal, + const std::vector& fileHasCode ) +{ + const std::vector tierOfFile = pathTierIndexOver( ing, raw, []( const GrepRawHit& r ) { return r.fileId; } ); + const auto groupOf = [ & ]( std::uint32_t f ) -> int + { + const bool isSource = f < tierOfFile.size() && tierOfFile[f] == std::uint8_t( PathTier::Source ); + const std::string_view rel = rootRelPath( ing, f ); + if( !isSource || isBareWordCodePath( rel ) ) + { + return 3; + } + const std::size_t slash = rel.rfind( '/' ); + const std::string_view base = slash == std::string_view::npos ? rel : rel.substr( slash + 1 ); + if( !literal.empty() && base.find( literal ) != std::string_view::npos ) + { + return 0; + } + return ( f < fileHasCode.size() && fileHasCode[f] != 0 ) ? 1 : 2; + }; + std::vector groupOfFile( ing.files.size(), 0 ); // one decision per hit FILE, not per comparison + std::uint32_t lastFile = UINT32_MAX; + for( const GrepRawHit& r : raw ) + { + if( r.fileId != lastFile && r.fileId < groupOfFile.size() ) + { + lastFile = r.fileId; + groupOfFile[r.fileId] = std::uint8_t( groupOf( r.fileId ) ); + } + } + const auto groupAt = [ & ]( std::uint32_t f ) { return f < groupOfFile.size() ? groupOfFile[f] : std::uint8_t( 3 ); }; + std::stable_sort( raw.begin(), raw.end(), [ & ]( const GrepRawHit& a, const GrepRawHit& b ) { return groupAt( a.fileId ) < groupAt( b.fileId ); } ); +} + +// idea #5's two votes. `string`: CLASSIFIED string-tier hits in the SAME "source code" grepLiteralLiftsStrings means (review +// item 3 of that lane) — a PathTier::Source file that is not a shell/YAML/TOML/JSON file; a lift needs one (strings only in +// other files, or unclassified hits, never lift). `code` (fix round 1): code-tier hits in program code — source or test +// files, neither a doc nor a shell/YAML/TOML/JSON file; a ranked lift needs fewer than kGrepLiftCodeFloor of them. Fix +// round 2 (rv-lean-trio2 item 1b): an UNCLASSIFIED hit in program code counts here too — past the tier budget it is as +// likely a code hit as any other, and counting it as zero let a stopped classification hide 30 code callers (fxL). +struct GrepSourceTierHits +{ + std::uint32_t code = 0; + std::uint32_t string = 0; +}; + +inline GrepSourceTierHits grepSourceTierHits( const IngestResult& ing, const std::vector& raw, const std::vector& hitTier ) +{ + EXPECTS( hitTier.size() == raw.size(), "one tier byte per raw hit" ); + const std::vector tierOfFile = pathTierIndexOver( ing, raw, []( const GrepRawHit& r ) { return r.fileId; } ); + GrepSourceTierHits counts; + std::uint32_t lastFile = UINT32_MAX; + bool lastProgram = false; // source or test, not a doc nor a bare-word file + bool lastSource = false; // ...and not a test either + for( std::size_t h = 0; h < raw.size(); ++h ) // one path decision per hit FILE (a file's hits are contiguous) + { + const std::uint32_t f = raw[h].fileId; + if( f != lastFile ) + { + lastFile = f; + lastProgram = f < tierOfFile.size() && tierOfFile[f] != std::uint8_t( PathTier::Doc ) && !isBareWordCodePath( rootRelPath( ing, f ) ); + lastSource = lastProgram && tierOfFile[f] == std::uint8_t( PathTier::Source ); + } + const bool codeOrUnclassified = hitTier[h] == std::uint8_t( SpanTier::Code ) || hitTier[h] == kGrepUnclassifiedTier; + counts.code += lastProgram && codeOrUnclassified ? 1u : 0u; + counts.string += lastSource && hitTier[h] == std::uint8_t( SpanTier::String ) ? 1u : 0u; + } + ENSURES( counts.code + counts.string <= raw.size(), "a source-tier count never exceeds the hits it counts" ); + return counts; +} + +// The span-tier election's code+string arm, and the posture it is served in (fix round 2, rv-lean-trio2 item 1: ONE count +// — grepSourceTierHits().code, unclassified hits included — decides the shape lift AND the ranked window). +enum class GrepStringLift : std::uint8_t +{ + None, // the code tier answers alone (today's rule) + Served, // main's literal-question lift on a FULL count: strings beside code, collection order, the default window + Ranked, // either lift on a THIN count: grepRankLiftedStrings' order, kGrepLiftRowCap, string_hits=, ref= +}; + +// The lifts: idea #5's (the literal's SHAPE, or a thin code tier, with a source string to serve) and the literal-question +// lift (grepLiteralLiftsStrings: no classified code hit in source code). Both need a classified code hit; `literal` is +// empty for a regex, which only the second lift can serve. The shape lift needs the thin count; the literal lift fires +// either way (main's tier choice) and is RANKED only on the thin count. +inline GrepStringLift grepStringLiftOf( const IngestResult& ing, const std::vector& raw, const std::vector& hitTier, + std::uint32_t codeHits, std::uint32_t stringHits, std::string_view literal ) +{ + if( codeHits == 0 ) + { + return GrepStringLift::None; + } + const bool literalLift = grepLiteralLiftsStrings( ing, raw, hitTier ); + const bool shapeAsks = !literal.empty() && stringHits > 0 && ( grepLiteralWantsStrings( literal ) || codeHits < kGrepLiftCodeFloor ); + if( !literalLift && !shapeAsks ) + { + return GrepStringLift::None; // no lift can fire: the program-code count is never computed + } + const GrepSourceTierHits src = grepSourceTierHits( ing, raw, hitTier ); + const bool thin = src.code < kGrepLiftCodeFloor; + if( thin && ( literalLift || src.string > 0 ) ) // grepLiteralLiftsStrings is true only beside a source string + { + return GrepStringLift::Ranked; + } + return literalLift ? GrepStringLift::Served : GrepStringLift::None; +} + +// idea #5's rank key: by file id, 1 when the file holds a classified code-tier hit. +inline std::vector grepFilesHoldingCode( const IngestResult& ing, const std::vector& raw, const std::vector& hitTier ) +{ + EXPECTS( hitTier.size() == raw.size(), "one tier byte per raw hit" ); + std::vector fileHasCode( ing.files.size(), 0 ); + for( std::size_t h = 0; h < raw.size(); ++h ) + { + if( hitTier[h] == std::uint8_t( SpanTier::Code ) && raw[h].fileId < fileHasCode.size() ) + { + fileHasCode[raw[h].fileId] = 1; + } + } + return fileHasCode; +} + // Filters `collected.raw` order-preserving (already tier-then-path sorted — filtering never reorders), the // same post-filter shape as grepApplyBooleanTerms above, and runs AFTER it: tiering the survivors of a // boolean query is both cheaper and the only reading that matches what the answer will print. @@ -2164,7 +2338,7 @@ inline bool grepLiteralLiftsStrings( const IngestResult& ing, const std::vector< // full" budget would be cheaper and would reintroduce exactly the bug §A1 was written for — every page a // window into a differently-filtered list. inline GrepCollection grepApplySpanTiers( const IngestResult& ing, GrepCollection collected, GrepIn mode, GrepTierReport& report, - bool useMemo = true ) + bool useMemo = true, std::string_view literal = {} ) { report = GrepTierReport{}; if( mode == GrepIn::Any || collected.raw.empty() ) @@ -2228,7 +2402,7 @@ inline GrepCollection grepApplySpanTiers( const IngestResult& ing, GrepCollectio // the last SpanTier, so an appended tier moves it instead of colliding with it, and the counter grows with the // enum instead of being indexed past its end. serveMask below is a u8 bit per tier, hence the bound. static_assert( kSpanTierCount < 8, "serveMask holds one bit per SpanTier in a std::uint8_t" ); - constexpr std::uint8_t kUnclassifiedTier = std::uint8_t( kSpanTierCount ); + constexpr std::uint8_t kUnclassifiedTier = kGrepUnclassifiedTier; std::vector hitTier( collected.raw.size(), kUnclassifiedTier ); std::uint32_t tierHitCount[kSpanTierCount] = {}; for( std::size_t h = 0; h < collected.raw.size(); ++h ) @@ -2254,8 +2428,10 @@ inline GrepCollection grepApplySpanTiers( const IngestResult& ing, GrepCollectio // collapse costs nothing on the queries that already worked: a comment-only or string-only corpus still // serves the one tier it has. The choice is made over the CLASSIFIED hits only — an unclassified hit // cannot vote for a tier nobody proved it belongs to, and it is emitted either way. - std::uint8_t serveMask = 0; - if( tierHitCount[std::size_t( SpanTier::Code )] > 0 && grepLiteralLiftsStrings( ing, collected.raw, hitTier ) ) + std::uint8_t serveMask = 0; + const GrepStringLift lift = grepStringLiftOf( ing, collected.raw, hitTier, tierHitCount[std::size_t( SpanTier::Code )], + tierHitCount[std::size_t( SpanTier::String )], literal ); + if( lift != GrepStringLift::None ) { // The literal question (comparison table tmux-16 / textual-16, 2026-09-30): every code hit is a USAGE of // the literal — a test/doc file, or a shell/YAML/TOML/JSON file where a bare word parses as code — while @@ -2283,6 +2459,8 @@ inline GrepCollection grepApplySpanTiers( const IngestResult& ing, GrepCollectio return collected; // nothing was classified at all — nothing to hold back, and nothing to disclose } + const bool ranked = lift == GrepStringLift::Ranked; + const std::vector fileHasCode = ranked ? grepFilesHoldingCode( ing, collected.raw, hitTier ) : std::vector(); // idea #5's rank key std::vector kept; kept.reserve( collected.raw.size() ); for( std::size_t h = 0; h < collected.raw.size(); ++h ) @@ -2302,6 +2480,12 @@ inline GrepCollection grepApplySpanTiers( const IngestResult& ing, GrepCollectio } } collected.raw = std::move( kept ); + if( ranked ) // a Served lift keeps main's bytes: no order, no window, no string_hits=, no ref= (all key on servedString) + { + report.servedString = tierHitCount[std::size_t( SpanTier::String )]; + ASSUME( report.servedString > 0, "a ranked lift serves a source string (both lifts need one)" ); + grepRankLiftedStrings( ing, collected.raw, literal, fileHasCode ); + } return collected; } @@ -2406,6 +2590,82 @@ inline std::vector grepEnclosingRows( const IngestResult& ing, const return rows; } +// ─── idea #5: ref= — where a callerless enclosing definition is registered ───────────────────────── +// +// A literal served from the string tier often sits in a TABLE ENTRY (`.name = "split-window"` inside +// `cmd_split_window_entry`), and a table entry has no caller edge: what makes it live is the one place that +// names it (`&cmd_split_window_entry` in `cmd_table[]`). The enc row already hands that grep over as +// floor_next=; this runs it for the answer: the first indexed site that names `name` as a whole word inside an +// enclosing symbol that is neither the definition itself nor another of the same name (an extern declaration), on a +// line after the one that declares that enclosing symbol (inside its body or initializer), in CODE (not a comment or string). +// Bounded: at most kGrepRefSitesScanned candidate sites are enriched. Returns {fileId, line}, or line 0 when none. +inline constexpr std::size_t kGrepRefSitesScanned = 64; + +struct GrepRefSite +{ + std::uint32_t fileId = 0; + std::uint32_t line = 0; +}; + +inline bool grepIsWordAt( std::string_view text, std::size_t at, std::size_t len ) noexcept +{ + const auto isIdent = []( char c ) { return ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ) || c == '_'; }; + return ( at == 0 || !isIdent( text[at - 1] ) ) && ( at + len >= text.size() || !isIdent( text[at + len] ) ); +} + +inline GrepRefSite grepFirstRefSite( const IngestResult& ing, const std::string& name, NodeId defId, bool useMemo = true ) +{ + EXPECTS( !name.empty(), "a ref is looked up for a named definition" ); + const GrepCollection found = grepCollect( ing, name ); + const std::span cand = std::span( found.raw ).first( std::min( found.raw.size(), kGrepRefSitesScanned ) ); + // Fix round 1 (rv-lean-trio item 3): only a CODE-tier occurrence is a registration. A comment (`/* unlike do_thing_entry, + // this one is not registered */`) or a string naming the entry is prose, so the candidates are classified by the same span + // parse the tier election uses (spanTiersOfFiles / spanTierAt), and an unclassified site — no grammar, unparsed — is + // never claimed either: ref= absent is a floor, ref= present is a claim. + std::vector batchIndexOf( ing.files.size(), UINT32_MAX ); + std::vector tierPaths; + for( const GrepRawHit& r : cand ) + { + if( r.fileId < batchIndexOf.size() && batchIndexOf[r.fileId] == UINT32_MAX ) + { + batchIndexOf[r.fileId] = std::uint32_t( tierPaths.size() ); + tierPaths.push_back( diskPath( ing, r.fileId ) ); + } + } + const SpanTierBatch batch = spanTiersOfFiles( std::span( tierPaths ), useMemo ); // <= kGrepRefSitesScanned files: the bound + const auto isCode = [ & ]( const GrepRawHit& r ) -> bool + { + const std::uint32_t index = r.fileId < batchIndexOf.size() ? batchIndexOf[r.fileId] : UINT32_MAX; + return index < batch.perFile.size() && batch.perFile[index].isParsed && spanTierAt( batch.perFile[index], r.byteOffset ) == SpanTier::Code; + }; + const std::vector sites = grepEnrich( ing, cand ); + ASSUME( sites.size() == cand.size(), "grepEnrich returns one row per raw hit, in order" ); + std::size_t occurrence = 0; // which occurrence of `name` on its line raw hit i is (the scanner steps pos+1, as find does below) + for( std::size_t i = 0; i < sites.size(); ++i ) + { + const GrepHit& h = sites[i]; + occurrence = ( i > 0 && cand[i - 1].fileId == cand[i].fileId && cand[i - 1].line == cand[i].line ) ? occurrence + 1 : 0; + if( h.enclosingId == kNoNode || h.enclosingId == defId || h.enclosingId >= ing.symbols.size() + || ing.symbols[h.enclosingId].name == name || h.line <= ing.symbols[h.enclosingId].line || !isCode( cand[i] ) ) + { + // the definition itself, file scope, a same-named declaration, a site on the line that DECLARES its enclosing + // symbol (a prototype parsed as `enum cmd_retval`), or a comment/string/unclassified occurrence: none of them + // is a body that registers it + continue; + } + std::size_t at = h.text.find( name ); + for( std::size_t k = 0; k < occurrence && at != std::string::npos; ++k ) + { + at = h.text.find( name, at + 1 ); + } + if( at != std::string::npos && grepIsWordAt( h.text, at, name.size() ) ) + { + return GrepRefSite{ h.fileId, h.line }; // a whole-word, code-tier occurrence (a cut line that lost it is skipped) + } + } + return {}; +} + // ─── R1a: the zero-hit follow-up (the 2026-08-12 usage mine) ────────────────────────────────────── // // A zero-hit --grep is the measured dead-end: the agent retries with another literal and never switches diff --git a/src/serialize.h b/src/serialize.h index d1a650f1a..830ed426e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -26,7 +26,9 @@ #include "sarif.h" // R-E (2026-08-17): rootRelativeUri/rootPrefixOf — the same root= single-root-only // strip --grep's emitGrepReport uses, reused here so the two verbs cannot diverge #include "prconverge.h" // W2-F: RankDisclosure + the pr_iters= / pr_converged= spellings (CLI and MCP share them) +#include "historyread.h" // K51: history_unread= on every root that prints churn=/amp= #include "gitmine.h" // F3 (H2H-Graft): RecentFile — the map's rows are the churn-decay miner's own product +#include "mapinventory.h" // D7: the map scope's tier — computeMapInventory, kMapInventoryLegend #include #include // std::iota — codeFirstKeep @@ -349,7 +351,9 @@ inline std::string ctxRootOpen( std::string_view task, std::string_view routeNot { out += " route_scrubbed=\"1\""; } + out += renderHistoryUnread( DiscloseAs::XmlAttrs ); // K51: absent unless the amp=/churn= walk could not be read; shared by --for/--pack-task/--from-trace out += ">"; + out += renderHistoryUnread( DiscloseAs::LegendComment ); // its own definition (historyread.h) return out; } @@ -377,6 +381,7 @@ inline std::string ctxRootJsonScrubKeys( std::string_view task, std::string_view { keys += ",\"route_xml_scrubbed\":true"; } + keys += renderHistoryUnread( DiscloseAs::JsonKeys ); // K51: this is the one JSON root writer every ctx-family document shares (--for, --pack-task, --from-trace) return keys; } @@ -1074,12 +1079,20 @@ inline FileTail computeFileTail( const IngestResult& ing, const std::vector& esc ) +// `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 = ""; + + "\" capped=\"" + ( shown < t.total ? "1" : "0" ) + "\""; + if( shown < t.total ) + { + x += cutNextAttr; + } + x += ">"; for( std::size_t i = 0; i < shown; ++i ) { x += ""; @@ -1128,11 +1141,11 @@ inline constexpr std::string_view kForHdrLegend = // 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 ) +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 ).size() <= budgetBytes ) + if( renderFileTailXml( t, shown, esc, cutNextAttr ).size() <= budgetBytes ) { return shown; } @@ -1953,6 +1966,11 @@ struct MapAnnotations // FE-B: the compact posture strips the map's via="name" comment (compactlegend.h, the via/x rows restate it), so the // map does not write it at all — and the --max-tokens fit, which measures this render, prices the delivered bytes. bool viaLegendStripped = false; + // D7 MAP-INVENTORY (mapinventory.h): the map scope's inventory tier — after the ranked rows, naming or counting + // every indexed file they did not show, plus the program entries. Set where codeFirstRows is (the plain CLI map, MCP + // analyze on a clean working set, MCP rank_by=pagerank) except under --max-tokens (mapInventoryWanted). Filled by + // assignment, like the trailing fields above. + bool inventoryTier = false; }; // ── the code-first row pick: data_sections_cut= / next= (docs/EVALS.md "Map data Sections never crowd code out of the @@ -2312,6 +2330,11 @@ inline constexpr const char* kDeclinedMapLegend = // only to a map where the gate declined at least one call, so a map the gate never touched keeps its bytes. inline constexpr const char* kDeclinedGateMapLegend = ""; +// The C++ standard-member gate's declines (graph.h StdMemberGate) ride the same declined= count; charged only to a map where +// that gate declined at least one call, so a map it never touched keeps its bytes. No '>' and no double hyphen inside. +inline constexpr const char* kDeclinedStdMapLegend = + ""; // #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 @@ -2743,6 +2766,65 @@ inline bool hopSlotHasProvenEdge( const std::vector& outOff, cons []( std::uint8_t bit ) { return bit != 0; } ); } +// D7 MAP-INVENTORY: whether THIS map carries the inventory tier (mapinventory.h), decided once for both dialects. The +// default map scope only (MapAnnotations::inventoryTier); never under in=DIR's stub (no ranked rows to complement), +// multi-root (the roots table already partitions the paths, and a per-root inventory is a different answer), or +// --max-tokens (its fit binary-searches top-k over the rendered bytes; an inventory that GROWS as top-k shrinks would +// make that search non-monotone). +inline bool mapInventoryWanted( const IngestResult& ing, const MapAnnotations& ann ) noexcept +{ + return ann.inventoryTier && !ann.stubSymbols && ing.rootLabels.size() < 2 && ann.maxTokensFit == nullptr; +} + +// The XML element: `……`. Entry callees go +// through writeMapCalleeRows, the ranked rows' own writer, so prov=/via="name"/x= read exactly as they do above. +template +inline void writeMapInventoryXml( XmlWriter& w, const IngestResult& ing, const MapInventory& inv, const PathRel& pathRel, + const std::vector& outOff, const std::vector& outTargets, + const std::vector* outProv, const std::vector* outNameOnly, + std::vector& esc, std::vector& scratch ) +{ + EXPECTS( inv.active, "the caller emits the tier only when it has a cut to inventory" ); + w.write( " inv.entries.size() ) + { + w.write( " entries_capped=\"1\" entries_total=\"" ); w.write( std::to_string( inv.entryTotal ) ); w.write( "\"" ); + w.write( nextAttrXml( inv.entriesNext, "entries_next" ) ); // rule 5: the cut is recoverable + } + if( inv.namesCapped ) + { + w.write( " names_capped=\"1\" names_total=\"" ); w.write( std::to_string( inv.namesTotal ) ); w.write( "\"" ); + w.write( nextAttrXml( inv.namesNext, "names_next" ) ); + } + w.write( ">" ); + for( NodeId id : inv.entries ) + { + const Symbol& s = ing.symbols[ id ]; + w.write( "" ); + writeMapCalleeRows( w, ing, outOff[ id ], outOff[ id + 1 ], outTargets, outProv, outNameOnly, esc, scratch ); + w.write( "" ); + } + for( const MapInventoryDir& d : inv.dirs ) + { + w.write( "" ); + } + w.write( "" ); +} + inline void serialize( std::FILE* out, const IngestResult& ing, const std::vector& rank, const std::vector& outOff, const std::vector& outTargets, int topK, bool mostImportantLast = false, @@ -2805,7 +2887,10 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto std::size_t gateDeclinedCalls = 0, // FE-B: Graph::outNameOnly (parallel to outTargets) → via="name" on a row and the via legend // comment; nullptr/empty ⇒ every edge is evidence-bound and the map is byte-identical. - const std::vector* outNameOnly = nullptr ) + 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 ) { const std::size_t* changedCount = ann.changedCount; const std::string* mapAtStamp = ann.atStamp; @@ -2854,6 +2939,9 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto if( !seen[f] ) { seen[f] = 1; fileOrder.push_back( f ); } buckets[f].push_back( id ); } + // D7: the inventory over the files the kept rows did NOT reach (`seen` is exactly the printed groups) + const MapInventory inventory = ( mapInventoryWanted( ing, ann ) && !stubbed ) + ? computeMapInventory( ing, outOff, outTargets, seen, pathRel ) : MapInventory{}; // T1/§H7: the byte MODEL. It keeps two jobs and loses one. It still decides the T3 emit order below // (which has to be decided before any byte exists — see §H7 at estimateTokens) and it still supplies @@ -2992,8 +3080,9 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto { legend += ""; legend += seedBiteLegend( ann.seed ); // C2: charged only on a run whose root carries a bite attribute (the at= rule) // H1: charged only on a run whose root carries unproven_defs= — the same rule, and part of the head est_tokens prices. @@ -3029,6 +3118,10 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto { legend += kDataSectionsCutLegend; // charged to the map that carries data_sections_cut= } + if( inventory.active ) + { + legend += kMapInventoryLegend; // D7: charged to the map that carries + } // W2-F: the pr_iters= / pr_converged= definition, charged to the maps that carry the attributes — empty // for a lexical or HITS ordering, and the prose half only on the map whose iteration stopped short. legend += renderDisclosure( ann.prDisclosure, DiscloseAs::LegendComment ); @@ -3041,6 +3134,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::size_t declinedTotal = counterTotal( declinedOut ); // calls tier 3 declined: no edge, and no guess legend += declinedTotal > 0 ? kDeclinedMapLegend : ""; // charged to the map that carries declined= legend += gateDeclinedCalls > 0 ? kDeclinedGateMapLegend : ""; // only where the builtin-method gate declined a call + legend += stdMemberDeclinedCalls > 0 ? kDeclinedStdMapLegend : ""; // only where the C++ standard-member gate declined a call if( outNameOnly && !stubbed && !ann.viaLegendStripped && namesOnlyOutAny( outOff, *outNameOnly, std::span( order.data(), std::min( keep, order.size() ) ) ) ) { @@ -3247,6 +3341,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto // L3 follow-up (CodeRabbit 4053600616): TRULY last, past every pre-existing attribute — same placement // rule as lens= just above, so no attribute-adjacency assertion in test/ can break on it. if( ann.notesDegraded ) { h += notes::kNotesDegradedAttr; } + h += renderHistoryUnread( DiscloseAs::XmlAttrs ); // K51: absent unless the amp=/churn= walk could not be read // The code-first pick's swaps, last of all (the same placement rule as the two above): absent when nothing was swapped. if( dataSecCut.cut > 0 ) { @@ -3254,6 +3349,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto h += nextAttrXml( dataSectionsNext( dataSecCut ) ); } h += ">"; + h += renderHistoryUnread( DiscloseAs::LegendComment ); // K51: its own definition, right behind the root it marks return h; }; const auto buildTail = [ & ]( std::size_t estTokens ) -> std::string @@ -3335,6 +3431,21 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto stub += "/>"; w.write( stub ); } + // D7: the inventory tier rides the LOW-importance end of the document — after the rows, or before them when the + // map is emitted important-last (an explicit flag or the fill-aware auto flip), so the top-ranked rows keep the + // end position that order promises. + const auto writeInventory = [ & ]() + { + if( inventory.active ) + { + writeMapInventoryXml( w, ing, inventory, pathRel, outOff, outTargets, outProv, outNameOnly, esc, hedgedScratch ); + } + }; + const bool inventoryFirst = !stable && effImportantLast; // --stable's path order wins, as it does for the rows + if( inventoryFirst ) + { + writeInventory(); + } static const std::vector kNoFiles; for( std::uint32_t f : ann.stubSymbols ? kNoFiles : fileOrder ) { @@ -3553,6 +3664,10 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto } w.write( "" ); } + if( !inventoryFirst ) + { + writeInventory(); + } w.write( "" ); }; // DEGRADE, one path for both failures: the head goes out FIRST carrying the MODELLED estimate (the pre-§H7 number, so @@ -4949,6 +5064,8 @@ struct SigsCutReport bool hasContinuation = false; // the caller asked for the continuation (next=/next_budget_tokens=/next_offset=) bool terseContinuation = false; // a CHARGED pasteable one under a hard ceiling: next= alone (no next_offset=, no // clause — the bundle's own next= reading defines it); see SigsCutContinuation + std::size_t 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 @@ -5011,6 +5128,10 @@ struct SigsCutContinuation // 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 @@ -5198,6 +5319,22 @@ 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 @@ -5343,9 +5480,14 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // for the caller's legend splices. Lens path only; zeroed // (nothing cut) on every other path. knob-honesty-068: its // continuationRequest is an INPUT, read before the reset. - SigRowSpelling spelling = {} ) // lean-answers lane: the --for lens's zero cx=/ccx=/in= omitted + SigRowSpelling spelling = {}, // lean-answers lane: the --for lens's zero cx=/ccx=/in= omitted // (SigRowFacts::elideZero); its legend says absent = 0. Default ⇒ // byte-identical (pack-task, from-trace and the map stay as they were). + std::uint32_t rankBase = 0 ) // d1/paging (#294): 1-based GLOBAL candidate-rank base for the windowed + // serving (0 = the un-windowed default, byte-identical) — a continuation + // page's rows carry their GLOBAL r= (page 2's first row is r=201, not + // r=1: the rank-adaptive doc/sig tiers and the r=1 row's next= hint key + // on GLOBAL rank), so pages merge without duplicate r= { const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; if( cutOut ) @@ -5413,7 +5555,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: globalRankOf.assign( S, 0 ); for( std::size_t k = 0; k < keep; ++k ) { - globalRankOf[order[k]] = std::uint32_t( k + 1 ); + globalRankOf[order[k]] = rankBase + std::uint32_t( k + 1 ); // + rankBase: the WINDOWED serving's global base } } @@ -5674,10 +5816,12 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // knob-honesty-068: a CHARGED continuation is paid from the rows (paySigsContinuationFromRows); what the floor // cannot pay rides anyway and is reported for the caller's over_ceiling label // …only where the caller lets rows pay (payFromRows): otherwise the whole of it rides unpaid on the cut's own rows - const std::size_t cutNextOwed = plan.capped ? cutNextCost : 0u; + const std::size_t 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; // 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. @@ -5727,6 +5871,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: } cut.continuationRequest = cutNext; cut.continuationUnpaidBytes = cutNextUnpaid; + cut.continuationPaidSurplus = cutNextSurplus; sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { @@ -5754,7 +5899,11 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: { continue; } - pushShownSigId( shownIdsOut, order, e.globalRank ); // lane 2 + // lane 2 — the LOCAL rank under rankBase: globalRank carries the WINDOWED serving's + // global offset (#294 review), and order[] holds the WINDOW's rows in its head, so the + // id mapping must subtract rankBase or it reads past the window into the masked tail + // (the #362 review round's next_offset=2^32 seam). + pushShownSigId( shownIdsOut, order, std::uint32_t( e.globalRank - rankBase ) ); writeSigHeadWithEnd( w, e.head, e.endLine ); // e= after l=, when the lens asked and the extent is known if( !e.doc.empty() ) { w.write( "" ); w.write( escapeXml( e.doc, esc ) ); w.write( "" ); } w.write( escapeXml( e.sig, esc ) ); @@ -6327,24 +6476,38 @@ struct CalleeCallsSink // FE-B: set once the block's caller has been charged the via="name" legend comment it will write (packBodies/packHops // write it ahead of the rows, after the walk) — so the budget the walk honours already holds those bytes bool* viaLegendCharged = nullptr; + // owner-hop (verbs_for.h THE QUESTION'S OWNER): an owner row lists its callees up to a RUNAWAY GUARD instead of the + // sixteen a ranked row shows, and a cut listing carries the pasteable full list. Defaulted to the ranked shape, so + // 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; }; // The wrapper, written in front of the rows it describes (they have to be walked before `shown` // is known). pageview.h THE TRUNCATION VOCABULARY rule 3: shown=/capped= are added ONLY when the 16-row // cap or the byte budget actually cut the list, and they are added TOGETHER — never shown="T" capped="0" // on a complete listing. -inline void appendCallsBlock( std::string& out, std::uint32_t total, int shown, const std::string& rows ) +// `nextEscaped` (owner rows only): the continuation that lists the whole set, written beside capped="1" and only then. +inline void appendCallsBlock( std::string& out, std::uint32_t total, int shown, const std::string& rows, std::string_view nextEscaped = {} ) { char callsHdr[ 64 ]; - if( static_cast( shown ) < total ) + const bool cut = static_cast( shown ) < total; + if( cut ) { - rw::formatTo( callsHdr, sizeof( callsHdr ), "", total, shown ); + rw::formatTo( callsHdr, sizeof( callsHdr ), "", total ); + rw::formatTo( callsHdr, sizeof( callsHdr ), " nameRows; // names-only rendering: collected, merged by name, written after the walk int shown = 0; - for( std::uint32_t k = outOff[id]; k < outOff[id + 1] && shown < 16 && used < budgetBytes; ++k ) + for( std::uint32_t k = outOff[id]; k < outOff[id + 1] && shown < int( sink.rowCap ) && used < budgetBytes; ++k ) { const NodeId cid = walk[ k - outOff[id] ]; if( cid >= ing.symbols.size() ) @@ -6597,7 +6760,7 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector } } appendMergedCalleeNameRows( callsBody, nameRows, esc ); // no-op on the signature rendering (nameRows stays empty) - appendCallsBlock( out, total, shown, callsBody ); + appendCallsBlock( out, total, shown, callsBody, sink.cappedNext ); } // THE DISCLOSURE (§B8.3). This was the one budgeted section element carrying NO attributes at all: @@ -7348,6 +7511,137 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect w.flush(); } +// ── THE QUESTION'S OWNER ROWS (lane owner-hop-068; the selection is verbs_for.h forOwnerHopPlan) ────────────────── +// The first `rows` nodes handed to packHops are OWNER rows: a ranked callable definition whose whole name equals a word +// of the question. Each prints `qword=""`, lists its callees up to kForOwnerHopNameGuard (a runaway guard, the +// kMaxExpandSibs precedent — never a target) and, when that listing is cut, says where the whole of it is +// (``). Owners the plan could not seat (the row cap) or the budget could not fund are counted +// in ``, the continuation naming the first of them. +inline constexpr std::uint32_t kForOwnerHopNameGuard = 100; +struct HopOwners +{ + std::size_t rows = 0; // the first `rows` nodes are owner rows, in slot order + std::vector words; // the folded question word of each owner row (one per row) + std::size_t budgetBytes = 0; // 0: ride ON TOP of the hop budget (default regime, guard only) + bool fundFirst = false; // explicit ceiling: owners are funded first and the ranked rows get + // what they leave of budgetBytes (never more than their own budget) + std::size_t capCut = 0; // owners with a proven edge past the row cap (never seated) + NodeId firstCapCut = kNoNode; +}; + +// The continuation that lists one owner's callees: the canonical id when the symbol has a scope (two same-named methods +// of one file are two entities, and FILE:NAME would merge them), else FILE:NAME. Escaped for an attribute value. +inline std::string ownerCalleesNext( const IngestResult& ing, const Symbol& s, std::string_view pathRel, std::string_view rootArg, + std::vector& esc ) +{ + std::string target = s.scope.empty() ? std::string( pathRel ) + ":" + std::string( s.name ) : canonicalIdForEmit( ing, s, rootArg ); + return "--callees=" + std::string( escapeXml( target, esc ) ); +} + +// What the owner walk hands back to packHops: the rows, how many nodes they consumed, and the budget and via-legend charge +// the RANKED rows then run on, plus the cut disclosure (`cutNext` already escaped; empty when nothing was cut). +struct OwnerHopRows +{ + std::string xml; + std::size_t rows = 0; // leading nodes that were owner rows (served or budget-cut) + std::size_t shown = 0; + std::size_t rankedBudget = SIZE_MAX; // the ranked rows' budget: unchanged, or (explicit ceiling) what the owners left + bool rankedVia = false; // the ranked rows' via-legend charge state + std::size_t cut = 0; // qword_cut=: cap cuts + budget cuts + std::string cutNext; +}; + +// The owner walk (packHops' first rows). Default regime: their own byte room, unlimited, the name guard the only stop, and +// their own via-legend charge — so the ranked rows are charged exactly what they were charged without owners. Explicit +// ceiling (fundFirst): funded first; the ranked rows get the rest. A row that cannot pay for its identity and its next= is +// not started: it is counted as cut, so an owner row never drops its names without saying where the rest are. +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, + std::string_view rootArg, std::size_t rankedBudget, bool rankedVia, + PathRelFn&& pathRel, ContentOfFn&& contentOf, std::vector& esc ) +{ + OwnerHopRows out; + out.rankedBudget = rankedBudget; + out.rankedVia = rankedVia; + if( owners == nullptr ) + { + return out; + } + EXPECTS( owners->words.size() == owners->rows, "one question word per owner row" ); + out.rows = std::min( owners->rows, nodes.size() ); + out.cut = owners->capCut; + std::size_t used = 0; + const std::size_t budget = owners->budgetBytes > 0 ? owners->budgetBytes : SIZE_MAX; + bool via = rankedVia; + NodeId firstCut = kNoNode; // the first owner the BUDGET cut (it ranks before every cap cut) + for( std::size_t i = 0; i < out.rows; ++i ) + { + const NodeId id = nodes[ i ]; + // forOwnerHopPlan seats only indexed symbols, each with a proven callee edge (hopSlotHasProvenEdge, read there) + ASSUME( id < ing.symbols.size(), "the owner plan seats only indexed symbols (forOwnerHopPlan)" ); + const Symbol& s = ing.symbols[ id ]; + const std::string next = ownerCalleesNext( ing, s, pathRel( s.fileId ), rootArg, esc ); + std::string row = "words[ i ], esc ); + row += "\">"; + const std::size_t rowFixed = row.size() + next.size() + 8 + 16; // + ' next=""' + '' + if( used + rowFixed >= budget ) + { + firstCut = firstCut == kNoNode ? id : firstCut; + ++out.cut; + continue; + } + used += rowFixed; + emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budget, + CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, + kForOwnerHopNameGuard, next } ); + out.xml += row; + out.xml += ""; + ++out.shown; + } + if( owners->fundFirst ) + { + out.rankedBudget = std::min( rankedBudget, owners->budgetBytes > used ? owners->budgetBytes - used : 0 ); + out.rankedVia = rankedVia || via; + } + const NodeId first = firstCut != kNoNode ? firstCut : owners->firstCapCut; + if( out.cut > 0 ) + { + ASSUME( first < ing.symbols.size(), "a counted owner cut names an indexed symbol (forOwnerHopPlan / the walk above)" ); + out.cutNext = ownerCalleesNext( ing, ing.symbols[ first ], pathRel( ing.symbols[ first ].fileId ), rootArg, esc ); + } + return out; +} + +// The open tag: the truncation triple, noedge= when non-zero, and the owner cut disclosure when there is one. +inline std::string hopsOpenTag( std::size_t shown, std::size_t requested, std::size_t noEdge, const OwnerHopRows& own ) +{ + char open[ 128 ]; + if( noEdge > 0 ) + { + rw::formatTo( open, sizeof( open ), " 0 ) + { + tag += " qword_cut=\"" + std::to_string( own.cut ) + "\" next=\"" + own.cutNext + "\""; // DISCLOSE the owners not served + } + tag += ">"; + return tag; +} + // ── THE SECTION: one-hop edge context without the body text ──────────────────────────────────── // (pre-registered: docs/EVALS.md, the T3 route-narrowing round; the allowance is // kForCompactHopBudgetBytes above, with its derivation.) @@ -7367,8 +7661,9 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // group for, and walking `nodes` in the order the caller ranked them spends the budget on the // best-ranked symbols first. // -// A candidate with no resolved out-edges gets NO row and is counted in `noedge=` instead — see the loop -// for why that is the honest form rather than the cheap one. Call edges are name-based, so dynamic +// A candidate with no resolved out-edges gets NO row and is counted in `noedge=` instead — and so does an OWNER of the +// question (THE QUESTION'S OWNER ROWS above) with none, exactly once whether or not it was also a ranked candidate — see +// the loop for why that is the honest form rather than the cheap one. Call edges are name-based, so dynamic // dispatch, callbacks and macros can be missing: `noedge=` means none FOUND, never none exists // (CONTRIBUTING #3), and it is deliberately a separate number from `capped=`, which says only that the // byte budget stopped before the remaining candidates. @@ -7377,10 +7672,10 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector const std::vector& outOff, const std::vector& outTargets, const std::vector& outNameOnly, // FE-B: Graph::outNameOnly — the rows' via="name" RedactCounts* redact = nullptr, - std::size_t* outShown = nullptr, // rows actually emitted; nullptr ⇒ not recorded const std::vector* rank = nullptr, // query relevance, for ordering a CUT callee listing 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 + bool viaLegendInHead = false, // FE-B: packBodies' parameter of the same name + const HopOwners* owners = nullptr ) // owner-hop: the leading owner rows; nullptr ⇒ none (byte-identical) { bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) if( budgetBytes == 0 ) // 0 ⇒ UNLIMITED, packBodies' own convention @@ -7424,12 +7719,19 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector return contents.emplace( fid, std::move( s ) ).first->second; }; - std::string children; - std::size_t requestedCount = 0; - std::size_t shownCount = 0; + // 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 ); + std::string children = own.xml; + std::size_t requestedCount = own.shown; + std::size_t shownCount = own.shown; std::size_t noEdgeCount = 0; - for( NodeId id : nodes ) + budgetBytes = own.rankedBudget; + viaLegendCharged = own.rankedVia; + for( auto it = nodes.begin() + std::ptrdiff_t( own.rows ); it != nodes.end(); ++it ) { + const NodeId id = *it; if( id >= ing.symbols.size() ) { continue; // never a request this function could answer @@ -7482,29 +7784,15 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // capped= is the BUDGET bit only: the candidates neither printed nor accounted by noedge=. Emitted // as the 0|1 boolean the vocabulary requires either way; noedge= follows the house silence rule and // appears only when it is non-zero. - char open[ 128 ]; - if( noEdgeCount > 0 ) - { - rw::formatTo( open, sizeof( open ), "", - shownCount, requestedCount, shownCount + noEdgeCount < requestedCount ? 1 : 0, noEdgeCount ); - } - else - { - rw::formatTo( open, sizeof( open ), "", - shownCount, requestedCount, shownCount < requestedCount ? 1 : 0 ); - } + const std::string openTag = hopsOpenTag( shownCount, requestedCount, noEdgeCount, own ); if( !viaLegendInHead && children.find( " via=\"name\"" ) != std::string::npos ) { w.write( viaNameLegendComment() ); // FE-B: exactly when a hop's row carries via="name" } - w.write( open ); + w.write( openTag ); w.write( children ); w.write( "" ); w.flush(); - if( outShown ) - { - *outShown = shownCount; - } } // ── M6 (density audit 2026-08-08): the WHOLE-FILE form a bare --expand can serve ───────────────────── @@ -7868,8 +8156,10 @@ inline std::string outlineSkeleton( const std::string& src, const Symbol& s, boo // , byte-identical). total= counts the valid requests with a body to outline (a module scope has // none, the packBodies bodyless rule); a skeleton whose span is unreadable is not shown and so also cuts. inline void packOutline( std::FILE* out, const IngestResult& ing, const std::vector& nodes, std::size_t budgetBytes, bool compress, RedactCounts* redact, - std::string_view rootArg = {} ) // R-E (2026-08-17): same single-root-only root + std::string_view rootArg = {}, // R-E (2026-08-17): same single-root-only root // argument serialize() takes — see its comment. + std::vector* shownIdsOut = nullptr ) // expand-lean-k64: the ids this call EMITTED an for + // (nullptr ⇒ not recorded; output unchanged either way) { XmlWriter w( out ); std::vector esc; @@ -7934,6 +8224,10 @@ inline void packOutline( std::FILE* out, const IngestResult& ing, const std::vec piece += safe; piece += "]]>"; used += safe.size(); ++shownCount; + if( shownIdsOut != nullptr ) + { + shownIdsOut->push_back( id ); + } pieces.push_back( PackedBodyPiece{ slot, id, SIZE_MAX, false, std::move( piece ) } ); } std::stable_sort( pieces.begin(), pieces.end(), []( const PackedBodyPiece& x, const PackedBodyPiece& y ) { return x.fileSlot < y.fileSlot; } ); @@ -8169,6 +8463,82 @@ inline SectionStubPricing priceSectionStub( const char* tag, std::size_t preCapT return p; } +// One stubbable --for section ( or ) as the stub plan reads it. +struct ForStubbableSection +{ + bool hasContent; // the section would render non-empty + std::size_t preCapTotal; // its own pre-cap row count: the stub's total= + bool rendered; // its bytes were measured (false = the open_memstream degrade path, never collapsed) + std::size_t renderedBytes; // the measured full render (0 when !rendered) +}; + +// The / stub decision of a ranked --for answer: which section collapses (L2 / R2-L2', priceSectionStub) and +// the ONE restoring spelling. One function for the CLI lens (verbs_for.h runForLens) and its MCP twin (mcpverbs.h +// forTaskText), for both the final decision and the charge (forSectionsSigCharge below). +struct ForSectionStubPlan +{ + std::string nextInvocation; // empty when neither section is a candidate + SectionStubPricing lego{}; + SectionStubPricing compose{}; + bool legoWillStub = false; + bool composeWillStub = false; + + // the bytes the two sections occupy as served: a collapsing section at its stub, any other at its measured render. + // kForSectionStubLegend is not in it: a disclosure clause, exempt from the charge like the e=/owner/tail clauses. + [[nodiscard]] std::size_t servedBytes( const ForStubbableSection& legoIn, const ForStubbableSection& composeIn ) const noexcept + { + return ( legoWillStub ? lego.stubXml.size() : legoIn.renderedBytes ) + ( composeWillStub ? compose.stubXml.size() : composeIn.renderedBytes ); + } +}; + +inline ForSectionStubPlan planForSectionStubs( std::string_view sections, std::string_view forTask, const ForStubbableSection& legoIn, + const ForStubbableSection& composeIn ) +{ + ForSectionStubPlan p; + // a CANDIDATE has content and was not opted back in by --sections=; it collapses only when its stub is cheaper + const bool legoCandidate = legoIn.hasContent && !sectionsWant( sections, "lego" ); + const bool composeCandidate = composeIn.hasContent && !sectionsWant( sections, "compose" ); + if( legoCandidate || composeCandidate ) + { + p.nextInvocation = nextFlag( "--for=", forTask ); + p.nextInvocation += ' '; + p.nextInvocation += nextFlag( "--sections=", "lego,compose" ); + } + if( legoCandidate ) + { + p.lego = priceSectionStub( "lego", legoIn.preCapTotal, p.nextInvocation, legoIn.rendered, legoIn.renderedBytes ); + } + if( composeCandidate ) + { + p.compose = priceSectionStub( "compose", composeIn.preCapTotal, p.nextInvocation, composeIn.rendered, composeIn.renderedBytes ); + } + p.legoWillStub = legoCandidate && p.lego.collapse; + p.composeWillStub = composeCandidate && p.compose.collapse; + const std::size_t served = p.servedBytes( legoIn, composeIn ); + ENSURES( served <= legoIn.renderedBytes + composeIn.renderedBytes, + "planForSectionStubs: a collapse made the two sections bigger than their measured renders" ); + return p; +} + +// 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. +// Two rules decide what "served" means here: +// * Without an explicit ceiling, the charge is the DEFAULT answer's, also under --sections=: the stub's next= promises the +// sections back "byte-identically in one call" (kForSectionStubLegend), and only a call whose match the stubbed +// answer's renders them so (the lego is narrowed to the rendered sigs' files, §P3×§P4). The restored sections then ride +// on top of that answer, priced in est_tokens=; charging them in full would serve FEWER rows than the stub promised. +// * Under an explicit ceiling (--token-budget / MCP budget_tokens), a section --sections= opts into is +// charged at its full render: the ceiling is a hard bound, and the stub's next= never carries one. +// The charge is taken before the §P3×§P4 narrow, on the pre-narrow render and count. The narrow only empties implementor +// lists, so a served stub's total= can only shrink (runForLens ENSURES it). A section served whole was charged at its +// pre-narrow render, as it was before this change. +inline ForSectionStubPlan forSectionsSigChargePlan( std::string_view sections, bool explicitCeiling, std::string_view forTask, + const ForStubbableSection& legoIn, const ForStubbableSection& composeIn ) +{ + return planForSectionStubs( explicitCeiling ? sections : std::string_view(), forTask, legoIn, composeIn ); +} + // B6.3 HTTP-route cross-service view: for a set of relevant symbols, emit the synthesized route USE→DEF // edges (client call → server handler) as a block. ONLY called from --for and --around (NOT the // default map) — see model.h RouteEdge / graph.h buildGraph's B6.3 section for how these are matched. @@ -8468,6 +8838,128 @@ inline bool narrowLegoToRenderedSigs( const IngestResult& ing, std::vector deeper; // every depth >= 2 row of the closure, in walk order + PageWindow window{ 0, 0 }; // the slice of `deeper` this answer prints + std::size_t otherBase = 0; // fix 1: types left out — their clause names a DIFFERENT same-named type + std::vector unwalked; // fix 2: direct rows the walk stops below (their own base unproven) +}; + +// fix 1: the closure is graph.h legoImplementorClosure — walked through PROVEN extends edges only, a name-only edge's row +// hedged (base_by="name") and not walked, a name collision's row left out and counted (transitive_other_base=). +inline LegoClosurePage legoClosurePage( const std::vector>& implementors, const std::vector>& bound, + NodeId iface, int pageLimit, int pageOffset ) +{ + LegoClosurePage page; + const LegoClosure closure = legoImplementorClosure( implementors, bound, iface ); + page.otherBase = closure.otherBase; + page.unwalked = closure.unwalked; + for( const ImplementorRow& row : closure.rows ) + { + if( row.depth >= 2 ) + { + page.deeper.push_back( row ); + } + } + page.window = pageWindow( page.deeper.size(), effectiveRowCap( pageLimit, int( kLegoClosureRowCap ) ), pageOffset ); + ENSURES( page.window.begin <= page.window.end && page.window.end <= page.deeper.size() ); + return page; +} + +// The attributes of that page. EMPTY when the closure has no deeper row, no left-out row and no unwalked direct +// row, so an answer without one is byte-identical to the direct-only answer. `selector` is the iface's own file:name, the +// same handle implementors_next= names, so the next page resolves the same definition whatever the reader typed. +// `unwalkedSelector` (fix 2) is the file:name of the FIRST direct row the walk stopped below: transitive_unwalked=N counts +// those rows (each also carries unwalked="1") and unwalked_next= is that first row's own --lego, which walks below it. +inline std::string legoClosureAttrs( const LegoClosurePage& page, std::string_view selector, std::string_view unwalkedSelector, int pageLimit ) +{ + const std::size_t total = page.deeper.size(); + std::string other = page.otherBase == 0 ? std::string() : " transitive_other_base=\"" + std::to_string( page.otherBase ) + "\""; + if( !page.unwalked.empty() ) + { + other += " transitive_unwalked=\"" + std::to_string( page.unwalked.size() ) + "\""; + other += rw::nextAttrXml( nextFlag( "--lego=", unwalkedSelector ), "unwalked_next" ); + } + if( total == 0 ) + { + return other; + } + std::string out = " transitive=\"" + std::to_string( total ) + "\"" + other; + const std::size_t shown = page.window.end - page.window.begin; + if( shown < total ) + { + out += " transitive_shown=\"" + std::to_string( shown ) + "\""; + } + if( page.window.begin > 0 ) + { + out += " transitive_offset=\"" + std::to_string( page.window.begin ) + "\""; + } + if( page.window.end < total ) + { + out += " has_more=\"1\""; + out += rw::nextAttrXml( pagedNext( nextFlag( "--lego=", selector ), pageLimit, page.window.end ) ); + } + return out; +} + +// The TARGETED verb's own inputs to packLego, one parameter: the root's gauge + marker (M15) and the deeper-row page +// (lane lego-transitive). The ranked --for bundle passes `{}` — no gauge on its section, no page to window. +struct LegoTargetedArgs +{ + std::string_view graphCountFloorAttr; // graphCountFloorAttrXml( g ) — the caller owns the graph + 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) + 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 ) {} +}; +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 +// --callers spelling) so the reader can open the class itself; the ranked bundle rows keep p="file". `deep` is the +// closure row of a depth >= 2 implementor (via= names the type it extends, depth= its hops), null for a direct one. +// `unwalked` (fix 2): a direct row the closure walk stopped below — its own extends clause is not proven — rides unwalked="1". +inline void writeLegoImplRow( XmlWriter& w, const IngestResult& ing, NodeId id, std::string_view relPath, bool withPaths, bool line, + const ImplementorRow* deep, bool unwalked, std::vector& esc ) +{ + const Symbol& im = ing.symbols[ id ]; + w.write( " 0 ) + { + w.write( ":" ); w.write( std::to_string( im.line ) ); + } + } + if( deep != nullptr ) + { + w.write( "\" via=\"" ); w.write( escapeXml( ing.symbols[ deep->via ].name, esc ) ); + w.write( "\" depth=\"" ); w.write( std::to_string( deep->depth ) ); + if( deep->byName ) + { + w.write( "\" base_by=\"name" ); // fix 1: the extends clause names via= by spelling alone — hedged, not walked below + } + } + if( unwalked ) + { + w.write( "\" unwalked=\"1" ); + } + w.write( "\"/>" ); +} + // the Lego view: for the top relevant interfaces/base-classes (those with implementors), emit the // concrete implementations — the socket → interchangeable bricks. Descriptive (no pattern labels); // steers an agent to snap a new brick into the socket instead of reimplementing. Emitted in --for. @@ -8487,7 +8979,7 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector NodeId focusId = kNoNode, bool withPaths = false, std::string_view rootArg = {}, // R-E (2026-08-17): same single-root-only root // argument serialize() takes — see its comment. - std::string_view graphCountFloorAttr = {}, // M15: the TARGETED root's gauge + marker + const LegoTargetedArgs& targeted = {}, // M15: the TARGETED root's gauge + marker; lane lego-transitive: its page // (graphCountFloorAttrXml( g ) — the caller owns the graph); // the ranked --for section passes nothing and keeps its shape std::size_t* outPreCapCount = nullptr, // L2 (round-1 lever B1): the STUB's total= — this @@ -8507,6 +8999,7 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector // (implementors_shown= + implementors_next=). Null keeps the old shape. { EXPECTS( graphImplementors == nullptr || focusId == kNoNode ); // the targeted verb's map IS the graph's + EXPECTS( focusId != kNoNode || ( targeted.pageLimit == 0 && targeted.pageOffset == 0 ) ); // the ranked bundle has no page to window const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); const auto pathRel = [ & ]( std::uint32_t fileId ) -> std::string_view { @@ -8568,7 +9061,7 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector // floor. The marker rides on the TARGETED verb root (--lego=TYPE and its MCP twin); the --for bundle's // ranked section is described by the bundle's own legend and keeps its byte shape. w.write( "" ); for( std::size_t k = 0; k < keep; ++k ) { @@ -8589,9 +9082,13 @@ 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 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() ) + "\"" + + 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 ); } else { @@ -8670,17 +9167,22 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector } } + std::size_t nextUnwalked = 0; // deep.unwalked is a subsequence of impls (the walk lists the direct rows in map order) for( std::size_t j = 0; j < cap; ++j ) { - const Symbol& im = ing.symbols[ impls[j] ]; - w.write( "" ); + const bool unwalked = nextUnwalked < deep.unwalked.size() && deep.unwalked[ nextUnwalked ] == impls[j]; + nextUnwalked += unwalked ? 1u : 0u; + writeLegoImplRow( w, ing, impls[j], pathRel( ing.symbols[ impls[j] ].fileId ), withPaths, focusId != kNoNode, nullptr, unwalked, esc ); } + ENSURES( focusId == kNoNode || nextUnwalked == deep.unwalked.size() ); // targeted: every unwalked row was marked if( impls.size() > cap ) { w.write( "" ); } + 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 ); + } w.write( "" ); } w.write( "" ); @@ -8690,6 +9192,64 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector // --deps: the file→file physical dependency view. Files ranked by include count (heaviest first = // the "pulls in 100 headers to do something simple" detector); each lists its #include/import // targets. Descriptive — names the number so the agent can choose a lighter path. +// --report / --deps godfiles: the layer=test group. A file joins it only when its built-in layer is "test" (a +// directory name: test/, tests/, bench/) AND every file that depends on it is a test path (isTestPath) — both +// by path, nothing is parsed. A production file under test/ (src/*.cpp includes ../test/lib/hooks.h) therefore +// stays ranked. `adj` is the same file→file graph the afferent counts come from; the result is indexed by file id. +inline std::vector testLayerGroup( const IngestResult& ing, const std::vector>& adj ) +{ + const std::uint32_t F = std::uint32_t( ing.files.size() ); + std::vector prodDependent( F, 0 ); + for( std::uint32_t a = 0; a < std::uint32_t( adj.size() ) && a < F; ++a ) + { + if( isTestPath( rootRelPath( ing, a ) ) ) + { + continue; + } + for( std::uint32_t b : adj[a] ) + { + if( b < F ) + { + prodDependent[b] = 1; + } + } + } + std::vector group( F, 0 ); + for( std::uint32_t f = 0; f < F; ++f ) + { + group[f] = !prodDependent[f] && std::string_view( builtinLayer( rootRelPath( ing, f ) ) ) == "test"; + } + ENSURES( group.size() == ing.files.size(), "indexed by file id" ); + return group; +} + +// --deps godfiles row layer=: the built-in arch layer from a dir name, except that "test" is spelled only for a +// file in the testLayerGroup (every dependent a test path); a test-dir file with a production dependent has none. +inline const char* godfileLayer( const IngestResult& ing, std::uint32_t f, const std::vector& testGroup ) noexcept +{ + const char* fl = builtinLayer( rootRelPath( ing, f ) ); + return std::string_view( fl ) == "test" && !( f < testGroup.size() && testGroup[f] ) ? "" : fl; +} + +// --deps : the most depended-on files (afferent > 0), most first with a path tie-break, at most 12; +// `total` receives how many files have any dependent (the listing's total=). +inline std::vector depsGodfileRows( const IngestResult& ing, const std::vector& afferent, std::size_t& total ) +{ + std::vector byAff; + for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) + { + if( f < afferent.size() && afferent[f] > 0 ) + { + byAff.push_back( f ); + } + } + std::sort( byAff.begin(), byAff.end(), [ & ]( std::uint32_t a, std::uint32_t b ) + { return afferent[a] != afferent[b] ? afferent[a] > afferent[b] : ing.files[a] < ing.files[b]; } ); + total = byAff.size(); + byAff.resize( std::min( byAff.size(), 12 ) ); + return byAff; +} + inline void packDeps( std::FILE* out, const IngestResult& ing, int topN, const std::vector>& cycles, const std::vector& transitive, const std::vector& afferent, @@ -8767,6 +9327,16 @@ inline void packDeps( std::FILE* out, const IngestResult& ing, int topN, "dependency: it is in the impact verb's importer tier (lazy=1) and in this row's inc t= list, and it is NOT in " "afferent=/instab=/transitive=/godfiles/stabledeps/cycles/ccd/acd/nccd/shape=; health lazy_edges= counts the " "pairs left out and a row's lazy_edges= its own — both absent when 0. " ); + // present-only: the layer= reading rides exactly when a shown godfiles row carries layer= + std::size_t godTotal = 0; + const std::vector godRows = depsGodfileRows( ing, afferent, godTotal ); + const std::vector testGroup = testLayerGroup( ing, adj ); + const bool godLayered = std::any_of( godRows.begin(), godRows.end(), + [ & ]( std::uint32_t f ) { return *godfileLayer( ing, f, testGroup ) != '\0'; } ); + if( godLayered ) + { + w.write( "a godfiles row's layer= is its built-in arch layer from a dir name (absent if none); layer=test only for a file under a test directory whose dependents are all test paths, by path. " ); + } w.write( rw::depsImportsUnresolvedLegend( tsImports.unresolved > 0 ) ); // #220: exactly when the root carries the pair w.write( rw::depsTsImportExtrasLegend( tsImports.dts, tsImports.unread ) ); w.write( "raise the default cap with limit=N (offset=M pages; a cut listing carries total=/has_more=/next_offset= so a paging loop can continue from it). -->" ); @@ -8812,17 +9382,7 @@ inline void packDeps( std::FILE* out, const IngestResult& ing, int topN, // most depended-ON (afferent coupling Ca) = highest blast radius: changing these recompiles the most. // The complement of the transitive-cone ranking below (efferent, "pulls in 100 headers"). { - std::vector byAff; - for( std::uint32_t f = 0; f < F; ++f ) - { - if( f < afferent.size() && afferent[f] > 0 ) - { - byAff.push_back( f ); - } - } - std::sort( byAff.begin(), byAff.end(), [ & ]( std::uint32_t a, std::uint32_t b ) - { return afferent[a] != afferent[b] ? afferent[a] > afferent[b] : ing.files[a] < ing.files[b]; } ); - const std::size_t capG = byAff.size() < 12 ? byAff.size() : 12; + const std::size_t capG = godRows.size(); if( capG ) { // §P9 N6: this listing used to emit exactly 12 rows with no total/cap disclosure while @@ -8830,12 +9390,16 @@ inline void packDeps( std::FILE* out, const IngestResult& ing, int topN, // src/pageview.h, THE TRUNCATION VOCABULARY, rules 1-3, applied here too. char gfb[ 64 ]; rw::formatTo( gfb, sizeof( gfb ), "", - byAff.size(), capG, capG < byAff.size() ? 1 : 0 ); + godTotal, capG, capG < godTotal ? 1 : 0 ); w.write( gfb ); // ranked by afferent = # files that #include this one for( std::size_t i = 0; i < capG; ++i ) { - char gb[ 48 ]; rw::formatTo( gb, sizeof( gb ), "\" afferent=\"{}\"/>", afferent[ byAff[i] ] ); - w.write( "" ); } w.write( "" ); } @@ -9061,6 +9625,67 @@ inline void emitImportRowsXml( std::FILE* out, const IngestResult& ing, } } +// fix27: an rf row's hedge on a multi-definition selector (graph.h impactFileRollup counts nameOnly only there): via="name" +// when every reached symbol in the file is a via="name" row, by_name=K when K of its syms= are; nothing when none is. +template +inline std::string impactRollupHedgeXml( const RowT& row ) +{ + if( row.nameOnly == 0 ) { return std::string(); } + return row.nameOnly == row.syms ? std::string( " via=\"name\"" ) : " by_name=\"" + std::to_string( row.nameOnly ) + "\""; +} +template +inline std::string impactRollupHedgeJson( const RowT& row ) +{ + if( row.nameOnly == 0 ) { return std::string(); } + return row.nameOnly == row.syms ? std::string( ",\"via\":\"name\"" ) : ",\"by_name\":" + std::to_string( row.nameOnly ); +} + +// idea #6: the reach set's per-file rollup (graph.h impactFileRollup), BEFORE the symbol window: holding one per file. Its own tag (rf), so a reader or gate +// counting the import tier's rows never picks it up, and noun-prefixed counts (the import tier's importers=/ +// shown_importers=/importers_capped= shape), so the root's own shown=/capped= stay the only bare ones in the answer. +// Emitted only when the rollup has rows (it rides beside a cut symbol window only). +template +inline void emitImpactFileRollupXml( std::FILE* out, const IngestResult& ing, const RollupT& r, std::string_view rootPrefix ) +{ + if( r.rows.empty() ) + { + return; + } + std::vector esc; + rw::emitTo( out, "", r.rows.size(), r.shown, r.capped ? "1" : "0", + rw::nextAttrXml( r.next, "files_next" ) ); + for( std::size_t i = 0; i < r.shown; ++i ) + { + 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::emitRaw( out, "" ); +} + +// idea #6: the JSON form of the rollup above — `,"files":{files,shown_files,files_capped[,files_next],rows:[{p,syms,d}]}`, +// the XML element's own noun-prefixed names. Emits nothing when the rollup has no rows, exactly when the element is absent. +template +inline void emitImpactFileRollupJson( std::FILE* out, const IngestResult& ing, const RollupT& r, std::string_view rootPrefix ) +{ + if( r.rows.empty() ) + { + return; + } + rw::emitTo( out, ",\"files\":{{\"files\":{},\"shown_files\":{},\"files_capped\":{}{},\"rows\":[", r.rows.size(), r.shown, + r.capped ? "true" : "false", r.next.empty() ? std::string() : ",\"files_next\":\"" + jsonStr( r.next ) + "\"" ); + for( std::size_t i = 0; i < r.shown; ++i ) + { + 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::emitRaw( out, "]}" ); +} + inline void emitImportRowsJson( std::FILE* out, const IngestResult& ing, std::span files, std::string_view rootPrefix, std::span lazy = {} ) { @@ -9406,6 +10031,7 @@ inline void writeJsonMapHeader( JsonWriter& w, std::string& esc, const JsonMapHe // (every MCP client) reading a truncated ranking with no key to tell it so is the exact defect the XML // side just closed. Same slot, same absent-means-converged rule. w.write( renderDisclosure( h.ann->prDisclosure, DiscloseAs::JsonKeys ) ); + w.write( renderHistoryUnread( DiscloseAs::JsonKeys ) ); // K51: the JSON twin of the root's history_unread= (absent unless the amp= walk was unread) // The JSON twin of the XML root's data_sections_cut= / next= (codeFirstKeep): same keys, same absent-at-zero rule. if( h.dataSectionsCut.cut > 0 ) @@ -9438,6 +10064,64 @@ inline void writeJsonMapHeader( JsonWriter& w, std::string& esc, const JsonMapHe w.write( "]" ); } +// D7: the JSON twin of writeMapInventoryXml — `,"inv":{"listed":L,"unlisted":U,"entry":[{"p":…,"n":…,"c":[…]}], +// "ls":[{"p":…,"n":N,"f":[…]}]}` after the "r" array. The callee objects spell exactly what a "r" row's "c" array does. +template +inline void writeMapInventoryJson( JsonWriter& w, const IngestResult& ing, const MapInventory& inv, const PathRel& pathRel, + const std::vector& outOff, const std::vector& outTargets, + const std::vector* outProv, const std::vector* outNameOnly, + std::string& esc ) +{ + EXPECTS( inv.active, "the caller emits the tier only when it has a cut to inventory" ); + w.write( ",\"inv\":{\"listed\":" ); w.write( std::to_string( inv.listed ) ); + w.write( ",\"unlisted\":" ); w.write( std::to_string( inv.unlisted ) ); + // each guard with its recovery argv (rule 5), as the XML twin prints them + if( inv.entryTotal > inv.entries.size() ) { w.write( ",\"entries_capped\":true,\"entries_total\":" + std::to_string( inv.entryTotal ) + ",\"entries_next\":" ); writeJsonStr( w, inv.entriesNext, esc ); } + if( inv.namesCapped ) { w.write( ",\"names_capped\":true,\"names_total\":" + std::to_string( inv.namesTotal ) + ",\"names_next\":" ); writeJsonStr( w, inv.namesNext, esc ); } + w.write( ",\"entry\":[" ); + for( std::size_t i = 0; i < inv.entries.size(); ++i ) + { + const NodeId id = inv.entries[ i ]; + const Symbol& s = ing.symbols[ id ]; + w.write( i ? ",{\"p\":" : "{\"p\":" ); writeJsonStr( w, pathRel( s.fileId ), esc ); + w.write( ",\"n\":" ); writeJsonStr( w, s.name, esc ); + w.write( ",\"c\":[" ); + for( std::uint32_t e = outOff[ id ]; e < outOff[ id + 1 ]; ++e ) + { + w.write( e > outOff[ id ] ? ",{\"n\":" : "{\"n\":" ); writeJsonStr( w, ing.symbols[ outTargets[ e ] ].name, esc ); + if( outProv && e < outProv->size() && ( *outProv )[ e ] ) + { + w.write( ",\"prov\":" ); writeJsonStr( w, provLabel( ( *outProv )[ e ] ), esc ); + } + if( outNameOnly && e < outNameOnly->size() && ( *outNameOnly )[ e ] != 0 ) + { + w.write( ",\"via\":\"name\"" ); + } + w.write( "}" ); + } + w.write( "]}" ); + } + w.write( "],\"ls\":[" ); + for( std::size_t i = 0; i < inv.dirs.size(); ++i ) + { + const MapInventoryDir& d = inv.dirs[ i ]; + w.write( i ? ",{\"p\":" : "{\"p\":" ); writeJsonStr( w, d.dir, esc ); + w.write( ",\"n\":" ); w.write( std::to_string( d.total ) ); + if( !d.named.empty() ) + { + w.write( ",\"f\":[" ); + for( std::size_t j = 0; j < d.named.size(); ++j ) + { + if( j ) { w.write( "," ); } + writeJsonStr( w, namesplit::afterLast( pathRel( d.named[ j ] ), "/" ), esc ); + } + w.write( "]" ); + } + w.write( "}" ); + } + w.write( "]}" ); +} + // The default-map JSON sibling of serialize() — same rank/order/bucket logic (kept a deliberate, mechanical // duplication per the L2 seam decision: a sibling emitter, not a shared-emission refactor of the XML path, // so the G5 byte-identical-default contract carries zero risk from this addition). Scope: the common single- @@ -9481,7 +10165,8 @@ inline void serializeJson( std::FILE* out, const IngestResult& ing, const std::v std::size_t externalCalls = 0, // Phase 5: same as serialize()'s const std::vector* declinedOut = nullptr, // tier-3 declines: same as serialize()'s std::size_t /*gateDeclinedCalls*/ = 0, // serialize()'s legend clause; JSON carries no legend, so unread here - const std::vector* outNameOnly = nullptr ) // FE-B: same as serialize()'s → "via":"name" + const std::vector* outNameOnly = nullptr, // FE-B: same as serialize()'s → "via":"name" + std::size_t /*stdMemberDeclinedCalls*/ = 0 ) // serialize()'s legend clause; JSON carries no legend, so unread here { const std::size_t S = ing.symbols.size(); const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); @@ -9510,6 +10195,9 @@ inline void serializeJson( std::FILE* out, const IngestResult& ing, const std::v if( !seen[f] ) { seen[f] = 1; fileOrder.push_back( f ); } buckets[f].push_back( id ); } + // D7: the XML twin's inventory, the same selection (mapinventory.h) over the same printed-file set + const MapInventory inventory = mapInventoryWanted( ing, ann ) ? computeMapInventory( ing, outOff, outTargets, seen, pathRel ) + : MapInventory{}; // T1/§H7: SAME byte-model the XML sibling uses, in the same two roles — the fill-order oracle and the // rate source. The REPORTED est_tokens is measured from the emitted bytes in PHASE 2 below, exactly as @@ -9670,7 +10358,12 @@ inline void serializeJson( std::FILE* out, const IngestResult& ing, const std::v } w.write( "]}" ); } - w.write( "]}" ); + w.write( "]" ); + if( inventory.active ) + { + writeMapInventoryJson( w, ing, inventory, pathRel, outOff, outTargets, outProv, outNameOnly, esc ); + } + w.write( "}" ); }; // DEGRADE, one path for both failures: the header FIRST with the MODELLED estimate, the array streamed behind it. const auto emitModelled = [ & ]() @@ -10234,10 +10927,12 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s // knob-honesty-068: a charged continuation is paid from the rows (the XML twin's rule); the floor's unpaid rest rides anyway // and this dialect's own predicate (forLensJsonOverCeiling: bytes past the allowance) labels any overshoot // …only where the caller lets rows pay (payFromRows): otherwise the whole of it rides unpaid on the cut's own rows - const std::size_t cutNextOwed = plan.capped ? cutNextCost : 0u; + const std::size_t 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; if( outCapped ) { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped @@ -10253,6 +10948,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s } cut.continuationRequest = cutNext; cut.continuationUnpaidBytes = cutNextUnpaid; + cut.continuationPaidSurplus = cutNextSurplus; sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for if( cutOut ) { diff --git a/src/situ.h b/src/situ.h index 4fda159eb..6aa77e842 100644 --- a/src/situ.h +++ b/src/situ.h @@ -1434,7 +1434,8 @@ inline void writeTestGateReport( std::FILE* out, const IngestResult& ing, const untestedModscopeLegend( r.untestedModscope > 0 ), // F3: the full clause at N>0, a one-line definition at 0 ( std::string( rw::graphUnindexedLegend( rw::graphGaugeClauses( g ) ) ) // #66: exactly when the root carries the attribute + ( r.declinedCalls > 0 ? rw::kDeclinedCallsTestGateLegend : "" ) // exactly when it carries declined_calls= - + rw::declinedCallsGateLegend( r.declinedCalls > 0 && g.gateDeclinedCalls > 0 ) ).c_str(), + + rw::declinedCallsGateLegend( r.declinedCalls > 0 && g.gateDeclinedCalls > 0 ) ).c_str(), // the short form above makes no + // claim the C++ standard-member gate's declines would contradict, so its clause stays off this budgeted legend rw::rootRelPathsLegend( !tgRootAttr.empty() ) ); // §P11.4: this gate EXITS 4 on the obligation, so its rows carry the command that discharges it — where // one is derivable. Absent run= = not derivable (testmap.h states why a fallback would be a lie). diff --git a/src/taskroute.h b/src/taskroute.h index 85f6435db..820df9003 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -15,6 +15,7 @@ #include "infra/jsonesc.h" // shSingleQuote — the repository's canonical POSIX argv quoting #include "model.h" // IngestResult +#include "queryshape.h" // detail::lowerAscii — the one ASCII lowercaser (K50) #include "query.h" // isKnownLayerWord — the layer vocabulary --verify enforces at evaluation #include "sarif.h" // rootRelativeUri / rootPrefixOf — the ONE root-relative path rule the map emits with #include "verify.h" // parseClaim — the SHIPPED claim grammar; the router never re-implements it @@ -62,16 +63,10 @@ struct TaskRouteResult int margin = 0; }; -inline std::string lowerAscii( std::string_view text ) -{ - std::string out; - out.reserve( text.size() ); - for( const unsigned char c : text ) - { - out.push_back( char( std::tolower( c ) ) ); - } - return out; -} +// K50: ONE ASCII lowercaser for the tree's task-text readers. This file used to spell its own loop (`for( const unsigned char c : +// text )`, an implicit sign change on every byte >= 0x80 that aborted the sanitizer build on a non-ASCII prompt), and the fix for it +// made that loop byte-identical to queryshape.h's, which --quality-delta flagged as a clone of the reused helper. It is that one now. +using queryshape::detail::lowerAscii; // "is this word one of these?" — the shape four tables' predicates were each writing out by hand, which is // how a five-line any_of becomes a duplication finding against its own neighbours. @@ -447,8 +442,9 @@ inline SymbolMention symbolMention( std::string_view task, std::string_view lowe // fall through to the weak tier, where Section-kind symbols are never weak evidence (weakEvidenceKind), // so the name could not resolve even when explicitly backtick-marked in the task text (backticks are // step 1 of identifierMentionShape, itself unreachable without this). - const bool hasMark = std::any_of( name.begin(), name.end(), []( const unsigned char c ) + const bool hasMark = std::any_of( name.begin(), name.end(), []( const char ch ) { + const unsigned char c = static_cast( ch ); // explicit narrowing (K50): a byte >= 0x80 must not convert implicitly return std::isupper( c ) || c == '_' || c == ':' || c == '.' || c == '$'; } ); if( hasMark ) @@ -626,7 +622,7 @@ inline std::string firstFileLineToken( std::string_view task ) const std::string_view lineDigits = token.substr( colon + 1 ); const std::string_view fileGuess = token.substr( 0, colon ); const bool allDigits = !lineDigits.empty() && std::all_of( lineDigits.begin(), lineDigits.end(), - []( const unsigned char c ) { return std::isdigit( c ) != 0; } ); + []( const char c ) { return std::isdigit( static_cast( c ) ) != 0; } ); // explicit narrowing (K50) if( allDigits && looksLikeFileToken( fileGuess ) ) { return std::string( fileGuess ) + ":" + std::string( lineDigits ); diff --git a/src/verbs_change.h b/src/verbs_change.h index b8d3afe04..f3d8b6756 100644 --- a/src/verbs_change.h +++ b/src/verbs_change.h @@ -688,6 +688,13 @@ inline constexpr int kRunTraceExitCommandFailed = 4; // H7 (capture-audit 2026-09-04; hosts added at the wave-3 close): the ONE sentence for a --stray-content=SUBSTR // filter that selected no ref NAME — spoken by the bare verb ("" host) and by its two hosts ("--plan: ", "--abi: "). // refs="0" at exit 0 would read as "no branch carries stray work", the most reassuring answer these verbs give. +// K75: the blob batch behind a cross-branch verb never started or died part-way. The refusal names the cause where the reader +// looks (stderr, exit 1) — the other answer was merged="1" / "no break" for every branch, a measured-looking zero made of nothing. +inline void printBlobsUnreadRefusal( const char* hostPrefix ) +{ + DISCLOSE( Diagnostics::answerRefused, "the blob batch behind the cross-branch sweep did not serve every blob, so no verdict is printed - stderr names the cause, exit 1" ); + rw::emitTo( stderr, "ripwire: {}{} - refusing rather than print verdicts nothing measured\n", hostPrefix, rw::crossref::blobsUnreadCause( rw::crossref::UnreadBlobBatch::Branches ) ); +} inline void printStrayFilterNoMatch( const char* hostPrefix, std::string_view filter ) { rw::emitTo( stderr, "ripwire: {}--stray-content={} matches no local ref — a zero here would be a failure, not a " @@ -1416,6 +1423,10 @@ int runAbiCheck( const MainDispatch& d ) { printStrayFilterNoMatch( "--abi: ", d.cfg.strayFilter ); // H7, the host's spelling (wave-3 close) } + else if( result.blobsUnread ) + { + printBlobsUnreadRefusal( "--abi: " ); + } else { rw::emitTo( stderr, "ripwire: --abi: more than {} refs match — narrow it with --stray-content=SUBSTR\n", crossref::kMaxRefs ); @@ -1465,6 +1476,10 @@ std::optional runCrossRef( const MainDispatch& d ) { printStrayFilterNoMatch( "--plan: ", cfg.strayFilter ); // H7, the host's spelling (wave-3 close) } + else if( result.blobsUnread ) + { + printBlobsUnreadRefusal( "--plan: " ); + } else { rw::emitTo( stderr, "ripwire: --plan: more than {} refs match — narrow it with --stray-content=SUBSTR\n", crossref::kMaxRefs ); @@ -1499,6 +1514,10 @@ std::optional runCrossRef( const MainDispatch& d ) // branch carries stray work" — from a sweep that matched no branch NAME at all. printStrayFilterNoMatch( "", cfg.strayFilter ); } + else if( result.blobsUnread ) + { + printBlobsUnreadRefusal( "--stray-content: " ); // K75 + } else { rw::emitTo( stderr, "ripwire: --stray-content: more than {} refs match — narrow it with --stray-content=SUBSTR\n", crossref::kMaxRefs ); @@ -1517,6 +1536,11 @@ std::optional runCrossRef( const MainDispatch& d ) const crossref::EvalReport rep = crossref::evalStray( root, std::string( cfg.evalStray ) ); if( !rep.ok ) { + if( rep.blobsUnread ) + { + printBlobsUnreadRefusal( "--eval-stray: " ); // K75 + return 1; + } if( !rep.badRefs.empty() ) { // H13: the file read fine and this IS a git repo — the refusal is that one or more @@ -1605,14 +1629,26 @@ std::optional runCrossRef( const MainDispatch& d ) // §A7: HEAD's rows are documented as the PARSED answer, so hand the tree scan what the index knows. const std::vector indexDefs = crossref::whereisIndexDefSites( d.ing, whereisSel, root ); + // --detail lifts every cap of this verb: the row cap below, the runaway guard on the other refs' blob parse and the t= preview. + const crossref::WhereisParseBudget parseBudget = cfg.detail ? crossref::WhereisParseBudget{ SIZE_MAX, SIZE_MAX } : crossref::WhereisParseBudget{}; crossref::WhereResult result = crossref::computeWhereis( root, whereisSel, cfg.strayFilter, - crossref::WhereisEvidence{ cfg.withHistory ? &history : nullptr, indexDefs } ); + crossref::WhereisEvidence{ cfg.withHistory ? &history : nullptr, indexDefs }, parseBudget ); if( !result.ok ) { rw::emitTo( stderr, "ripwire: --whereis: {} is not a git repository (or has no HEAD commit) — no refs to search\n", root.c_str() ); return 1; } + if( result.blobsUnread ) + { + DISCLOSE( Diagnostics::answerRefused, "whereis: the blob stream did not serve every blob, so no hit count is printed — stderr names the cause, exit 1" ); + rw::emitTo( stderr, "ripwire: --whereis: {} — refusing rather than print a hit count nothing measured\n", + crossref::blobsUnreadCause( crossref::UnreadBlobBatch::Trees ) ); // K75's one renderer: the sweep's words, this verb's noun + return 1; + } result.seedSpec = std::move( whereisSeed ); + // --whereis-full-text prints every t= whole and lifts nothing else (the of a page that printed a + // preview names it); --detail lifts the preview with the row cap and the parse guard. + result.textWhole = cfg.detail || cfg.whereisFullText; // The tree zero stays an answer; the near-miss only says WHICH zero it is (a name this repo never // had, or a keystroke away from one it has). Computed only on the zero, so a real hit list is // byte-identical. diff --git a/src/verbs_for.h b/src/verbs_for.h index ddcfa33fc..a14ab77a9 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -556,6 +556,11 @@ struct ForLensHeaderParts bool viaPresent = false; // FE-B: may a row of this bundle carry via="name"? Some head row // (the same row head modScopePresent reads) has a name-only out-edge // (serialize.h namesOnlyOutAny). Over-approximates the 16-per-symbol cap. + bool ownerPresent = false; // owner-hop: the compact carries a qword= row or a qword_cut= disclosure + // (forOwnerHopPlan, decided before the header, cleared with the section) + 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 @@ -574,6 +579,17 @@ struct ForLensHeaderParts 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; + } }; // Splice a pre-formatted fragment in front of a structural boundary, or leave the document exactly as it @@ -698,7 +714,9 @@ using rw::deriveForConfidence; // // 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. +// 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). @@ -726,7 +744,8 @@ inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, { note += ", " + identity; } - note += " and r=/tail (total= shown= capped=) dropped (ceiling) - the attributes stay; a wider token-budget defines them]"; + 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; } @@ -785,6 +804,41 @@ inline constexpr std::string_view kForCompactLegendConfidence = "; confidence=/margin_pct= head score drop (low=flat)"; inline constexpr std::string_view kForCompactLegendHops = "; h l= p= n=, c n= l= (joined for same-named callees, shown= counts them), noedge= no callee resolved"; +// owner-hop: present-only, right after the hops clause, on the same plan the rows come from. Says it is BY NAME (a lookup in +// the ranked head, never a resolution), widens noedge= to the owners, and defines the two disclosures an owner cut carries. +// What qword= does NOT mean: that the row is the only, or the true, implementation of the question's subject. +inline constexpr std::string_view kForCompactLegendOwner = + "; h qword= its name (case-folded, leading _#$@ dropped) equals that question word (by name, not resolved, not the only one), first, calls to 100 (capped=1 next= all); " + "qword_cut= owners not so served, next= the first"; +// owner-hop: the full dialect's reading of the same two attributes (present-only, after the clause above; no "--": a comment) +inline constexpr std::string_view kForOwnerHopLegend = + "; hops row qword= the question word that row's NAME equals (ASCII case-folded, leading _ # $ @ dropped; by name in the " + "ranked head, a lookup, not a resolution; it " + "does not mean the symbol is the only or the true implementation): such an owner row comes first (a word on a short " + "common-verb list such as get, set, run or write ranks after the other owner words) and its calls child " + "lists up to 100 callee names (capped=1 with next= naming the full list when the guard or the budget cut it); an owner " + "with no resolved callee is counted in noedge= like any ranked symbol. hops qword_cut=N: owners not served as a qword " + "row (the three-row cap or the budget; one of the six ranked candidates still keeps its plain row), next= the first"; +// …and the one question both dialects ask: the clause in this dialect, or nothing (ownerPresent is set only on the compact +// route and cleared with the section, so it never rides without the hops clause in front of it). +inline std::string_view ownerHopLegend( const ForLensHeaderParts& p, bool compact ) noexcept +{ + if( !p.ownerPresent ) + { + return {}; + } + return compact ? kForCompactLegendOwner : kForOwnerHopLegend; +} +// …and what the sig-trim ledger exempts for the owner rows in this dialect: the qword clause, plus the via="name" clause when +// the owner rows alone switched it on (a base answer without them never carried it, so charging it cost a row: +// fastify-13 Context r23, pdoc AstInfo r38). ownerPresent and viaPresent are cleared with their sections, so neither part +// is exempted for a clause the header did not write. +inline std::size_t ownerLegendExemptBytes( const ForLensHeaderParts& p, bool compact ) +{ + const bool viaByOwner = p.viaPresent && p.viaByOwnerOnly; + const std::size_t viaBytes = !viaByOwner ? 0u : ( compact ? rw::kForCompactViaNameClause.size() : rw::forViaNameClause().size() ); + return ownerHopLegend( p, compact ).size() + viaBytes; +} inline constexpr std::string_view kForCompactLegendBodies = "; b t= n= p= l= full bodies, c n= l= callee signatures"; inline constexpr std::string_view kForCompactLegendTail = @@ -894,6 +948,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 // 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 @@ -1042,6 +1097,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 if( p.tailLegend ) { h.append( rw::kForFileTailLegend ); // deep-tail: defines r= and the element (sigs-charge-exempt, serialize.h) @@ -1436,6 +1492,32 @@ 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 ) +{ + 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; +} + +// 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 +{ + return { compact ? std::string_view() : base.echoDropped, compact ? kCompactRouteDroppedNote : base.echoAndRouteDropped, lastRung }; +} + // 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 @@ -1934,6 +2016,52 @@ std::size_t forSigSideCeiling( bool autoBundleMode, int packTopN, std::size_t bu 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 +// --detail still takes whatever the ceiling leaves, exactly as before. 0 when there is no servable first body. +inline constexpr std::size_t kForDetailFloorShareDivisor = 4; // the floor's cap = bundleBudget / this +std::size_t forDetailFirstBodyFloor( const rw::IngestResult& ing, const std::vector& detailIds, std::size_t bundleBudget ) +{ + for( const rw::NodeId id : detailIds ) // packBodies' own skip rule: an invalid id or a module scope is no body + { + if( id >= ing.symbols.size() || ing.symbols[id].kind == rw::SymKind::ModuleScope ) + { + continue; + } + const rw::Symbol& s = ing.symbols[id]; + const std::size_t raw = s.endByte > s.sigStartByte ? s.endByte - s.sigStartByte : 0; + return std::min( raw, bundleBudget / kForDetailFloorShareDivisor ); + } + return 0; +} + // The auto-body candidate head: the top-kPackTaskBodyCandidates positive-score rows of the ranked // surface — the same "top heads with a positive score" rule packTaskBundleText applies, on the same // (score desc, id asc) order sigs selected with. @@ -2209,6 +2337,233 @@ inline std::vector forCompactHopIds( const std::vector& } return hopIds; } +// ── THE QUESTION'S OWNER (lane owner-hop-068) ───────────────────────────────────────────────────────────────────── +// The hop candidates above are the six best-RANKED rows, and a question that names the function it is about ("How does +// the JWT middleware verify a token?" -> jwt(), rank 12 behind a dozen symbols that repeat the question's other words) +// never got that function's callees — the graded gold items of two round-1 rows (getCookie, HTTPException, set; +// validateAsync*). An OWNER is a symbol of the ranked pool (the first kForLensDefaultTopN rows with a positive score) that +// * is a callable DEFINITION — a function or method with a body (a C prototype and its body are one entity: the +// body's row is the owner, the prototype is no second one) — and not a test symbol (rw::isTestSymbol); +// * has a name that, with leading _ # $ @ removed and ASCII case folded, EQUALS a whole word of the question (words +// split on every byte that is not a letter, digit, '_' or a non-ASCII byte; length >= 3; not a route stopword) — +// whole name only: "log" never makes catalog or dialog owners; `__init__` folds to init__ and matches no word; +// * names a word with at least one such definition inside the head tier (rank <= kForDocExcerptRankCount). The gate +// is per WORD: once a word qualifies, EVERY same-named definition in the pool is an owner, each its own row (two +// classes' _notify are two entities, never one picked silently). +// This is a NAME lookup over the ranked head, not a resolution: qword= says the name equals the word, never that the +// symbol is the only or the true implementation of the subject. The case fold matches the question's word only; it never +// merges two entities. Language-neutral by construction: a name, a kind, a body span and the test partition are what +// every indexed language already sets. +// SLOTS: owners with a proven callee edge (the hop-slot rule, rw::hopSlotHasProvenEdge) take up to kForOwnerHopRows rows +// in rank order — except that a word on kOwnerCommonWords (verbs and function words a question USES: get, then, write …) +// ranks after every other word, so "how does the gateway get and then write the token" seats gateway first (gate (V)). +// The rest are disclosed (qword_cut= + next=). An owner with no proven edge never takes a slot; it is counted in noedge= +// 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" }; + +struct ForOwnerPlan +{ + std::vector rowIds; // the owner rows, slot order (<= kForOwnerHopRows) + std::vector rowWords; // the folded question word of each row + std::vector noEdgeIds; // owners with no proven edge that are NOT among the six candidates + std::size_t capCut = 0; // owners with a proven edge past the row cap + rw::NodeId firstCapCut = rw::kNoNode; + bool any() const noexcept { return !rowIds.empty() || capCut > 0; } // a qword row or a qword_cut= disclosure (the legend's bit) + bool changesNodes() const noexcept { return any() || !noEdgeIds.empty(); } // …or an edgeless owner to count in noedge= +}; + +// What the --for header already decided for the enrichment section it introduces: FE-B's "the header defines +// via=\"name\"" (ForLensHeaderParts::viaPresent) and the owner plan its qword clause read. One aggregate, so the section +// builders render exactly what the header promised without a parameter per promise. +struct ForSectionPromises +{ + bool viaLegendInHead = false; + const ForOwnerPlan* owners = nullptr; // nullptr ⇒ no owner rows (an empty plan reads the same) +}; + +// 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 ) +{ + ForOwnerPlan plan; + if( std::getenv( "RIPWIRE_NO_OWNER_HOP" ) != nullptr ) + { + 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 ) + { + if( !rw::hopSlotHasProvenEdge( g.outOff, g.outNameOnly, o.id ) ) + { + if( std::ranges::find( hopIds, o.id ) == hopIds.end() ) + { + plan.noEdgeIds.push_back( o.id ); // counted in noedge= once; a candidate already is + } + continue; + } + if( plan.rowIds.size() < kForOwnerHopRows ) + { + plan.rowIds.push_back( o.id ); + plan.rowWords.push_back( std::move( o.word ) ); + continue; + } + if( plan.capCut++ == 0 ) + { + plan.firstCapCut = o.id; + } + } + ENSURES( plan.rowIds.size() == plan.rowWords.size() && plan.rowIds.size() <= kForOwnerHopRows, "one word per seated owner, at most the cap" ); + return plan; +} + +// The node list with the owner plan applied: owner rows first, then the six candidates minus the owners MOVED out +// of them (moved, never duplicated), then the edgeless owners that are not candidates (each counted in noedge= once). +inline std::vector forOwnerHopNodes( const ForOwnerPlan& plan, const std::vector& hopIds ) +{ + std::vector nodes = plan.rowIds; + for( rw::NodeId id : hopIds ) + { + if( std::ranges::find( plan.rowIds, id ) == plan.rowIds.end() ) + { + nodes.push_back( id ); + } + } + nodes.insert( nodes.end(), plan.noEdgeIds.begin(), plan.noEdgeIds.end() ); + return nodes; +} + +// 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). +inline rw::HopOwners forHopOwners( const rw::Config& cfg, const rw::IngestResult& ing, const ForOwnerPlan& plan, + std::size_t leftBytes, std::string_view rootArg ) +{ + rw::HopOwners owners; + owners.rows = plan.rowIds.size(); + owners.words.assign( plan.rowWords.begin(), plan.rowWords.end() ); + owners.capCut = plan.capCut; + owners.firstCapCut = plan.firstCapCut; + if( cfg.tokenBudget == 0 || !plan.any() ) + { + return owners; + } + std::vector esc; + const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); + std::size_t cutReserve = 0; // ' qword_cut="NNN" next="--callees=' + the longest target + '"' + std::vector cuttable = plan.rowIds; + if( plan.firstCapCut != rw::kNoNode ) + { + cuttable.push_back( plan.firstCapCut ); + } + for( rw::NodeId id : cuttable ) + { + const rw::Symbol& s = ing.symbols[ id ]; + const std::string_view rel = rootArg.empty() ? std::string_view( ing.files[ s.fileId ] ) + : rw::sarif::rootRelativeUri( ing.files[ s.fileId ], rootPrefix ); // packHops' pathRel + cutReserve = std::max( cutReserve, rw::ownerCalleesNext( ing, s, rel, rootArg, esc ).size() + 28 ); + } + owners.budgetBytes = leftBytes > cutReserve ? leftBytes - cutReserve : 1; // 1, never 0 (0 reads as "ride on top") + owners.fundFirst = true; + return owners; +} + // --detail=N's bodies: the top min(N, forTopN) symbols by (score desc, id asc) — the order the sigs use. One spelling, read // by the --detail section and by the header's via="name" question (FE-B). inline std::vector forDetailIds( const rw::IngestResult& ing, const std::vector& lensRank, int detail, int forTopN ) @@ -2224,11 +2579,87 @@ inline std::vector forDetailIds( const rw::IngestResult& ing, const return ids; } +// fix round 4 (rule 5, review rv-wrap-recipe-k29 F1): the compact section an exhausted explicit ceiling +// leaves out, as a counted marker instead of nothing — ``. +// total= counts the CANDIDATE head the section covers (ids the ingest knows) — not the owner rows: at a spent ceiling the +// answer must stay byte-identical with and without the owner-hop plan (ownerhopcheck (B2), owner-hop's signed contract: +// "owner rows add nothing"), and the next= answer serves those owner rows too. How many candidates have proven edges +// (noedge=) is not known without the render, so the marker does not claim it. next= is the same question without +// --token-budget, whose own compact allowance serves the hop rows. No candidates => no marker (nothing was cut). Markup +// bytes, charged through buildForEnrichment's markupBytes like the rendered section. +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. +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 +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; + std::size_t total = 0; + for( const rw::NodeId id : hopIds ) + { + total += id < ing.symbols.size() ? 1u : 0u; + } + if( total == 0 ) + { + return marker; + } + marker.xml = ""; + marker.tokens = rw::tokensForEmittedBytes( marker.xml.size(), rw::kBytesPerTokenDefault ); + marker.isRendered = true; + return marker; +} + +// fix round 4 (rule 5): an explicit-budget the budget CUT names the call that serves the rest. A cut with no +// next= of its own (the qword_cut= disclosure already carries one) gets ` next=` spliced into the open tag; the bytes +// were held back from the rows' budget (buildForCompactHops' rowRoom), so the section stays inside what it was given. +// A rendered section with shown="0" capped="1" is spliced the same way and then reads as the exhausted-ceiling marker +// does (counted, continued). An uncut section (capped="0") is untouched. +void forHopsCutDisclose( rw::ChargedSection& section, std::string_view cutNext ) +{ + std::string& x = section.xml; + const std::size_t end = x.find( '>' ); + if( !section.isRendered || end == std::string::npos || x.compare( 0, 5, " 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 ); return out; } // the surface allowance MINUS this surface's own fixed disclosure — see kForCompactSurfaceBudgetBytes. constexpr std::size_t kCompactFixedBytes = kForCompactBundleLegend.size() + kCompactAttrReserve + kCompactWrapReserve; - const std::size_t hopBudget = std::min( leftBytes, rw::kForCompactSurfaceBudgetBytes > kCompactFixedBytes - ? rw::kForCompactSurfaceBudgetBytes - kCompactFixedBytes - : std::size_t( 1 ) ); + // 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 ) ); + + // 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). + const std::vector hopNodes = forOwnerHopNodes( ownerPlan, hopIds ); + const rw::HopOwners owners = forHopOwners( cfg, ing, ownerPlan, leftBytes, fcRootArg ); out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packHops( f, ing, hopIds, hopBudget, g.outOff, g.outTargets, g.outNameOnly, redactPtr, /*outShown=*/nullptr, &lensRank, fcRootArg, - viaLegendInHead ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) + { 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) // 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 @@ -2284,8 +2728,12 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest out.section = rw::ChargedSection{}; return out; } - out.attr = hopIds.empty() ? " bundle=\"compact\" bodies=\"0\" reason=\"no_candidates\"" + out.attr = hopNodes.empty() ? " bundle=\"compact\" bodies=\"0\" reason=\"no_candidates\"" : " bundle=\"compact\" bodies=\"0\" reason=\"compact-route\""; + if( !cutNext.empty() ) + { + forHopsCutDisclose( out.section, cutNext ); + } return out; } @@ -2300,12 +2748,12 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR const std::vector& lensSurfaceIds, const std::vector& lensRank, const ForEnrichmentPlan& plan, const std::vector& anchorDefs, rw::RedactCounts* redactPtr, std::size_t committedBytes, std::size_t bundleBudget, - bool viaLegendInHead ) // FE-B: the header defines via="name" (ForLensHeaderParts::viaPresent) + const ForSectionPromises& promised ) // FE-B via="name" in the head + the owner plan { const std::size_t committed = committedBytes + plan.attrReserve; ForAutoBodiesResult out = plan.compact - ? buildForCompactHops( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, viaLegendInHead ) - : buildForAutoBodies( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, anchorDefs, viaLegendInHead ); + ? buildForCompactHops( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, promised ) + : buildForAutoBodies( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, anchorDefs, promised.viaLegendInHead ); if( plan.compact ) { out.markupBytes = out.section.xml.size(); @@ -2322,8 +2770,11 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR // 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. inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t tokenBudget, - std::size_t bundleBudget, std::size_t spentBytes ) + std::size_t bundleBudget, std::size_t spentBytes, std::string_view cutNextAttr = {} ) { std::vector esc; if( tokenBudget == 0 ) @@ -2331,8 +2782,16 @@ inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t t return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc ); } const std::size_t tailAllowed = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, - rw::kForFileTailShellReserve ); - return rw::renderFileTailXml( tail, rw::fileTailShownForBudget( tail, tailAllowed, esc ), esc ); + rw::kForFileTailShellReserve + cutNextAttr.size() ); + return rw::renderFileTailXml( tail, rw::fileTailShownForBudget( tail, tailAllowed, esc, cutNextAttr ), esc, cutNextAttr ); +} + +// 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, @@ -2371,13 +2830,65 @@ inline std::string renderForHdrRowsXml( const rw::IngestResult& ing, const std:: return x; } +// the CLI half of the shared page (forpage.h owns the doc): write it to stdout. One implementation, +// three dialects — the CLI bundle verbs and the MCP twins all build the SAME document. +inline int emitForCandidatePage( const rw::IngestResult& ing, const std::vector& lensRank, const rw::AdaptiveCut& forCut, + const rw::ForCandidatePageReq& req ) +{ + // the CLI half of the shared page (forpage.h owns the doc): write it to stdout. One + // implementation, three dialects — the CLI bundle verbs and the MCP twins all build the SAME + // document. rw::emitRaw (#362 review's ask): the repo's own stdout writer, not the raw stream call. + const std::string doc = rw::forCandidatePageDoc( ing, lensRank, forCut, req ); + rw::emitRaw( stdout, doc.c_str() ); + return 0; +} + +// B2 (#362 review round 4): the argv fragment of every accepted flag that changes the lens ranking, +// echoed into the page's next= so a pasted continuation re-ranks identically. The bundle-shaping +// set (--adaptive/--anchor/--signatures-only/…) is REFUSED beside a window (cli.h), so it never +// appears here; these four are accepted because they select the ranking, not the bundle's shape. +inline std::string forPageRankFlagArgv( const rw::Config& cfg ) +{ + std::string s; + if( cfg.noRoute ) { s += " --no-route"; } + if( cfg.noMentionBoost ) { s += " --no-mention-boost"; } + if( cfg.noDocMention ) { s += " --no-doc-mention"; } + if( cfg.cochangeBoost ) { s += " --cochange-boost"; } + return s; +} + +// #362 review round 4, follow-up (quality bar): the windowed-page predicate, named once — the +// inline spelling appeared twice (runForLens's bundle path, runPackTask's B1 gate) and the second +// read pushed runPackTask past its complexity bar. +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). -std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bool sigsNextPayFromRows, bool* sigsNextTryOther ) +// `forAtStamp` (train 26c, CodeRabbit on #383): the at= stamp, read ONCE per run by runForLens — every render shares it. +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; const Graph& g = d.g; @@ -2450,7 +2961,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // positive-score file, ranked file-first (forpage.h says how), the follow-up the routing-loop ladder // showed completes answers where a body cannot. It owns its own root, so it bypasses the whole // bundle below; cli.h refuses every bundle-shaping flag beside it rather than ignoring one. - if( cfg.pageLimit > 0 || cfg.pageOffset > 0 ) + // PAGING-POC (issue #294): under --token-budget an explicit window is NOT the file page — it is + // the BUDGETED-BUNDLE CANDIDATE PAGE, served by the shared inline below. The file + // page keeps every windowless-budget call exactly as forwidencheck pins it. + if( ( cfg.pageLimit > 0 || cfg.pageOffset > 0 ) && cfg.tokenBudget == 0 ) { const ForFilePage page = computeForFilePage( ing, lensRank, lr.evidence ); const std::string pageXml = renderForFilePageXml( ing, page, ForPageRenderParts{ cfg.forTask, ctxRootOpen( cfg.forTask, routeNoteRaw, flRootArg ), @@ -2593,6 +3107,22 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo } ForConfidence forConf = deriveForConfidence( forCut, forTopN, forHomonymDecline ); + + // ── THE BUDGETED-BUNDLE CANDIDATE PAGE (paging PoC, issue #294): budget + explicit window ⇒ the + // resumable candidate page (shared inline; the un-paged bundle below stays byte-identical). + if( cfg.tokenBudget != 0 && ( cfg.pageLimit > 0 || cfg.pageOffset > 0 ) ) + { + // W3-N1/#294 review: the page redacts through packSignatures — the tally must be reported, + // exactly like the --json early return below (a redacted credential must never be silent). + const std::string rankFlags = forPageRankFlagArgv( cfg ); + const int pageRc = emitForCandidatePage( ing, lensRank, forCut, rw::ForCandidatePageReq{ cfg.forTask, "--for=", rankFlags, + /*pasteHandle=*/true, /*compactLegend=*/cfg.legend != "full", + routeNoteRaw, flRootArg, redactPtr, + flSingleRoot ? std::string_view( gitstamp::stampAt( std::string( root ) ) ) : std::string_view(), + cfg.tokenBudget, cfg.pageLimit, cfg.pageOffset } ); + reportRedactions( stderr, redactCounts ); // W3-N1: the page redacts — the tally reports + return pageRc; + } // L-W: coverage= rides the SAME sentence and the SAME byte exemption as confidence=/margin_pct= — but ONLY // on a THIN answer (owner decision 2026-09-12: present-only). The thin verdict is decided HERE, from the // resolved surface above and the top symbol's term share, and it drives three things at once: the @@ -2677,9 +3207,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // flRootArg itself uses): "" on multi-root or a non-git root, same silence-means-omitted convention // every other stamped verb follows. ALWAYS present when non-empty, like confidence= — a root fact, // never budget-dropped, which is why it rides the SAME byte-exempt reserve as confidence= below. - // Computed once as the raw stamp (both dialects need it: XML wraps it as an attribute, JSON quotes - // it bare) so a single --token-budget run pays exactly one extra git subprocess, not two. - const std::string forAtStamp = flSingleRoot ? gitstamp::stampAt( root ) : std::string(); + // Computed once per RUN, in runForLens (both dialects need it: XML wraps it as an attribute, JSON quotes + // it bare), so a --token-budget run that renders in up to three payment modes pays the stamp's two git + // subprocesses once, not once per render (CodeRabbit on #383, verbs_for.h:3843; test/forrankordercheck.sh (10c)). const std::string forAtAttrStr = forAtStamp.empty() ? std::string() : ( " at=\"" + forAtStamp + "\"" ); // R2-AF (round 2, S4): resolved once, ahead of the header build below, so its legend clause can be @@ -2794,16 +3324,28 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // node set is known before the header: the compact route's hop ids, T3's auto-body candidates, or --detail's head // (autoBundleMode excludes --detail). Their name-only out-edges decide it — over-approximating only the byte ladder // and the 16-per-symbol cap inside those sections, never the rows the header itself lists. + // owner-hop: THE QUESTION'S OWNER plan (forOwnerHopPlan), decided here, before the header, because the header's + // qword clause and its via="name" question both read it; the compact route's renders exactly this plan. + const ForOwnerPlan ownerPlan = ( autoBundleMode && plan.compact ) + ? forOwnerHopPlan( ing, g, cfg.forTask, lensSurfaceIds, lensRank, forCompactHopIds( lensSurfaceIds, lensRank ) ) + : ForOwnerPlan{}; 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() ) { std::vector viaIds; if( autoBundleMode && plan.compact ) { - viaIds = forCompactHopIds( lensSurfaceIds, lensRank ); // only the hops packHops gives a slot can carry a row (the hop-slot rule drops a hop whose every callee // edge is name-only, so its via="name" rows never print) - std::erase_if( viaIds, [ & ]( rw::NodeId id ) { return !rw::hopSlotHasProvenEdge( g.outOff, g.outNameOnly, id ); } ); + const auto unslotted = [ & ]( rw::NodeId id ) { return !rw::hopSlotHasProvenEdge( g.outOff, g.outNameOnly, id ); }; + viaIds = forCompactHopIds( lensSurfaceIds, lensRank ); + std::erase_if( viaIds, unslotted ); + const bool rankedVia = rw::namesOnlyOutAny( g.outOff, g.outNameOnly, viaIds ); // the question without owners + std::vector ownerViaIds = ownerPlan.rowIds; // owner rows carry too + std::erase_if( ownerViaIds, unslotted ); + forViaByOwnerOnly = !rankedVia && rw::namesOnlyOutAny( g.outOff, g.outNameOnly, ownerViaIds ); + viaIds.insert( viaIds.end(), ownerViaIds.begin(), ownerViaIds.end() ); } else if( autoBundleMode && plan.autoBodies ) { @@ -2821,7 +3363,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo cfg.anchor, plan.autoBodies, plan.compact, cfg.legend == "compact", /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg, /*hdrLegend=*/!forHdrRows.empty(), forScPresent, forComposePresent, - forLegoPresent, forLayerPresent, forModScopePresent, forEndLinePresent, forViaPresent }; + forLegoPresent, forLayerPresent, forModScopePresent, forEndLinePresent, forViaPresent, + ownerPlan.any(), forViaByOwnerOnly }; 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, {} ); @@ -3093,8 +3636,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // admits are the ones it admitted without it (owner ruling: e= is exempt from the row budget). const std::size_t endLineLegendEmitted = !headerParts.endLinePresent ? 0u : ( compactLegendOn ? rw::kForCompactEndLineLegend.size() : rw::kForEndLineLegend.size() ); + // owner-hop: the qword clause — and the via="name" clause when only the owner rows switched it on — are disclosures + // 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 ); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted - + endLineLegendEmitted; + + endLineLegendEmitted + ownerLegendEmitted; if( exemptBytes > headerStr.size() ) { DISCLOSE( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); @@ -3118,7 +3665,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo const std::size_t fullExempt = adaptiveNote.size() + enrichmentLegendBytesEmitted( plan, false ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes() - + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ); + + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ) + + ownerLegendExemptBytes( headerParts, /*compact=*/false ); 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 ); @@ -3126,12 +3674,71 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // 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 } - const std::size_t fixedBytes = chargedHeaderBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" + // 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) + 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 std::size_t sigsBudget = sigSideCeiling > fixedBytes ? sigSideCeiling - fixedBytes : 1; // ≥1: 0 would mean "no budget" + + // §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). + rw::ChargedSection graphSection, detailSection; + if( cfg.withGraph ) + { + graphSection = rw::chargeSection( [ & ]( std::FILE* f ) { packGraphBlock( f, ing, lensRank, g.outOff, g.outTargets ); }, + rw::kBytesPerTokenDefault ); + if( !graphSection.isRendered ) + { + // the block streams uncharged below: the lens's own contract omits an est_tokens that left it out + 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 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 @@ -3204,7 +3811,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo 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 }; + sigsNextPayFromRows, /*overrunBytes=*/forSigsNextCharged ? mode.overrunReserveBytes : 0u }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation @@ -3346,45 +3953,34 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // whichever state they are already in (post-narrow on the happy path, since narrowing already ran // above; un-narrowed on the degrade path, since narrowing never runs there either) — the same state // each path's own render (buffered or direct-to-stdout) would use. - const bool sectionsWantLego = rw::sectionsWant( cfg.sections, "lego" ); - const bool sectionsWantCompose = rw::sectionsWant( cfg.sections, "compose" ); const std::size_t legoStubTotal = legoPreRendered ? legoPreCapCount : rw::legoPreCapRowCount( ing, legoScoped ); const std::size_t composeStubTotal = composePreRendered ? composePreCapCount : rw::composePreCapRowCount( ing, g.composeEdges, lensSurfaceIds ); - const bool legoHasContent = legoPreRendered ? !legoStr.empty() : legoStubTotal > 0; - const bool composeHasContent = composePreRendered ? !composeStr.empty() : composeStubTotal > 0; - // CANDIDATE for collapse — has content, and --sections did not already opt it back in. Round 1 - // stubbed every candidate unconditionally; round 2 (below) additionally PRICES it. - const bool legoCandidate = legoHasContent && !sectionsWantLego; - const bool composeCandidate = composeHasContent && !sectionsWantCompose; - // the ONE restoring invocation, built once and used at every site (the size-gate probe below, the - // buffered substitution, and the degrade-path fallback further down) that needs it — never a second, - // differently-spelled build. - std::string sectionsNextInvocation; - if( legoCandidate || composeCandidate ) - { - sectionsNextInvocation = rw::nextFlag( "--for=", cfg.forTask ); - sectionsNextInvocation += ' '; - sectionsNextInvocation += rw::nextFlag( "--sections=", "lego,compose" ); - } - // R2-L2' (round-2, priced re-registration of L2/B1, rv-prereg2 Amendment 1 R4): a candidate collapses - // ONLY WHEN CHEAPER — see rw::priceSectionStub (serialize.h) for the shared rule (posture-independent - // by construction) and the DEGRADE path it documents: no rendered bytes to gate on there, so the - // section never collapses and is streamed whole below. Pulled into one small function so this - // already-long lens carries none of the pricing branching itself. - const rw::SectionStubPricing legoPricing = legoCandidate - ? rw::priceSectionStub( "lego", legoStubTotal, sectionsNextInvocation, legoPreRendered, legoStr.size() ) - : rw::SectionStubPricing{}; - const rw::SectionStubPricing composePricing = composeCandidate - ? rw::priceSectionStub( "compose", composeStubTotal, sectionsNextInvocation, composePreRendered, composeStr.size() ) - : rw::SectionStubPricing{}; - const bool legoWillStub = legoCandidate && legoPricing.collapse; - const bool composeWillStub = composeCandidate && composePricing.collapse; + // R2-L2' (round-2, priced re-registration of L2/B1, rv-prereg2 Amendment 1 R4): a CANDIDATE (has content, not opted + // back in by --sections=) collapses ONLY WHEN CHEAPER — rw::planForSectionStubs / priceSectionStub (serialize.h) hold + // 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. + const rw::ForSectionStubPlan stubPlan = rw::planForSectionStubs( + 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, + .rendered = composePreRendered, .renderedBytes = composeStr.size() } ); + const std::string& sectionsNextInvocation = stubPlan.nextInvocation; + const rw::SectionStubPricing& legoPricing = stubPlan.lego; + const rw::SectionStubPricing& composePricing = stubPlan.compose; + const bool legoWillStub = stubPlan.legoWillStub; + const bool composeWillStub = stubPlan.composeWillStub; // postcondition of the rule itself: whichever section actually collapses is, by construction, smaller // than what it replaced (the WHOLE point of pricing it) — never a stub that grew the answer. ENSURES( !( legoPreRendered && legoWillStub ) || legoPricing.stubXml.size() < legoStr.size(), "R2-L2': a lego stub collapsed without being smaller than the section it replaced" ); ENSURES( !( composePreRendered && composeWillStub ) || composePricing.stubXml.size() < composeStr.size(), "R2-L2': a compose stub collapsed without being smaller than the section it replaced" ); + // lego-charge: the budget priced a collapsing lego at its PRE-narrow stub; the narrow only empties implementor + // lists, so the stub served now is never longer than the one charged (total= can only shrink). + ENSURES( !( legoWillStub && sigChargePlan.legoWillStub ) || legoPricing.stubXml.size() <= sigChargePlan.lego.stubXml.size(), + "lego-charge: the served lego stub is longer than the stub the budget was charged for" ); std::string sectionsStubNote; // present-only legend clause (kForSectionStubLegend), spliced below if( legoWillStub || composeWillStub ) { @@ -3420,22 +4016,11 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // budget knob, so its size is a FIXED cost; the bodies are the one section with a byte budget, so they // are the section that absorbs whatever the ceiling has left. Pricing the fixed cost first is what lets // the bodies' budget be exact. - rw::ChargedSection graphSection, detailSection; - if( cfg.withGraph ) - { - graphSection = rw::chargeSection( [ & ]( std::FILE* f ) { packGraphBlock( f, ing, lensRank, g.outOff, g.outTargets ); }, - rw::kBytesPerTokenDefault ); - if( !graphSection.isRendered ) - { - // the block streams uncharged below: the lens's own contract omits an est_tokens that left it out - DISCLOSE( blockCharge, ForLensBlockCharge::DisclosureWhy::SiblingBlockUnmeasured, "runForLens: the graph block streams uncharged — est_tokens omitted" ); - } - } + // (graphSection is rendered above, before — fix round 4 reserves its bytes out of the sig side.) // 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) - std::vector detailIds; - std::size_t detailBodyBudget = 0; + // the SAME budget (the map path's emitSection lambda has the identical contract); detailIds is computed above + std::size_t detailBodyBudget = 0; // --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 @@ -3444,7 +4029,6 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // the lens. N=0 emits nothing → byte-identical to a run without --detail. if( cfg.detail > 0 ) { - detailIds = forDetailIds( ing, lensRank, cfg.detail, forTopN ); // (score desc, id asc) — same order as the sigs // Composes with --max-tokens: when set, it bounds the body byte budget (same conservative rate the // map path uses). §F1: --token-budget SHAPES this lens (D10 — trims to fit, always exit 0), so it // has to bound the bodies as well; before this it bounded ONLY and the bodies rode along on @@ -3459,9 +4043,11 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo { 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; // deep-tail: the shell's reserved bytes (explicit regime only — this branch) + + 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; - detailBodyBudget = std::min( detailBodyBudget, leftBytes ); + // 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 + 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, @@ -3488,16 +4074,17 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo 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 : 0u ), + + ( 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, headerParts.viaPresent ); + bundleBudget, ForSectionPromises{ headerParts.viaPresent, &ownerPlan } ); if( enrich.surfaceOff || enrich.legendOff ) { headerParts.autoBundle = headerParts.compactBundle = false; headerParts.viaPresent = false; // FE-B: the section that could carry it is gone (autoBundleMode excludes --detail) + headerParts.ownerPresent = false; // owner-hop: the same section headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); } } @@ -3512,6 +4099,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo } const rw::ChargedSection& autoSection = enrich.section; const std::string& autoAttr = enrich.attr; + if( enrich.legendOff && !autoSection.xml.empty() ) + { + // 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) + sectionsStubNote += headerParts.compactLegend ? kForHopsBudgetCutLegendCompact : kForHopsBudgetCutLegend; + } // ── 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 @@ -3523,7 +4116,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo 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 ); + + 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 — @@ -3533,6 +4127,26 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo " [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]" }; // 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 @@ -3668,6 +4282,18 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // 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; @@ -3684,8 +4310,25 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo } // 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(), kNotes ); + /*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 @@ -3701,6 +4344,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo 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 @@ -3817,29 +4466,55 @@ std::optional runForLens( const MainDispatch& d ) } rw::LensRanking lr = computeLensRanking( d, cfg.forTask, forCompactPosture( cfg ), /*fullDistribution=*/!cfg.candidates ); // deep-tail: the bundle serves the file-grain tail; candidates has no tail and keeps the H2 pruning + // 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 ), /*sigsNextPayFromRows=*/true, nullptr ); + 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, /*sigsNextPayFromRows=*/false, &unpaidOver ); + 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; - rc = runForLensPass( d, lr, /*sigsNextPayFromRows=*/true, &paidOver ); + 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; - return runForLensPass( d, std::move( lr ), /*sigsNextPayFromRows=*/false, nullptr ); + // 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 } ); } std::optional runTargetedViews( const MainDispatch& d ) @@ -3886,12 +4561,14 @@ std::optional runTargetedViews( const MainDispatch& d ) // graphUnindexedLegendComment) because kLegoLegend is one closed literal: the attribute below is // conditional on g.unindexedFiles, so its definition has to be too. // H1: the unproven_defs= clause takes the same route for the same reason (graphlegend.h unprovenDefsVerbComment). - rw::emitTo( stdout, "{}{}{}{}{}", rw::ctxRootOpen( {}, {}, tvRootArg ).c_str(), rw::kLegoLegend, + rw::emitTo( stdout, "{}{}{}{}{}{}", rw::ctxRootOpen( {}, {}, tvRootArg ).c_str(), rw::kLegoLegend, rw::graphUnindexedLegendComment( rw::graphGaugeClauses( g ) ).c_str(), rw::unprovenDefsVerbComment( rw::UnprovenDefsVerb::Lego, legoUnprovenDefs > 0, "{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ).active ), - rw::declinedCallsLegendWithGate( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0 ), // exactly when the root carries declined_calls= - rw::declinedIfaceLegend( chRows.declinedIface > 0 ), // likewise, exactly when declined_iface= is there + rw::declinedCallsLegendWithGates( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + 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::modScopeLegend( chHasModScope ), // #60: likewise, exactly when a t="modscope" row is + ( 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 ) + rw::valueRefsDepthLegendFor( chRows.valueRefs ) ).c_str(), // exactly when the root carries the depth disclosure rw::rootRelPathsLegend( chSingleRoot ), @@ -282,7 +299,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) { pageVia[ i ] = rowNameOnly( g, matches, page[ i ], wantCallers ) ? 1 : 0; } - emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, &pageVia ); + 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 return 0; } @@ -302,18 +320,24 @@ std::optional runCallHierarchy( const MainDispatch& d ) // 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::declinedIfaceKeyJson( chRows.declinedIface ) // A6; then the XML root's declined_calls=/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, cfg.pageLimit, cfg.pageOffset, chDiscloseCap, kJsonPageSyntax ), rw::graphCountFloorAttrJson( g ).c_str() ); // §H4 §3.4 — the JSON dialect's spelling of the same marker rw::emitTo( stdout, ",\"{}\":[", tag ); std::vector chVia( result.size(), 0 ); // FE-B: the same hedge bit the XML rows carry + 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; + if( chSites.any ) + { + chAtKeys[ i ] = rw::callSitesKeyJson( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ); + } } - printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia ); + 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 ) ); return 0; } @@ -338,11 +362,13 @@ std::optional runCallHierarchy( const MainDispatch& d ) const std::string_view rp = chSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ s.fileId ], chRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — the same absence-meaningful convention tested= // already follows everywhere else (serialize.h/verbs_for.h), so an untested row costs 0 bytes. - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + 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( rowNameOnly( g, matches, result[i], wantCallers ) ), // FE-B: a name-only edge says so on its row + chSites.any ? rw::callSitesAttrXml( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ) : std::string() ); // CALLSITE-AT } - rw::emitTo( stdout, "{}", rw::valueRefsXml( ing, chRows.valueRefs, wantCallers, chVr, chVrNext ), tag ); + rw::emitTo( stdout, "{}{}", rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/false ), // the declined C++ standard-member calls, after the rows + rw::valueRefsXml( ing, chRows.valueRefs, wantCallers, chVr, chVrNext ), tag ); return 0; } return std::nullopt; @@ -772,7 +798,7 @@ std::optional runUses( const MainDispatch& d ) "defs_of_name=/call_sites_of_name= (qualifier only) are the un-narrowed totals. " "{}{}{}-->{}{}", rw::kUsesLegendOpen, ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Uses, usUnprovenDefs > 0 ) // H1: exactly when the root carries unproven_defs= - + rw::declinedCallsLegendWithGate( usDeclinedCalls > 0, g.gateDeclinedCalls > 0 ) // exactly when it carries declined_calls= + + rw::declinedCallsLegendWithGates( usDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ) // exactly when it carries declined_calls= + rw::usesValueRoleLegend( std::any_of( sites.begin(), sites.end(), []( const UseSite& u ) { return u.role == RefRole::Value; } ) ) + rw::valueRefsDepthLegend( usDepthCut.files > 0 ) ).c_str(), // exactly when the root carries the depth disclosure rw::capLegendClause( rw::computePageDisclosure( pageRows, sites.size(), upw.end, @@ -873,15 +899,17 @@ struct SafeDeleteLegendFlags bool hasModScope = false; // a row is on this page std::size_t declinedCalls = 0; // declined_calls= on the root (graph.h declinedCallsNaming) bool gateDeclined = false; // the builtin-method gate declined a call in this graph + bool stdDeclined = false; // the C++ standard-member gate declined a call in this graph (graph.h StdMemberGate) std::size_t valueRefs = 0; // value_refs= on the root (valuerefs.h): SYM is used as a value bool callersFloor = false; // callers_floor= on the root (countfloor.h): callers= may be short for THIS definition bool usesFloor = false; // uses_floor= on the root: this kind's reads/type mentions are not indexed in this run - bool hasSitesL = false; // a row is on this page + 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 }; -// count-floor (countfloor.h) + CALLSITE-LINE: the safe-delete legend's clauses for callers_floor=, uses_floor=, next= and -// sites_l=, each exactly when its attribute rides the root/rows, each saying what it does NOT mean. +// count-floor (countfloor.h) + CALLSITE-AT: the safe-delete legend's clauses for callers_floor=, uses_floor=, next= and +// sites_at=/sites_total=, each exactly when its attribute rides the root/rows, each saying what it does NOT mean. inline std::string safeDeleteFloorLegend( const SafeDeleteLegendFlags& flags, std::string_view risk ) { std::string clause; @@ -907,12 +935,11 @@ inline std::string safeDeleteFloorLegend( const SafeDeleteLegendFlags& flags, st clause += "next= is the call that lists what the floored counts could not: the uses verb (every call, value and, where " "indexed, read site spelled like sym=, bound or not), or the literal grep where this language's reads are never indexed. "; } - if( flags.hasSitesL ) + // CALLSITE-AT: the shared sites_at=/sites_total= reading (graphlegend.h), the callers verb's own wording. + clause += rw::callSitesAtLegend( flags.hasSitesAt, flags.sitesCut, rw::kCallSitesPerRowCap ); + if( flags.sitesCut && !( flags.callersFloor || flags.usesFloor ) ) { - clause += "sites_l= on a c row is the ascending LINE list of that caller's call sites spelled like sym= (the rows the uses " - "verb prints) — the lines to open, beside p=, which is where the CALLER is defined. It does NOT mean each line " - "binds to this definition (a same-named call to another definition in that caller is listed too); absent when no " - "call there is spelled like sym= (a renamed import). Two calls on one line are one site. "; + clause += "next= is that uses call. "; // runSafeDelete names it exactly then (sdNextAttr) } return clause; } @@ -947,14 +974,14 @@ 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::declinedCallsLegendWithGate( declinedCalls > 0, flags.gateDeclined ); + 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. "; } 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-LINE: present-only clauses + declinedClause += safeDeleteFloorLegend( flags, risk ); // count-floor + CALLSITE-AT: present-only clauses rw::emitTo( stdout, "", stdout ); // every section constant above ends with its own separating space + if( p.staleCollapsed ) + { + // idea #11: the collapse, named where its attributes are. No flag spelling (a double dash is illegal in a comment). + std::fputs( "stale_by_kind= splits stale= by ack kind (kind:N, comma-separated): on an answer with no regression the stale " + "sa rows are collapsed to these counts when that saves at least a quarter of the bytes, and stale_next= is the call that prints them. ", out ); + } + std::fputs( "-->", out ); // every section constant above ends with its own separating space } // F-13 — the one place both --dead-code and --quality-delta turn a RegisterMacroConfigDiagnostics into @@ -1673,34 +1680,43 @@ std::optional runQualityDelta( const MainDispatch& d ) // emitQualityDeltaLegend above holds the section table and the reasoning; it is passed the same // values the root element below prints, so "the legend defines what the header emits" is read off // one set of values rather than restated as a second condition that could drift from it. - emitQualityDeltaLegend( { baseSel.marker, refPair, identityAttrs, !saRows.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 } ); - 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; - if( baseSel.sidecarBadLines > 0 ) { sidecarHealthAttrs += " baseline_bad_lines=\"" + std::to_string( baseSel.sidecarBadLines ) + "\""; } - if( basis.acksBadLines > 0 ) { sidecarHealthAttrs += " acks_bad_lines=\"" + std::to_string( basis.acksBadLines ) + "\""; } - // #228: present-only, and its absence is the ordinary archived HEAD tree — the floor every git-HEAD - // marker named on its own before this attribute existed. - if( basis.headBasis != nullptr ) { sidecarHealthAttrs += std::string( " head_basis=\"" ) + basis.headBasis + "\""; } - 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( stdout, "", - baseMarker, regs.size(), minorCount, ackedCount, staleAcks.size(), preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, 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. - refPair ? "" : gitstamp::atAttr( root ).c_str(), refs.attrs.c_str(), identityAttrs.c_str(), - scopeAttrs.c_str(), configWarnAttr.c_str(), baselineAbsorbedAttr.c_str(), sidecarHealthAttrs.c_str() ); + // idea #11 — THE CLEAN-TREE PAYLOAD. A regression-free answer collapses its stale-ack rows to counts (stale= and + // stale_by_kind=), and stale_next= is the call that prints them; on a tree that IS its HEAD (head_basis="identity": + // the floor is this tree's own snapshot) with nothing else to disclose, the verdict is the whole answer. Foreign- + // scope rows are never collapsed: they are about findings suppressed RIGHT NOW. --show-stale restores today's + // document byte for byte, so every collapsed row and attribute is one flag away (PROCESS rule 5). + const bool collapseStale = regs.empty() && outOfScope.empty() && !cfg.showStale && !staleAcks.empty(); + std::string staleCollapseAttrs; + if( collapseStale ) + { + std::vector> byKind; // first-appearance order of the ledger's own row order + for( const quality::StaleAck& a : staleAcks ) + { + // the KIND, without the ack's :new-symbol/:preexisting facet — the rows (stale_next=) carry the facet + const std::string kind = a.kind.substr( 0, a.kind.find( ':' ) ); + const auto it = std::find_if( byKind.begin(), byKind.end(), [ & ]( const auto& kv ) { return kv.first == kind; } ); + if( it == byKind.end() ) { byKind.emplace_back( kind, 1 ); } else { ++it->second; } + } + std::string kinds; + for( const auto& [ kind, n ] : byKind ) + { + kinds += ( kinds.empty() ? "" : "," ) + kind + ":" + std::to_string( n ); + } + std::string replay = cfg.qualityDeltaRange.empty() ? std::string( "--quality-delta" ) : rw::nextFlag( "--quality-delta=", cfg.qualityDeltaRange ); + if( scope.active() ) + { + replay += " " + rw::nextFlag( "--scope=", cfg.qualityScope ); + } + staleCollapseAttrs = " stale_by_kind=\"" + ex( kinds ) + "\"" + rw::nextAttrXml( replay + " --show-stale", "stale_next" ); + } + const bool verdictOnly = collapseStale && !refPair && basis.headBasis != nullptr && std::string_view( basis.headBasis ) == "identity" + && !scope.active() && foreignAcks.empty() && ackedCount == 0 && configDiag.total() == 0 && baselineAbsorbed == 0 + && basis.apiNewSurface == 0 && baseSel.sidecarBadLines == 0 && basis.acksBadLines == 0; // P1: ONE row emitter for both halves of the scope partition — a disclosed row carries the identical // attribute set, because nothing about a finding changes by belonging to someone else. `gatingAllowed` // is the one difference: an out-of-scope row is not what the exit code fires on, and a gating // attribute on it would contradict the header's own gating counter inside the same document. - const auto emitRow = [ & ]( const quality::Regression& r, bool gatingAllowed ) + const auto emitRow = [ & ]( std::FILE* out, const quality::Regression& r, bool gatingAllowed ) { // duplication carries a member LIST (members=) + token count; the per-symbol was/now kinds // (complexity/verbosity/nesting/params) carry was/now; api-surface + dead-code are sym-only. @@ -1755,11 +1771,11 @@ std::optional runQualityDelta( const MainDispatch& d ) } if( r.kind == "duplication" ) { - rw::emitTo( stdout, "", ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), r.now, sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr ); + rw::emitTo( out, "", ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), r.now, sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr ); } else if( r.kind == "dead-code" ) { - rw::emitTo( stdout, "", r.kind.c_str(), ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr, nextAttr.c_str() ); + rw::emitTo( out, "", r.kind.c_str(), ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr, nextAttr.c_str() ); // B10.2e: api-surface now carries two shapes — a brand-new/newly-public symbol (was=now=0, no // param comparison to show) and a param-count contract-change (was/now = the real counts). Print // was/now whenever they differ from each other so the contract-change case's was=/now= is visible @@ -1767,39 +1783,99 @@ std::optional runQualityDelta( const MainDispatch& d ) } else if( r.kind == "api-surface" && r.was == r.now ) { - rw::emitTo( stdout, "", r.kind.c_str(), ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr, nextAttr.c_str() ); + rw::emitTo( out, "", r.kind.c_str(), ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr, nextAttr.c_str() ); } else { - rw::emitTo( stdout, "", r.kind.c_str(), ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), r.was, r.now, barAttr, sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr, nextAttr.c_str() ); + rw::emitTo( out, "", r.kind.c_str(), ex( quality::displaySym( r.sym, deltaRoot ) ).c_str(), r.was, r.now, barAttr, sev, facetAttr.c_str(), origin, locAttr.c_str(), gatingAttr, nextAttr.c_str() ); } }; - for( const quality::Regression& r : regs ) + // Fix round 1 (rv-lean-trio item 5, owner ruling 5): a fold ships only when it PAYS (compactlegend.h foldPays: shorter + // by its margin, as written AND as the compact layer delivers it). The whole XML document is one renderer over a FILE*, + // so both candidates are rendered and measured, and the full document wins a fold that does not pay or a failed render + // (fx, dirty tree, one stale ack: 1512 B folded vs 1481 B full). + // `collapse` picks the stale rows' form: true serves only the foreign-scope rows and the stale_by_kind=/stale_next= counts. + // R-I: at= is OMITTED for the ref-pair form (see the root below). Read ONCE: both candidates print the same stamp. + const std::string atAttrs = refPair ? std::string() : gitstamp::atAttr( root ); + const auto emitDocument = [ & ]( std::FILE* out, bool collapse ) { - emitRow( r, /*gatingAllowed=*/true ); - } - if( scope.active() ) - { - // P1 — the DISCLOSURE half. Dropping these rows would make a scoped report a quieter lie than - // the unscoped one it replaces: the debt is real, it is in the same tree, and it will be in - // someone's report. So they are printed, in their own element, marked non-gating, with the one - // sentence a reader needs written where they meet it. Emitted even when EMPTY — an absent - // element then means "no scope was given", never "nobody else has anything open". - rw::emitTo( stdout, "", - outOfScope.size(), scopedOutGating ); - for( const quality::Regression& r : outOfScope ) + const std::vector& served = collapse ? foreignAcks : saRows; + 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 ); + 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; + if( baseSel.sidecarBadLines > 0 ) { sidecarHealthAttrs += " baseline_bad_lines=\"" + std::to_string( baseSel.sidecarBadLines ) + "\""; } + if( basis.acksBadLines > 0 ) { sidecarHealthAttrs += " acks_bad_lines=\"" + std::to_string( basis.acksBadLines ) + "\""; } + // #228: present-only, and its absence is the ordinary archived HEAD tree — the floor every git-HEAD + // marker named on its own before this attribute existed. + if( basis.headBasis != nullptr ) { sidecarHealthAttrs += std::string( " head_basis=\"" ) + basis.headBasis + "\""; } + 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, + // 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() ); + for( const quality::Regression& r : regs ) { - emitRow( r, /*gatingAllowed=*/false ); + emitRow( out, r, /*gatingAllowed=*/true ); } - rw::emitRaw( stdout, "" ); + if( scope.active() ) + { + // P1 — the DISCLOSURE half. Dropping these rows would make a scoped report a quieter lie than + // the unscoped one it replaces: the debt is real, it is in the same tree, and it will be in + // someone's report. So they are printed, in their own element, marked non-gating, with the one + // sentence a reader needs written where they meet it. Emitted even when EMPTY — an absent + // element then means "no scope was given", never "nobody else has anything open". + rw::emitTo( out, "", + outOfScope.size(), scopedOutGating ); + for( const quality::Regression& r : outOfScope ) + { + emitRow( out, r, /*gatingAllowed=*/false ); + } + rw::emitRaw( out, "" ); + } + { // L2 — one row per stale ack (quality::staleAcksXml). M21(a): the escaper is passed in + // because sym= carries a canonical id (corpus text), unlike the closed-vocabulary kind=/why=. + std::vector saEsc; + std::fputs( quality::staleAcksXml( served, [ & ]( std::string_view t ) { return std::string( escapeXml( t, saEsc ) ); } ).c_str(), out ); + } + rw::emitRaw( out, "" ); + }; + // the identity-tree VERDICT (verdictOnly above): the root line alone + const auto emitVerdict = [ & ]( std::FILE* out ) + { + rw::emitTo( out, "" + "", + baseSel.marker, staleAcks.size(), atAttrs, staleCollapseAttrs ); + }; + if( !collapseStale ) + { + emitDocument( stdout, /*collapse=*/false ); + return gatingCount > 0 ? 2 : 0; } - { // L2 — one row per stale ack (quality::staleAcksXml). M21(a): the escaper is passed in - // because sym= carries a canonical id (corpus text), unlike the closed-vocabulary kind=/why=. - std::vector saEsc; - std::fputs( quality::staleAcksXml( saRows, [ & ]( std::string_view t ) { return std::string( escapeXml( t, saEsc ) ); } ).c_str(), stdout ); + 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 ); } } ); + if( !full.ok || !folded.ok ) + { + // renderToString DISCLOSEd the failed buffer; the full document is the superset, written straight to stdout + emitDocument( stdout, /*collapse=*/false ); + return gatingCount > 0 ? 2 : 0; } - rw::emitRaw( stdout, "" ); + 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 std::nullopt; @@ -2384,8 +2460,11 @@ std::optional runEditCheck( const MainDispatch& d ) // editCheckRowWindow). cli.h's honorsPaging() lists this verb, so the pair reaches here rather than // being refused, and 0/0 — the un-spelled window — is the default cap, not "unbounded". const std::string xml = editCheckBundleText( ing, d.g, d.root, cfg.maxFileBytes, cfg.excludes, focus, d.notesPtr, - /*preview=*/false, cfg.pageLimit, cfg.pageOffset, ecUnprovenDefs, - d.notesDegraded ); // L3 follow-up (CodeRabbit 4053600616) + /*preview=*/false, + // idea #11: an unchanged, unflagged contract is its verdict line under EVERY legend + // posture (L1: the legend dial never moves the payload); --limit lists the rows + EditCheckPage{ cfg.pageLimit, cfg.pageOffset, /*verdictOnly=*/true, cfg.editCheckSym }, + ecUnprovenDefs, d.notesDegraded ); // L3 follow-up (CodeRabbit 4053600616) std::fwrite( xml.data(), 1, xml.size(), stdout ); return 0; } diff --git a/src/verbs_report.h b/src/verbs_report.h index 84ad3afaa..65eaf90ce 100644 --- a/src/verbs_report.h +++ b/src/verbs_report.h @@ -50,6 +50,52 @@ void computeDirModules( const rw::IngestResult& ing, std::vector& } } +// --report "## God files": the files a change ripples through most (afferent = files that include/import/source +// each one). A file in serialize.h's testLayerGroup — built-in TEST layer by dir name (test/, tests/, bench/) AND +// every dependent a test path (isTestPath), both by path — is a test helper (e.g. the shell library every gate +// `source`s); ranking it above src/ headers named a test helper as the codebase's god file. Such files are counted +// on one layer=test line under the list, never dropped; the header's "of M" counts the ranked files. A test-dir +// file with any production dependent stays ranked. With no depended-on test-layer file the section is +// byte-identical to the single ranking it replaces. Language-neutral: the split is by path layer, not by the +// directive (#include, import, require, source) that made the edge. +void emitReportGodFiles( const rw::IngestResult& ing, const std::vector& afferent, + const std::vector& testGroup, bool singleRoot, std::string_view rootPrefix ) +{ + EXPECTS( afferent.size() == ing.files.size(), "afferent is indexed by file id" ); + EXPECTS( testGroup.size() == ing.files.size(), "testGroup is indexed by file id" ); + std::vector ranked, testLayer; + for( std::uint32_t f = 0; f < std::uint32_t( afferent.size() ); ++f ) + { + if( afferent[f] == 0 ) + { + continue; + } + ( testGroup[f] ? testLayer : ranked ).push_back( f ); + } + const auto byAfferent = [ & ]( std::uint32_t a, std::uint32_t b ) { return afferent[a] != afferent[b] ? afferent[a] > afferent[b] : a < b; }; + std::sort( ranked.begin(), ranked.end(), byAfferent ); + std::sort( testLayer.begin(), testLayer.end(), byAfferent ); + const auto relPath = [ & ]( std::uint32_t f ) -> std::string_view + { + return singleRoot ? rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) : std::string_view( ing.files[f] ); + }; + const std::size_t shown = std::min( ranked.size(), 10 ); + rw::emitTo( stdout, "\n## God files (most depended-on; showing {} of {})\n", shown, ranked.size() ); + for( std::size_t i = 0; i < shown; ++i ) + { + rw::emitTo( stdout, "- `{}` — {} dependents\n", relPath( ranked[i] ), afferent[ranked[i]] ); + } + if( ranked.empty() ) + { + rw::emitTo( stdout, "{}", testLayer.empty() ? "- (no include/import edges captured)\n" : "- (none outside layer=test)\n" ); + } + if( !testLayer.empty() ) + { + rw::emitTo( stdout, "- layer=test: {} more depended-on files under a test directory whose dependents are all test paths, by path, not ranked above; top `{}` — {} dependents\n", + testLayer.size(), relPath( testLayer[0] ), afferent[testLayer[0]] ); + } +} + // §P6.2: a community label anchored to a trivial accessor ("push_back@svector.h", "empty@notes.h" — the // real-repo instances that motivated this) tells a reader nothing about a multi-member module. These names // are called from everywhere by construction (every container user calls push_back/empty/size), so they @@ -3192,7 +3238,7 @@ std::optional runStructureText( const MainDispatch& d ) // untested cross-directory edges, grouped by DIRECTED dir pair (caller-module → callee-module) struct SeamEdge { NodeId u, v; }; HashMap> grp; - std::uint32_t bridges = 0, untested = 0; + std::uint32_t bridges = 0, untested = 0, docLinks = 0; for( NodeId u = 0; u < N; ++u ) { for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) @@ -3203,6 +3249,13 @@ std::optional runStructureText( const MainDispatch& d ) { continue; // same directory — not a seam } + // A link from one document section to another (a README heading naming docs/EVALS.md) is not + // code: no test can exercise it, so it is neither a bridge nor untested. Counted in doc_links=. + if( ing.symbols[u].kind == SymKind::Section && ing.symbols[v].kind == SymKind::Section ) + { + ++docLinks; + continue; + } ++bridges; if( u < testReach.size() && testReach[u] ) { @@ -3234,8 +3287,9 @@ std::optional runStructureText( const MainDispatch& d ) // §B12.5 — the UNIT clause is the same sentence on all three verbs that spell `untested=` (see // situ.h's kTestGateLegend and flipimpact.h's writeFlipHeader). Each legend was locally honest, // which is precisely why a reader comparing two of the numbers is misled. - rw::emitTo( stdout, "{}", + rw::emitTo( stdout, "{}", rw::modScopeLegend( stHasModScope ), // #60: exactly when a row is + docLinks > 0 ? "doc_links=N: cross-directory links between two document sections (headings, data keys), left out of bridges= and untested= because no test exercises a document link. " : "", rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str(), rw::rootRelPathsLegend( stSingleRoot ) ); // P2.1: two nested caps, neither previously marked — at most 20 seam PAIRS, and at most 5 example // EDGES inside each. Each gains shown= alongside its true untested= count. @@ -3255,8 +3309,9 @@ std::optional runStructureText( const MainDispatch& d ) const PageWindow seamsPw = pageWindow( pairs.size(), effectiveRowCap( cfg.pageLimit, 20 ), cfg.pageOffset ); const std::size_t shownPairs = seamsPw.end - seamsPw.begin; char seamsAb[ kPageDisclosureCap ]; - rw::emitTo( stdout, "", - dirName.size(), bridges, untested, testFileCount, pairs.size(), + const std::string docLinksAttr = docLinks > 0 ? " doc_links=\"" + std::to_string( docLinks ) + "\"" : std::string(); // present-only + rw::emitTo( stdout, "", + dirName.size(), bridges, untested, docLinksAttr, testFileCount, pairs.size(), ( pageDisclosure( seamsAb, sizeof( seamsAb ), shownPairs, pairs.size(), seamsPw.end, cfg.pageLimit, cfg.pageOffset, true ) + rw::renderDisclosure( prD, rw::DiscloseAs::XmlAttrs ) ).c_str(), @@ -3494,30 +3549,7 @@ std::optional runStructureText( const MainDispatch& d ) rw::emitTo( stdout, "- **{}** — {} symbols\n", presentation.label[c].c_str(), std::size_t( members[c].size() ) ); } - std::vector ford( F ); - for( std::uint32_t f = 0; f < F; ++f ) - { - ford[f] = f; - } - std::sort( ford.begin(), ford.end(), [ & ]( std::uint32_t a, std::uint32_t b ) { return afferent[a] != afferent[b] ? afferent[a] > afferent[b] : a < b; } ); - const std::size_t godFileCount = std::count_if( afferent.begin(), afferent.end(), []( std::uint32_t count ) { return count > 0; } ); - const std::size_t reportGodFiles = std::min( godFileCount, 10 ); - rw::emitTo( stdout, "\n## God files (most depended-on; showing {} of {})\n", reportGodFiles, godFileCount ); - bool anyGod = false; - for( std::uint32_t i = 0; i < F && i < 10; ++i ) - { - if( afferent[ford[i]] == 0 ) - { - break; - } - anyGod = true; - const std::string_view rp = stSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ford[i]], stRootPrefix ) : std::string_view( ing.files[ford[i]] ); - rw::emitTo( stdout, "- `{}` — {} dependents\n", std::string_view( rp.data(), rp.size() ), afferent[ford[i]] ); - } - if( !anyGod ) - { - rw::emitTo( stdout, "- (no include/import edges captured)\n" ); - } + emitReportGodFiles( ing, afferent, rw::testLayerGroup( ing, adj ), stSingleRoot, stRootPrefix ); const std::size_t reportCycles = std::min( cycles.size(), 6 ); // #220 part 1: while in-repo TS/JS imports drew no edge, the total is measured over the resolved edges only — NOT a diff --git a/src/wrap.h b/src/wrap.h index dfcdbf1ab..1210fcf27 100644 --- a/src/wrap.h +++ b/src/wrap.h @@ -705,7 +705,7 @@ inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, rw::emitTo( stdout, "# RECOMMENDED — this agent can run shell commands, so call the CLI directly. It costs\n" "# nothing until you invoke it, and it reads {}, so the paste block below IS the wiring:\n" - "{} . --for=\"\" --token-budget=2000\n" + "{} . --for=\"\"\n" "#\n" "# ...every follow-up call answers with the compact legend by default (terse definitions of only the\n" "# attributes the answer carries; the payload is byte-identical either way). Add --legend=full when a\n" @@ -808,7 +808,7 @@ inline void wrapEmitAgent( const std::string_view agent, const WrapListing& list { rw::emitRaw( stdout, "# ripwire -> aider (no MCP; feed a ranked repo map as read-only context)\n" - "ripwire . --for=\"\" --token-budget=2000 > .ripwire-map.txt\n" + "ripwire . --for=\"\" > .ripwire-map.txt\n" "aider --read .ripwire-map.txt\n" "# re-run the first line when the tree changes; the warm cache makes it ~instant.\n" ); } diff --git a/test/affectedcheck.sh b/test/affectedcheck.sh index 4ae0ef194..d97d324ae 100755 --- a/test/affectedcheck.sh +++ b/test/affectedcheck.sh @@ -395,10 +395,10 @@ IM="$( "$BIN" "$TMP/issue60prod" --no-cache --impact=setPhase 2>/dev/null )" || no "(8b) --callers=setPhase count=\"$( attr count "$CA" )\", expected 2" [ "$( attr reaches "$IM" )" = 2 ] && ok "(8b) --impact=setPhase reaches=\"2\" (the pre-#60 binary reports 1)" \ || no "(8b) --impact=setPhase reaches=\"$( attr reaches "$IM" )\", expected 2" -printf '%s' "$CA" | grep -q '' \ +printf '%s' "$CA" | grep -q '' \ && ok "(8b) the named-function caller row is unchanged — no existing edge moved" \ || no "(8b) the boot row changed shape: $CA" -printf '%s' "$CA" | grep -q '' \ +printf '%s' "$CA" | grep -q '' \ && ok "(8b) the module-scope caller is a LABELLED t=\"modscope\" row, not a fabricated function" \ || no "(8b) --callers=setPhase has no modscope row: $CA" # HONESTY, IN EVERY POSTURE. The default CLI legend is compact and the clause is a present-only term diff --git a/test/budgetpolicycheck.sh b/test/budgetpolicycheck.sh index 3cadcf38d..81c0c46af 100755 --- a/test/budgetpolicycheck.sh +++ b/test/budgetpolicycheck.sh @@ -355,5 +355,61 @@ head -1 "$TMP/rtb.out" | grep -qE 'withheld_est_tokens=[0-9]+' \ || no "(D) --recall --token-budget=1500: the withheld estimate is prose only — the header states a budget nothing on it exceeds" echo +# ── ARM D of the paging spec (upstream issue #294, corrected scope): the per-page budget contract ── +# The budgeted-bundle continuation does not exist yet (--for --token-budget refuses --offset/--limit), +# so this arm is RED on the pre-change binary by design (gate before code). Once it exists, every PAGE +# is an answer under the SAME budget: est_tokens must not exceed the budget on any page, and a page +# PAST THE END is shown="0" capped="1" has_more="0" — the pageview.h past-the-end contract — never an +# error, never a silent empty success (an agent's has_more loop terminates on the truth, not on a crash). +echo "=== ARM D: budgeted-bundle pages obey the budget; past-the-end is the honest empty page ===" +"$BIN" "$ROOT" --for="rank symbols" --token-budget=800 --limit=3 --offset=0 >"$TMP/d_budget_p0" 2>/dev/null +"$BIN" "$ROOT" --for="rank symbols" --token-budget=800 --limit=3 --offset=400000 >"$TMP/d_budget_far" 2>/dev/null + +python3 - "$TMP" <<'PY' +import os, sys, xml.etree.ElementTree as ET + +tmp = sys.argv[1] +problems = [] + +def load(name): + path = os.path.join(tmp, name) + if not os.path.exists(path) or os.path.getsize(path) == 0: + problems.append(f"{name}: no output — the budgeted-bundle page does not exist yet " + "(today --token-budget refuses --offset/--limit; that refusal IS this arm's recorded red)") + return None + raw = open(path, encoding="utf-8", errors="replace").read() + try: + return ET.fromstring(raw), raw + except ET.ParseError as e: + problems.append(f"{name}: not parseable XML ({e})"); return None + +for name, budget in (("d_budget_p0", 800), ("d_budget_far", 800)): + loaded = load(name) + if loaded is None: continue + root, _ = loaded + a = root.attrib + est = a.get("est_tokens") + if est is not None and int(est) > budget: + problems.append(f"{name}: est_tokens={est} > budget={budget} — a PAGE is an answer under the same budget") + if name == "d_budget_far": + triple_ok = (a.get("shown") == "0" and a.get("capped") == "1" and a.get("has_more") == "0") + if not triple_ok: + problems.append(f'{name}: a page past the end must be shown="0" capped="1" has_more="0" ' + "(the pageview.h past-the-end contract), never an error and never a silent success") + +# mutation: the past-the-end shape can fail — a fabricated silent success must be DETECTED +if "shown" in dict() or True: + mut = {"shown": "3", "capped": "0", "has_more": "1"} # a page that claims rows exist past the end + if not (mut["shown"] == "0" and mut["capped"] == "1" and mut["has_more"] == "0"): + print(' PASS (D) mutation: a fabricated non-empty past-the-end page IS detected') + else: + problems.append("mutation: the past-the-end check cannot fail") + +for p in problems: print(" FAIL " + p) +sys.exit(1 if problems else 0) +PY +[ $? = 0 ] && ok 'ARM D: budgeted-bundle pages obey est_tokens<=budget; past-the-end is shown="0" capped="1" has_more="0"' \ + || no 'ARM D: the per-page budget contract (upstream issue #294) does not hold — see FAIL lines above' + [ "$fail" = 0 ] && { echo "ALL PASS"; exit 0; } echo "FAILURES — see above"; exit 1 diff --git a/test/callerscheck.sh b/test/callerscheck.sh index 43ff6a9cb..e588972dc 100755 --- a/test/callerscheck.sh +++ b/test/callerscheck.sh @@ -42,14 +42,14 @@ cnt(){ printf '%s' "$1" | grep -oE 'count="[0-9]+"' | grep -oE '[0-9]+'; } # directly rather than a "queryfix/" suffix that used to tolerate an absolute-or-./-relative FIX path) ── OUT_HOT="$( c hot )" { [ "$( cnt "$OUT_HOT" )" = 2 ] \ - && printf '%s' "$OUT_HOT" | grep -qE '' \ - && printf '%s' "$OUT_HOT" | grep -qE ''; } \ - && ok "--callers=hot: exactly caller_a (util.cpp:3) + caller_b (util.cpp:4), count=2" \ + && printf '%s' "$OUT_HOT" | grep -qE '' \ + && printf '%s' "$OUT_HOT" | grep -qE ''; } \ + && ok "--callers=hot: exactly caller_a (util.cpp:3) + caller_b (util.cpp:4), count=2, each with its call site (CALLSITE-AT)" \ || no "--callers=hot: wrong callers/lines — got: $OUT_HOT" # ── #2: --callers=d3 lists exactly its one known caller (d2), correct file:line ───────────────────────── OUT_D3="$( c d3 )" -{ [ "$( cnt "$OUT_D3" )" = 1 ] && printf '%s' "$OUT_D3" | grep -qE ''; } \ +{ [ "$( cnt "$OUT_D3" )" = 1 ] && printf '%s' "$OUT_D3" | grep -qE ''; } \ && ok "--callers=d3: exactly d2 (chain.cpp:3), count=1" \ || no "--callers=d3: wrong caller/line — got: $OUT_D3" @@ -152,5 +152,200 @@ printf '%s' "$XK_MCP" | grep -q 'cross_kind' && printf '%s' "$XK_MCP" | grep -q # Same-kind overloads keep their bytes: queryfix's hot() is one function. c hot | grep -q 'cross_kind=' && no "X: a single-kind answer grew cross_kind=" || ok "X: a single-kind answer carries no cross_kind=" +# ── SA: CALLSITE-AT (2026-10-09) — every caller row carries its call sites as pasteable file:line (sites_at=), so +# "every call site that must change" is answered by the FIRST call, not a --uses follow-up (tmux-19 / hono-19 / +# textual-19 shape: 15 def rows, the 23 sites only in the follow-up). RED on the base binary (no sites_at= at all). +# Arms: multi-site rows, a file-scope () caller expanded to its sites, two calls on one line = one site, +# a near-miss row with NO call spelled like the name (an aliased import) carries none, --callees never carries it, +# equality with --uses role="call" per caller, the page window (offset), the per-row cap (sites_total= + next=), +# present-only legend (compact + full, xmllint), escaping (& in a path; ',' in a columnar cell), and the +# JSON / columnar / MCP (find_referencing_symbols, find_symbol calledBy) twins. +echo "=== SA: caller rows carry their call sites as file:line (sites_at=) ===" +SA="$( mktemp -d "${TMPDIR:-/tmp}/callerscheck-sa.XXXXXX" )" +trap 'rm -rf "$SA"' EXIT +mkdir -p "$SA/t" "$SA/a&b" +cat > "$SA/lib.c" <<'SAEOF' +int target(int x) { return x + 1; } +int two_lines(int y) { + int a = target(y); + int b = target(a); int c = target(b); + return target(c); +} +int one_call(int z) { return target(z); } +SAEOF +cat > "$SA/t/test_mod.py" <<'SAEOF' +def pyhelper(x): + return x + +def py_caller(): + return pyhelper(1) + pyhelper(2) + +pyhelper(3) +pyhelper(4) +SAEOF +cat > "$SA/k.ts" <<'SAEOF' +export function tsHelper(x: number): number { return x; } +export function tsDirect(): number { return tsHelper(1); } +SAEOF +cat > "$SA/j.ts" <<'SAEOF' +import { tsHelper as aliased } from "./k"; +export function tsCaller(): number { return aliased(1); } +SAEOF +{ printf 'def many(x):\n return x\n\n'; i=0; while [ $i -lt 20 ]; do printf 'many(%d)\n' $i; i=$((i+1)); done; } > "$SA/m.py" +printf 'int amp_target(int x) { return x; }\nint amp_caller(int y) { return amp_target(y); }\n' > "$SA/a&b/amp.c" +sa(){ perl -e 'alarm 15; exec @ARGV' "$BIN" "$SA" --no-cache "$@" 2>/dev/null; } +row(){ printf '%s' "$1" | sed 's///' | grep -oE "]*n=\"$2\"[^>]*/>" | head -1; } +sat(){ printf '%s' "$1" | grep -oE ' sites_at="[^"]*"' | head -1 | sed -e 's/^ sites_at="//' -e 's/"$//'; } + +SA_T="$( sa --callers=target )" +[ "$( sat "$( row "$SA_T" two_lines )" )" = "lib.c:3 lib.c:4 lib.c:5" ] \ + && ok "SA1: four calls on three lines -> sites_at=\"lib.c:3 lib.c:4 lib.c:5\" (two calls on one line are one site)" \ + || no "SA1: two_lines row: $( row "$SA_T" two_lines )" +[ "$( sat "$( row "$SA_T" one_call )" )" = "lib.c:7" ] && row "$SA_T" one_call | grep -q ' p="lib.c:7"' \ + && ok "SA1: a one-line caller keeps p= (its definition) and lists its one site" \ + || no "SA1: one_call row: $( row "$SA_T" one_call )" +SA_P="$( sa --callers=pyhelper )" +[ "$( sat "$( row "$SA_P" '<file-scope>' )" )" = "t/test_mod.py:7 t/test_mod.py:8" ] \ + && ok "SA2: a test file's caller expands to its sites (t/test_mod.py:7 t/test_mod.py:8)" \ + || no "SA2: modscope row: $( row "$SA_P" '<file-scope>' )" +SA_TS="$( sa --callers=tsHelper )" +{ row "$SA_TS" tsCaller | grep -q 'p="j.ts:2"' && ! row "$SA_TS" tsCaller | grep -q 'sites_at='; } \ + && ok "SA3: near miss: a caller whose call is spelled through an aliased import carries no sites_at=" \ + || no "SA3: tsCaller row: $( row "$SA_TS" tsCaller )" +[ "$( sat "$( row "$SA_TS" tsDirect )" )" = "k.ts:2" ] && ok "SA3: its direct sibling caller does (k.ts:2)" \ + || no "SA3: tsDirect row: $( row "$SA_TS" tsDirect )" +SA_CE="$( sa --callees=two_lines | sed 's///' )" +if ! printf '%s' "$SA_CE" | grep -q ']*count="[1-9]'; then + no "SA4: premise: --callees=two_lines printed no callees root with rows: $SA_CE" +elif printf '%s' "$SA_CE" | grep -q 'sites_at='; then + no "SA4: a --callees row carries sites_at=" +else + ok "SA4: --callees rows carry no sites_at=" +fi +# SA5: sites_at= equals the --uses role="call" lines inside that caller, for every row of the answer (one question, two documents). +SA5_BAD=0; SA5_N=0 +for nm in target pyhelper; do + ans="$( sa --callers=$nm )"; uses="$( sa --uses=$nm --limit=1000 )" + for r in $( printf '%s' "$ans" | sed 's///' | grep -oE ']*/>' | sed -E 's/.* n="([^"]*)".*/\1/' ); do + got="$( sat "$( row "$ans" "$r" )" )" + want="$( printf '%s' "$uses" | grep -oE "]*in_id=\"$r\"" | sed -E 's/.* p="([^"]*)".*/\1/' | sort -t: -k2,2n -u | tr '\n' ' ' | sed 's/ $//' )" + SA5_N=$((SA5_N+1)); [ "$got" = "$want" ] || { SA5_BAD=1; printf ' %s/%s: sites_at="%s" but --uses role=call sites "%s"\n' "$nm" "$r" "$got" "$want"; } + done +done +[ "$SA5_BAD" = 0 ] && [ "$SA5_N" -ge 4 ] && ok "SA5: every row's sites_at= equals its --uses role=call sites ($SA5_N rows)" \ + || no "SA5: sites_at= != --uses call sites ($SA5_N rows)" +SA_O="$( sa --callers=target --limit=1 --offset=1 )" +{ [ "$( printf '%s' "$SA_O" | sed 's///' | grep -oE '//' | grep -oE ']*/>' )" | grep -qE '^lib\.c:[0-9]'; } \ + && ok "SA6: a paged window (offset=1) still carries its row's sites" || no "SA6: paged answer: $SA_O" +SA_M="$( sa --callers=many )"; MROW="$( row "$SA_M" '<file-scope>' )" +{ [ "$( sat "$MROW" | wc -w | tr -d ' ' )" = 16 ] && printf '%s' "$MROW" | grep -q ' sites_total="20"' \ + && [ "$( sat "$MROW" | cut -d' ' -f1 )" = "m.py:4" ] && printf '%s' "$SA_M" | grep -q 'next="--uses=many"'; } \ + && ok "SA7: a 20-site row lists the first 16 (ascending) with sites_total=\"20\"; next= (--uses=many) lists all" \ + || no "SA7: capped row: $MROW" +printf '%s' "$SA_M" | grep -o '' | grep -q 'sites_total=' && ok "SA7: the compact legend defines sites_total= where it rides" \ + || no "SA7: sites_total= undefined" +printf '%s' "$SA_T" | grep -o '' | grep -q 'sites_total=' && no "SA7: present-only: an uncut answer defines sites_total=" || ok "SA7: present-only: an uncut answer pays nothing for sites_total=" +printf '%s' "$SA_T" | grep -o '' | grep -q 'sites_at=' && ok "SA8: the compact legend defines sites_at= where it rides" \ + || no "SA8: sites_at= undefined in the compact legend" +SA_F="$( sa --callers=many --legend=full )" +{ printf '%s' "$SA_F" | grep -o '' | grep -q 'sites_at= on a caller row' && printf '%s' "$SA_F" | grep -q 'sites_total=N' \ + && printf '%s' "$SA_F" | xmllint --noout - 2>/dev/null; } \ + && ok "SA8: --legend=full carries the sites_at=/sites_total= clauses and stays well-formed (no -- in the comment)" || no "SA8: full legend: $( printf '%s' "$SA_F" | head -c 300 )" +SA_D1="$( "$BIN" "$FIX" --callers=d1 --no-cache 2>/dev/null )" +if ! printf '%s' "$SA_D1" | grep -q ']*count="0"'; then + no "SA8: premise: --callers=d1 printed no zero-row callers root: $SA_D1" +elif printf '%s' "$SA_D1" | grep -q 'sites_at'; then + no "SA8: a zero-row answer mentions sites_at" +else + ok "SA8: present-only: a zero-row answer carries no sites_at reading" +fi +SA_AMP="$( sa --callers=amp_target )" +{ printf '%s' "$SA_AMP" | grep -q 'sites_at="a&b/amp.c:2"' && printf '%s' "$SA_AMP" | xmllint --noout - 2>/dev/null; } \ + && ok "SA9: a path holding & is escaped inside sites_at= (well-formed)" || no "SA9: amp answer: $SA_AMP" +SA_J="$( sa --callers=target --json )" +printf '%s' "$SA_J" | python3 -c ' +import json, sys +d = json.load( sys.stdin ); r = { x["n"]: x.get( "sites_at" ) for x in d["callers"] } +sys.exit( 0 if r.get( "two_lines" ) == [ "lib.c:3", "lib.c:4", "lib.c:5" ] and r.get( "one_call" ) == [ "lib.c:7" ] else 1 )' \ + && ok "SA10: --json rows carry \"sites_at\":[file:line,...] (the XML row's list)" || no "SA10: --json: $SA_J" +sa --callers=many --json | python3 -c ' +import json, sys +d = json.load( sys.stdin ); r = d["callers"][0] +sys.exit( 0 if len( r.get( "sites_at", [] ) ) == 16 and r.get( "sites_total" ) == 20 else 1 )' \ + && ok "SA10: a cut --json row carries sites_total" || no "SA10: --json cut row" +SA_C="$( sa --callers=target --format=columnar )" +{ printf '%s' "$SA_C" | grep -q 'fields="path,name,line,kind,tested,sites_at"' && printf '%s' "$SA_C" | grep -q '[^<]*lib.c:3 lib.c:4 lib.c:5' \ + && printf '%s' "$SA_C" | xmllint --noout - 2>/dev/null; } \ + && ok "SA10: --format=columnar carries the sites_at column (fields= names it)" || no "SA10: columnar: $( printf '%s' "$SA_C" | sed 's///' )" +sa --callers=many --format=columnar | grep -q '20' && ok "SA10: a cut columnar row carries the sites_total column" \ + || no "SA10: columnar cut row" +if ! printf '%s' "$SA_C" | grep -q ''; then + no "SA10: premise: the uncut columnar answer carries no sites_at column" +elif printf '%s' "$SA_C" | grep -q 'sites_total'; then + no "SA10: present-only: an uncut columnar answer carries sites_total" +else + ok "SA10: present-only: no sites_total column when nothing was cut" +fi +sa_mcp(){ printf '%s\n%s\n%s\n' \ + '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \ + '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"'"$1"'","arguments":{"path":"'"$SA"'","symbol":"'"$2"'"}}}' \ + | perl -e 'alarm 20; exec @ARGV' "$BIN" "$SA" --mcp --no-cache 2>/dev/null | tail -1; } +for tool in find_referencing_symbols find_symbol; do + sa_mcp "$tool" target | python3 -c ' +import json, sys +env = json.loads( sys.stdin.read() ); d = json.loads( env["result"]["content"][0]["text"] ) +r = { x["name"]: x.get( "sites_at" ) for x in d["calledBy"] } +calls_clean = all( "sites_at" not in x for x in d.get( "calls", [] ) ) +sys.exit( 0 if r.get( "two_lines" ) == [ "lib.c:3", "lib.c:4", "lib.c:5" ] and "sites_note" in d and calls_clean else 1 )' \ + && ok "SA11: MCP $tool calledBy rows carry sites_at (== CLI) and the answer carries sites_note" \ + || no "SA11: MCP $tool: $( sa_mcp "$tool" target | head -c 400 )" +done +# SA12 (fix round 1, review item 1): near misses the call-role filter (editcheck.h forEachCallSite: r.role == Call) must +# keep OUT of sites_at=: in ONE caller that also holds a real call, a value use, &f, a block-scope prototype, a string and +# two comments spelled like the name; plus a caller whose only use is a value use. RED when that filter is dropped (the +# value uses and &f print as sites). Then the mechanism the legend states: matching is by LEAF name, so a qualified +# selector (--callers=ns::nst_fn) lists the same row's member call w.nst_fn(5) too. +mkdir -p "$SA/nm" +cat > "$SA/nm/noise.c" <<'SAEOF' +typedef int (*nmfp)(int); +int nm_target(int x) { return x + 1; } +int nm_noise(void) { + /* nm_target(3) in a comment */ + // nm_target(4) in a line comment + const char *s = "nm_target(5) in a string"; + int nm_target(int); + nmfp p = nm_target; + nmfp q = &nm_target; + return nm_target(6) + (s != 0) + p(0) + q(0); +} +int nm_pointer_only(void) { + nmfp p = nm_target; + return p(7); +} +SAEOF +cat > "$SA/nm/ns.cpp" <<'SAEOF' +namespace ns { int nst_fn(int x) { return x + 1; } } +struct W { int nst_fn(int x) { return x; } }; +int nst_mixed() { + W w; + int a = w.nst_fn(5); + int b = ns::nst_fn(6); + return a + b; +} +SAEOF +SA_NM="$( sa --callers=nm_target )" +SA_NM_ALL="$( printf '%s' "$SA_NM" | sed 's///' | grep -oE ' sites_at="[^"]*"' | sed -e 's/^ sites_at="//' -e 's/"$//' | tr '\n' ' ' | sed 's/ $//' )" +if [ -z "$( row "$SA_NM" nm_noise )" ]; then + no "SA12: premise: --callers=nm_target printed no nm_noise row: $SA_NM" +elif [ "$( sat "$( row "$SA_NM" nm_noise )" )" = "nm/noise.c:10" ] && [ "$SA_NM_ALL" = "nm/noise.c:10" ]; then + ok "SA12: near miss: a value use, &f, a local prototype, a string and comments are not sites (only nm/noise.c:10)" +else + no "SA12: non-call references printed as call sites: every sites_at= is \"$SA_NM_ALL\", want \"nm/noise.c:10\"" +fi +SA_NS="$( sa --callers=ns::nst_fn )" +[ "$( sat "$( row "$SA_NS" nst_mixed )" )" = "nm/ns.cpp:5 nm/ns.cpp:6" ] \ + && ok "SA12: by LEAF name: --callers=ns::nst_fn lists the row's w.nst_fn(5) call too (nm/ns.cpp:5 nm/ns.cpp:6)" \ + || no "SA12: ns::nst_fn row: $( row "$SA_NS" nst_mixed )" [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/ceilingverdictcheck.sh b/test/ceilingverdictcheck.sh index 5f6695168..d1151fef4 100755 --- a/test/ceilingverdictcheck.sh +++ b/test/ceilingverdictcheck.sh @@ -130,6 +130,22 @@ else ok "(2) task text cannot forge over_ceiling=\"1\" (est_tokens=$fw_est under budget_tokens=$fw_bud, attribute absent)" 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 + # (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 )" diff --git a/test/childwalkscalecheck.sh b/test/childwalkscalecheck.sh index 35233a02b..692225986 100755 --- a/test/childwalkscalecheck.sh +++ b/test/childwalkscalecheck.sh @@ -306,6 +306,21 @@ for n in ( 1000, 16000 ): # lists, so both sides execute nodesMatchExactly and only the flooded list differs (CodeRabbit, #130) w( "patmeta/n%d/big.c" % n, [ "int a( int x );", "int f( void )", "{", " return a(" ] + [ C ] * n + [ " 1 ) == a(" ] + [ C ] * n + [ " 1 );", "}" ] ) w( "patmeta_off/n%d/big.c" % n, [ "int a( int x );", "int f( void )", "{", " return a( 1 ) == a( 1 );" ] + [ C ] * n + [ C ] * n + [ "}" ] ) +# #358 import-capture reach: importContainerReach reads one directive's ancestry. These two fixtures are about +# DEPTH, not child-list width, so they sit outside the n1000/n16000 loop and the (C) corpus list on purpose. +# Each puts 2000 IMPORTS inside 2000 nested containers; the control keeps the nesting and swaps the imports +# for statements that are not imports, so both sides run the same tags pass over the same tree and only the +# import emission differs. A reach read that costs a descent PER HOP (an upward ts_node_parent walk) is +# 60-230x its control here; one descent from the root is under 3x. +D = 2000 +w( "reachjs/big.js", [ "{" * D ] + [ 'require( "./m" );' ] * D + [ "}" * D ] ) +w( "reachjs_off/big.js", [ "{" * D ] + [ "noop();" ] * D + [ "}" * D ] ) +w( "reachc/big.c", [ "#ifdef G%d" % i for i in range( D ) ] + [ '#include "m.h"' ] * D + [ "#endif" ] * D ) +w( "reachc_off/big.c", [ '#include "m.h"' ] * D ) +for d in ( "reachjs", "reachjs_off" ): + w( d + "/m.js", [ "int x;" ] ) +for d in ( "reachc", "reachc_off" ): + w( d + "/m.h", [ "int x;" ] ) PY # user-CPU seconds (user+sys) of one cold run of "$@" against corpus $1 @@ -528,6 +543,14 @@ b_pm_off="$( usercpu "$TMP/patmeta_off/n16000" "$BIN" --pattern='$X == $X' )" b_pm_walk="$( usercpu "$TMP/patmeta/n16000" "$BIN" --pattern='$X == $X' )" arm "(B36) nodesMatchExactly" "$b_pm_off" "$b_pm_walk" 8 0.30 "--pattern='\$X == \$X' over two 16000-comment argument lists vs the same pattern with the floods outside both lists" +# #358 — importContainerReach (src/ingest_importcap.h): ancestry read in ONE descent, not k ts_node_parent hops +b_rjs_off="$( usercpu "$TMP/reachjs_off" "$BIN" --top-k=100000 )" +b_rjs_on="$( usercpu "$TMP/reachjs" "$BIN" --top-k=100000 )" +arm "(B37) importContainerReach/js" "$b_rjs_off" "$b_rjs_on" 8 0.30 "2000 require() calls inside 2000 nested blocks vs the same nesting holding 2000 non-import calls" +b_rc_off="$( usercpu "$TMP/reachc_off" "$BIN" --top-k=100000 )" +b_rc_on="$( usercpu "$TMP/reachc" "$BIN" --top-k=100000 )" +arm "(B38) importContainerReach/c" "$b_rc_off" "$b_rc_on" 8 0.30 "2000 #include lines inside 2000 nested #ifdef vs the same 2000 lines at file scope" + # ── (C) byte-identical against a reference binary ──────────────────────────────────────────────────── echo echo "=== (C) byte-identical output vs RIPWIRE_REF_BIN ===" diff --git a/test/churnjsonstampcheck.sh b/test/churnjsonstampcheck.sh index e3ec111ba..ef7eda89c 100755 --- a/test/churnjsonstampcheck.sh +++ b/test/churnjsonstampcheck.sh @@ -40,6 +40,14 @@ python3 - "$TMP/churn.json" "$TMP/pagerank.json" > "$TMP/verdict.txt" <<'PY' import json, sys churn = json.load( open( sys.argv[1] ) ) plain = json.load( open( sys.argv[2] ) ) +# D7 (2026-10-08): "inv" is PAYLOAD, not header — the JSON twin of the map scope's element, which rides the +# pagerank map whose ranked rows left files out and never a churn-ranked map (not the map scope). It sits beside "r" +# and is compared as payload: the header keysets below exclude it, and INV_* says where it rode. +PAYLOAD = { "inv" } +print( "INV_PLAIN=" + str( int( "inv" in plain ) ) ) +print( "INV_CHURN=" + str( int( "inv" in churn ) ) ) +churn = { k: v for k, v in churn.items() if k not in PAYLOAD } +plain = { k: v for k, v in plain.items() if k not in PAYLOAD } extra = sorted( set( churn ) - set( plain ) ) missing = sorted( set( plain ) - set( churn ) ) print( "EXTRA=" + ",".join( extra ) ) @@ -62,6 +70,9 @@ else fi [ -z "$MISSING" ] && ok "churn JSON header drops no key the pagerank header carries" \ || no "churn JSON header is MISSING keys the pagerank header has: $MISSING" +# D7: the inventory payload rides the map scope (pagerank, top-k=3 over src/ leaves files out) and never churn +[ "$INV_PLAIN" = 1 ] && [ "$INV_CHURN" = 0 ] && ok "\"inv\" (payload) rides the pagerank map and not the churn map" \ + || no "\"inv\" placement: pagerank=$INV_PLAIN churn=$INV_CHURN (want 1/0) — the payload exclusion above would hide a header key" # (2) the values carry MEANING (not pinned to a sha/window that moves with the tree) if [ "$RANK_BY" = "churn" ]; then ok "rank_by == \"churn\""; else no "rank_by is '$RANK_BY', want \"churn\""; fi diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 7e575a652..b850b2d50 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -305,6 +305,14 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # v1 legend says "each absent when 0;". Checked mechanically: restoring the zeros on every r= row and removing that clause # gives the previous pin byte for byte with at= and est_tokens= masked (est_tokens= prices the shorter bytes). The six # other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-09 (lane callers-sites-first-call, CALLSITE-AT, by design), callers.xml only: its two rows +# gain sites_at= (each caller's call site as file:line: geometry.cpp:16, sub/consumer.cpp:10) and the full legend gains the +# present-only sites_at= clause (graphlegend.h kCallSitesAtLegend), 3492 -> 3890 B. Checked mechanically: removing the two +# sites_at= attributes and that one clause gives the previous pin byte for byte (at= masked). The six other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-10 (lane callers-sites-first-call fix round 1, wording = mechanism), callers.xml only: the +# sites_at= clause says the match is by LEAF name ("its call sites spelled like the called name" -> "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. 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. @@ -531,6 +539,11 @@ probeFor() # PINNED 2026-10-01 (C/C++ declaration/definition fold): ripwire.edit-check/v1 610 (measured 595). The --edit-check=distance # probe used to be REFUSED — geometry.h declares distance and geometry.cpp defines it, and the verb counted the prototype and # its definition as two contracts — so the schema had no XML answer to pin. It now answers about the definition. +# RE-PINNED 2026-10-09 (lane callers-sites-first-call, CALLSITE-AT): ripwire.callers/v1 330 -> 450 (measured 438; 317 at +# the last pin) and ripwire.safe-delete/v1 820 -> 840 (measured 828, was 805). Every caller row now carries sites_at=, its +# call sites as pasteable file:line, and the compact legend reads it (": ..." on callers, +121 B with its +# separator; on safe-delete the sites_l= reading became the sites_at= one, +23 B). Twins: (U-twin-callers) below holds the +# 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-04 (lane/lean-answers-068), two new definitions, each measured on this gate's fixture against the @@ -551,9 +564,22 @@ probeFor() # the defs page whenever it lists MORE definitions than the all page under the row cap, whatever its bytes. Rides only a # page that carries listing=; nothing else moved. The OLD path keeps its pin: the whereis-listing=all twin holds the # whole-list page's legend at 700 B (measured 692, unchanged). +# RE-PINNED 2026-10-08 (lane/map-inventory-d7, D7), measured on this gate's fixture: ripwire.map/v1 892->1129, pin +# 910->1140 (measured + 10 rounded up to 10). The --top-k=3 probe leaves out three of the fixture's six files, so the map +# now carries the inventory tier (, ; no on this fixture) and its two +# present-only readings (+237 B with separators). Nothing else moved. The OLD path keeps its pin: the --no-inventory twin +# after the universe loop holds the same probe without the tier at 910 B. +# RE-PINNED 2026-10-08 (lane/expand-lean-k64): ripwire.expand-file/v1 360 -> 510 (measured 508, the --expand=distance probe). +# A name with several definitions now serves lean (no ride-along map), so the whole-file answer this probe reaches carries +# topk_default="0" (+112 B reading) and map_next= (+56 B reading) on its root, both present-only (compactlegend.h). The +# (U-twin-expand) arm below keeps the OLD 360 B pin for everything else in that legend. +# RE-PINNED 2026-10-10 (lane lean-trio, idea #11): ripwire.edit-check/v1 610 -> 650 (measured 636, the --edit-check=distance +# probe; 595 on main 0852bc0f). The probe's contract is unchanged with nothing flagged, so the answer is now the verdict +# line (no rows), and the compact legend reads its two new facts: rows_next= (the --limit call listing the callers) and +# callers= as the c rows' total (a floor). Nothing else moved. PIN_TABLE=' -ripwire.edit-check/v1 610 595 -ripwire.map/v1 910 892 +ripwire.edit-check/v1 650 636 +ripwire.map/v1 1140 1129 ripwire.map-diff/v1 900 885 ripwire.pack-signatures/v1 770 759 ripwire.metrics/v1 1040 1021 @@ -603,11 +629,11 @@ ripwire.match/v1 370 355 ripwire.lego/v1 290 275 ripwire.exemplar/v1 440 422 ripwire.around/v1 770 760 -ripwire.callers/v1 330 317 +ripwire.callers/v1 450 438 ripwire.callees/v1 500 484 ripwire.uses/v1 510 500 ripwire.batch/v1 250 238 -ripwire.safe-delete/v1 820 805 +ripwire.safe-delete/v1 840 828 ripwire.at/v1 180 161 ripwire.from-trace/v1 1300 1281 ripwire.plan-lint/v1 570 551 @@ -618,7 +644,7 @@ ripwire.layout/v1 1220 1203 ripwire.pack-task/v1 990 974 ripwire.pack-top-n/v1 760 745 ripwire.expand/v1 280 265 -ripwire.expand-file/v1 360 342 +ripwire.expand-file/v1 510 508 ripwire.for/v1 775 761 ' pinFor() @@ -626,7 +652,8 @@ pinFor() printf '%s\n' "$PIN_TABLE" | awk -v schema="$1" '$1 == schema { printf "%s", $2; exit }' } # (U-twin) the pre-2026-10-04 ripwire.safe-delete/v1 pin, kept (PROCESS rule 4: a re-pin keeps a twin for the old path). -# The 720 -> 820 re-pin above paid for ONE reading, the caller row's sites_l= (+90 B with its separator). Everything else +# The 720 -> 820 re-pin above paid for ONE reading, the caller row's sites_l= (+90 B with its separator); since CALLSITE-AT +# (2026-10-09) that reading is the row's sites_at= (file:line tokens), and the twin cuts that one instead. Everything else # in that prose legend still fits the OLD 720 B: the same --safe-delete=distance probe, with exactly that reading cut out # of a copy of the compact answer, is measured by the same `leg prose` operand the (U) arm uses. The cut is taken only when # a caller row carries sites_l= AND the reading occurs once; any other premise FAILs (a cut that removes nothing proves nothing). @@ -635,12 +662,12 @@ pinFor() sdtCut="$( python3 - "$TMP/sdt.c" "$TMP/sdt.cut" <<'PY' import re, sys doc = open( sys.argv[1], encoding = "utf-8", errors = "replace" ).read() -reading = " c sites_l=: its call-site lines (p= is the caller's def line); not proof each binds here." +reading = " c sites_at=: its calls of the leaf name, any receiver, as file:line (p= is its def line); not proof each binds." body = re.sub( r"", "", doc, flags = re.S ) -if not re.search( r']*sites_l="[0-9]', body ): - print( "NOPREMISE no caller row carries sites_l=" ); sys.exit( 0 ) +if not re.search( r']*sites_at="[^"]*:[0-9]', body ): + print( "NOPREMISE no caller row carries sites_at=" ); sys.exit( 0 ) if doc.count( reading ) != 1: - print( "NOPREMISE the sites_l= reading occurs %d times, not once" % doc.count( reading ) ); sys.exit( 0 ) + print( "NOPREMISE the sites_at= reading occurs %d times, not once" % doc.count( reading ) ); sys.exit( 0 ) open( sys.argv[2], "w", encoding = "utf-8" ).write( doc.replace( reading, "", 1 ) ) print( len( reading.encode() ) ) PY @@ -652,9 +679,73 @@ else NOPREMISE*) no "(U-twin) safe-delete twin cannot measure: ${sdtCut#NOPREMISE }" ;; *) sdtRest="$( leg prose "$TMP/sdt.cut" "$TMP/sdt.full" )" if [ -n "$sdtRest" ] && [ "$sdtRest" -eq "$sdtRest" ] 2>/dev/null && [ "$sdtRest" -le 720 ]; then - ok "(U-twin) safe-delete compact prose minus the sites_l= reading ($sdtCut B) is $sdtRest B <= 720 B (the pre-CALLSITE-LINE pin)" + ok "(U-twin) safe-delete compact prose minus the sites_at= reading ($sdtCut B) is $sdtRest B <= 720 B (the pre-CALLSITE-LINE pin)" else - no "(U-twin) safe-delete compact prose minus the sites_l= reading ($sdtCut B) is ${sdtRest:-?} B > 720 B — the rest re-inflated" + no "(U-twin) safe-delete compact prose minus the sites_at= reading ($sdtCut B) is ${sdtRest:-?} B > 720 B — the rest re-inflated" + fi ;; + esac +fi +# (U-twin-callers) the pre-2026-10-09 ripwire.callers/v1 pin, kept the same way: the 330 -> 450 re-pin paid for ONE +# reading, the caller row's sites_at=. The --callers=distance compact answer with exactly that reading cut out still fits +# 330 B under the same `leg prose` operand; the cut is taken only when a row carries sites_at= AND the reading occurs once. +( cd "$REPO" && "$BIN" . --callers=distance --legend=compact >"$TMP/cat.c" 2>/dev/null "$TMP/cat.full" 2>/dev/null ", "", doc, flags = re.S ) +if not re.search( r']*sites_at="[^"]*:[0-9]', body ): + print( "NOPREMISE no caller row carries sites_at=" ); sys.exit( 0 ) +if doc.count( reading ) != 1: + print( "NOPREMISE the sites_at= reading occurs %d times, not once" % doc.count( reading ) ); sys.exit( 0 ) +open( sys.argv[2], "w", encoding = "utf-8" ).write( doc.replace( reading, "", 1 ) ) +print( len( reading.encode() ) ) +PY +)" +if [ "$rcCat" -ne 0 ]; then + no "(U-twin-callers) --callers=distance exited $rcCat — the twin cannot measure" +else + case "$catCut" in + NOPREMISE*) no "(U-twin-callers) callers twin cannot measure: ${catCut#NOPREMISE }" ;; + *) catRest="$( leg prose "$TMP/cat.cut" "$TMP/cat.full" )" + if [ -n "$catRest" ] && [ "$catRest" -eq "$catRest" ] 2>/dev/null && [ "$catRest" -le 330 ]; then + ok "(U-twin-callers) callers compact prose minus the sites_at= reading ($catCut B) is $catRest B <= 330 B (the pre-CALLSITE-AT pin)" + else + no "(U-twin-callers) callers compact prose minus the sites_at= reading ($catCut B) is ${catRest:-?} B > 330 B — the rest re-inflated" + fi ;; + esac +fi +# (U-twin-expand) the pre-2026-10-08 ripwire.expand-file/v1 pin (360 B), kept (PROCESS rule 4): the same --expand=distance probe +# with exactly the two lean-default readings cut out of a copy of the compact answer must still fit the OLD pin. The cut is +# taken only when BOTH readings occur once; any other premise FAILs (a cut that removes nothing proves nothing). +( cd "$REPO" && "$BIN" . --expand=distance --legend=compact >"$TMP/xft.c" 2>/dev/null "$TMP/xft.full" 2>/dev/null /dev/null && [ "$xftRest" -le 360 ]; then + ok "(U-twin-expand) expand-file compact prose minus the two lean-default readings ($xftCut B) is $xftRest B <= 360 B (the pre-lean pin)" + else + no "(U-twin-expand) expand-file compact prose minus the two lean-default readings ($xftCut B) is ${xftRest:-?} B > 360 B — the rest re-inflated" fi ;; esac fi @@ -796,6 +887,18 @@ if [ "$rcWall" -eq 0 ] && [ "$( leg schema "$TMP/wall.c" )" = "ripwire.whereis/v else no "(U-TWIN) --whereis-listing=all compact PROSE legend is '${wallLb:-none}' B (pin 700; exit $rcWall): $( leg legend "$TMP/wall.c" | head -c 200 )" fi +# (U-TWIN) the map without its inventory tier keeps the pre-D7 pin: the --top-k=3 probe's pin rose to 1140 B for the +# readings, and without this twin the rest of the map legend could grow inside that headroom unseen. --no-inventory serves +# the same ranked rows with no tier (mapinventorycheck (N)). 910 = the pre-D7 pin (measured 892). +( cd "$REPO" && "$BIN" . --top-k=3 --no-inventory >"$TMP/mnoinv.c" 2>/dev/null "$TMP/mnoinv.f" 2>/dev/null /dev/null && [ "$mniLb" -le 910 ]; then + ok "(U-TWIN) --top-k=3 --no-inventory compact PROSE legend is $mniLb B (<= the pre-D7 910 B pin; the tiered probe's pin is 1140)" +else + no "(U-TWIN) --top-k=3 --no-inventory compact PROSE legend is '${mniLb:-none}' B (pin 910; exit $rcMni): $( leg legend "$TMP/mnoinv.c" | head -c 200 )" +fi echo echo "=== (F) the DEFAULT is the default posture; --legend=full restores MORE legend over the SAME payload (L1) ===" @@ -997,6 +1100,84 @@ 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="" +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 + fi +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)" +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))" +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 +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=" \ + || 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 # dialect's price before the compact layer ran, so a map that fits once compacted was withheld (exit 3). Budget = the # compact price itself: the default must answer (exit 0), --legend=full must still withhold (exit 3), and the full price @@ -1061,8 +1242,12 @@ done # 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 -for tb in 500 750; do +for tb in 500 900; do for lg in compact full; do ( cd "$P4" && "$BIN" . --for="widget routine dispatcher" --token-budget=$tb --legend=$lg 2>/dev/null ) >"$TMP/p4f.$lg" done @@ -1092,7 +1277,7 @@ for tb in 500 750; do p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: compact est_tokens=${pc:-?} over the budget AND over the full answer's ${pf:-?}" fi done -[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= unpaid at the rank 1..4 floor with over_ceiling=\"1\", the unpaid clause and an honest est_tokens, the 750 run's paid next= fits with no label, compact never further over than full" +[ "$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" # (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: @@ -1188,11 +1373,14 @@ echo # RE-ANCHORED 2026-09-26 (lane impact-depth-065, depth-labelled --impact): 7,500 → 7,700 B, measured 7,622 (7,495 on the # base binary b343b988). The loop's --impact=distance probe now carries by_depth= and d=, and its compact legend reads both # (+127 B, the (U) table's ripwire.impact/v1 row); the other nine verbs unmoved. Same rule: the next multiple of 100 B. +# RE-ANCHORED 2026-10-09 (lane callers-sites-first-call, CALLSITE-AT): 7,800 → 7,900 B, measured 7,830 (7,711 at the +# train-26b anchor below, which is this lane's base). The +119 B is the loop's --callers probe: its rows now carry +# sites_at= and the compact legend reads it (the ripwire.callers/v1 (U) row moved +121 B the same way). Same rule. # RE-ANCHORED 2026-10-08 (train 26b): 7,700 → 7,800 B, measured 7,711. Attributed on this fixture by merge step: 7,622 at # the train's phase-A head, 7,688 with receiver evidence merged (its present-only via= readings; under the old ceiling), # 7,711 with lean-answers merged: +23 B, all on the --for probe, the " (absent cx/ccx/in = 0)" reading the lane pinned # in the ripwire.for/v1 row. The other verbs are unmoved by that step. Same rule: the next multiple of 100 B. -echo "=== (L) the canonical ten-verb edit loop: compact legend bill ≤ 7,800 B (34,716 B in full on the fixture) ===" +echo "=== (L) the canonical ten-verb edit loop: compact legend bill ≤ 7,900 B (34,716 B in full on the fixture) ===" loopBytes=0; fullBytes=0 for v in "--for=geometry distance" "--callers=distance" "--impact=distance" "--uses=distance" "--edit-check=total_area" \ "--quality-delta" "--test-gate=geometry.cpp" "--affected=geometry.cpp" "--safe-delete=total_area" "--slice=total_area"; do @@ -1201,8 +1389,8 @@ for v in "--for=geometry distance" "--callers=distance" "--impact=distance" "--u b="$( leg bytes "$TMP/l.c" )"; f="$( leg bytes "$TMP/l.f" )" loopBytes=$(( loopBytes + b )); fullBytes=$(( fullBytes + f )) done -[ "$loopBytes" -le 7800 ] && ok "(L) ten-verb loop: $loopBytes B of compact legend (full: $fullBytes B)" \ - || no "(L) ten-verb loop pays $loopBytes B of compact legend (> 7,800 B; full: $fullBytes B)" +[ "$loopBytes" -le 7900 ] && ok "(L) ten-verb loop: $loopBytes B of compact legend (full: $fullBytes B)" \ + || no "(L) ten-verb loop pays $loopBytes B of compact legend (> 7,900 B; full: $fullBytes B)" echo echo "=== (M) MCP: legend:\"compact\" on edit_check answers in ≤ 900 B on a clean tree; every XML verb takes the argument, within its per-verb legend pin ===" diff --git a/test/compactlegendfix/pre_l1_full/callers.xml b/test/compactlegendfix/pre_l1_full/callers.xml index d6011a6a5..6f6ca06a7 100644 --- a/test/compactlegendfix/pre_l1_full/callers.xml +++ b/test/compactlegendfix/pre_l1_full/callers.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index caaff53ca..14ed41c79 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -23,9 +23,10 @@ # dead_code_candidate="1", no floor marker, no next=. # R (negatives) recursion: a self-call is bound to the definition itself (C static fn, Python self.m() and module fn, # mutual recursion); (near miss) a recursive TS method with an outside same-named call bound nowhere still floors. -# C CALLSITE-LINE: --safe-delete=width_of rows carry sites_l= (the call lines) beside p= (the definition line): -# a caller with three calls on three lines lists all three; one call split over two lines lists its line. -# D (near-miss) a single-definition symbol called once still prints its call site (sites_l= present, one line). +# C CALLSITE-LINE -> CALLSITE-AT (2026-10-09): --safe-delete=width_of rows carry sites_at= (the call sites as pasteable +# file:line tokens; it was sites_l=, bare lines) beside p= (the definition line): a caller with three calls on three +# lines lists all three; one call split over two lines lists its line. Twin: no safe-delete row carries sites_l= now. +# D (near-miss) a single-definition symbol called once still prints its call site (sites_at= present, one token). # E grep on a Python module VARIABLE: callers_floor="1" floor_next="--uses=BACKEND", and --uses=BACKEND # (the clue) lists the reading file. # F grep on a method whose only call the resolver DECLINED (two same-named defs in other dirs, an @@ -210,6 +211,7 @@ ROW_R2="$( enc_row "$OUT_R2" 'Walker::walk' )"; ROW_R3="$( enc_row "$OUT_R2" wal case "$ROW_R2" in *callers_floor=*) no "(R) Python self.walk() recursion is floored: $ROW_R2" ;; *) ok "(R) Python method recursing via self.walk(): no callers_floor" ;; esac case "$ROW_R3" in *callers_floor=*) no "(R) Python recursive module function is floored: $ROW_R3" ;; *) ok "(R) Python recursive module function walk_tree: no callers_floor" ;; esac R_R3="$( root_tag "$( run pyrec --safe-delete=walk_tree )" safe-delete )" +[ -n "$R_R3" ] && [ "$( attr "$R_R3" callers )" = 0 ] || no "(R) premise: walk_tree: $R_R3" # an empty answer must not read as "not floored" case "$R_R3" in *callers_floor=*|*' next='*) no "(R) --safe-delete=walk_tree is floored: $R_R3" ;; *) ok "(R) --safe-delete=walk_tree: no callers_floor, no next=" ;; esac OUT_RM="$( run crec --grep='n ? p' )"; ROW_PING="$( enc_row "$OUT_RM" pong )"; ROW_PONG="$( enc_row "$OUT_RM" ping )" [ "$( attr "$ROW_PING" callers )" = 1 ] && [ "$( attr "$ROW_PONG" callers )" = 2 ] || no "(R) premise: mutual recursion counts: [$ROW_PING] [$ROW_PONG]" @@ -223,10 +225,17 @@ OUT_C="$( run c --safe-delete=width_of )" ROW_PAD="$( printf '%s' "$OUT_C" | grep -oE ']*/>' | head -1 )" ROW_USER="$( printf '%s' "$OUT_C" | grep -oE ']*/>' | head -1 )" if [ "$( attr "$ROW_PAD" p )" = "a.c:9" ]; then ok "(C) p= still names the caller's definition line (a.c:9)"; else no "(C) p= changed on the pad row: $ROW_PAD"; fi -if [ "$( attr "$ROW_PAD" sites_l )" = "11,12,13" ]; then ok "(C) three calls on three lines: sites_l=\"11,12,13\""; else no "(C) pad row: $ROW_PAD"; fi -if [ "$( attr "$ROW_USER" sites_l )" = "22" ]; then ok "(C) a call continued onto its own line: sites_l=\"22\""; else no "(C) user row: $ROW_USER"; fi +if [ "$( attr "$ROW_PAD" sites_at )" = "a.c:11 a.c:12 a.c:13" ]; then ok "(C) three calls on three lines: sites_at=\"a.c:11 a.c:12 a.c:13\""; else no "(C) pad row: $ROW_PAD"; fi +if [ "$( attr "$ROW_USER" sites_at )" = "a.c:22" ]; then ok "(C) a call continued onto its own line: sites_at=\"a.c:22\""; else no "(C) user row: $ROW_USER"; fi +if [ -z "$ROW_PAD" ] || [ -z "$ROW_USER" ]; then + no "(C-twin) premise: the safe-delete answer printed no caller rows" +elif printf '%s' "$OUT_C" | grep -q ']*sites_l='; then + no "(C-twin) a safe-delete row still carries the bare-line sites_l=" +else + ok "(C-twin) safe-delete rows carry sites_at= only (the bare-line sites_l= stays on edit-check)" +fi ROW_TW="$( printf '%s' "$( run c --safe-delete=twice )" | grep -oE ']*/>' | head -1 )" -if [ "$( attr "$ROW_TW" sites_l )" = "20" ]; then ok "(D) a single-definition callee called once still prints its call site (20)"; else no "(D) twice row: $ROW_TW"; fi +if [ "$( attr "$ROW_TW" sites_at )" = "a.c:20" ]; then ok "(D) a single-definition callee called once still prints its call site (a.c:20)"; else no "(D) twice row: $ROW_TW"; fi R_C="$( root_tag "$OUT_C" safe-delete )" case "$R_C" in *callers_floor=*) no "(C) bound direct calls only, yet callers is floored: $R_C" ;; *) ok "(C) callers=\"2\" with no floor (every call bound)" ;; esac @@ -403,14 +412,16 @@ for dialect in compact full; do L_A="$( run c --safe-delete=cell --legend=$dialect )"; L_C="$( run c --safe-delete=width_of --legend=$dialect )" L_E="$( run py --grep=APP_BACKEND_ENV --legend=$dialect )" L_K="$( run tsl '--for=which router classes implement the Router interface' --legend=$dialect )" - for pair in "L_A:callers_floor=" "L_A:uses_floor=" "L_A:unmodelled" "L_C:sites_l=" "L_E:callers_floor=" "L_E:floor_next=" \ + 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 var="${pair%%:*}"; term="${pair#*:}" comments "${!var}" | grep -qF -- "$term" \ && ok "(N/$dialect) $term defined in the legend of the $var answer" || no "(N/$dialect) $term is not defined in the $var legend" done done -case "$( run c --safe-delete=helper_dead --legend=compact )" in *callers_floor*|*uses_floor*|*sites_l*) no "(N) a legend defines a floor/sites term its answer does not carry" ;; *) ok "(N) present-only: no floor/sites term on an answer without them" ;; esac +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 # ── O: determinism + xmllint ───────────────────────────────────────────────────────────────────────────── echo "=== O: determinism and well-formed XML ===" diff --git a/test/cpptmplscopecheck.sh b/test/cpptmplscopecheck.sh index 829b94e17..d845d5943 100755 --- a/test/cpptmplscopecheck.sh +++ b/test/cpptmplscopecheck.sh @@ -484,14 +484,14 @@ PCALL="$( run plain --callers=Box::grow --no-cache --legend=compact )" [ "$( printf '%s' "$PMAP" | grep -oE ']*>' | sed 's/ k="[^"]*"//' )" = '' ] \ && ok "control: declaration + out-of-line definition are ONE map row, sc=\"Box\" overloads=\"2\"" \ || no "control map row moved — the join this gate mirrors is not what it was: $( printf '%s' "$PMAP" | grep -oE ']*>' | tr '\n' ' ' )" -{ [ "$( defs "$PCALL" )" = 2 ] && [ "$( cnt "$PCALL" )" = 1 ] && printf '%s' "$PCALL" | grep -q ''; } \ +{ [ "$( defs "$PCALL" )" = 2 ] && [ "$( cnt "$PCALL" )" = 1 ] && printf '%s' "$PCALL" | grep -q ''; } \ && ok "control: --callers=Box::grow defs=\"2\" count=\"1\" -> use (box.hpp:8)" \ || no "control: --callers=Box::grow expected defs=2 count=1 use, got: $( el "$PCALL" )" # ── §2 THE CONTRAST: the template twin answers byte-identically ───────────────────────────────────────────────── TMAP="$( run templ --no-cache --legend=compact )" TCALL="$( run templ --callers=Box::grow --no-cache --legend=compact )" -{ [ "$( defs "$TCALL" )" = 2 ] && [ "$( cnt "$TCALL" )" = 1 ] && printf '%s' "$TCALL" | grep -q ''; } \ +{ [ "$( defs "$TCALL" )" = 2 ] && [ "$( cnt "$TCALL" )" = 1 ] && printf '%s' "$TCALL" | grep -q ''; } \ && ok "templ: --callers=Box::grow defs=\"2\" count=\"1\" -> use — the definition and the declaration are one member" \ || no "templ: --callers=Box::grow expected defs=2 count=1 use (the control's answer), got: $( el "$TCALL" )" [ "$( printf '%s' "$TMAP" | grep -oE ']*>' | sed 's/ k="[^"]*"//' )" = '' ] \ diff --git a/test/crashsweepcheck.sh b/test/crashsweepcheck.sh index d2b1cc35f..33a3b4912 100755 --- a/test/crashsweepcheck.sh +++ b/test/crashsweepcheck.sh @@ -294,6 +294,8 @@ cat > "$TMP/registry.tsv" <<'REGISTRY' ccjson.h ccCountLoc fopen 1 closes the only early exit is the failed open; nothing between open and fclose allocates clones.h findClones fopen 1 closes skips only a failed open; fclose right after the sized read clones.h findClonesType3 fopen 1 closes same shape as findClones +crossref.h catFileBatch fopen 1 closes returns only on a failed open; fclose after the list is written +crossref.h catFileBatch popen 1 closes the read loop leaves by break or its own condition only; pclose then unlink crossref.h evalStray fopen 1 closes returns only on a failed open; fclose after the read loop crossref.h streamBlobs fopen 1 closes returns only on a failed open; fclose after the list is written crossref.h streamBlobs popen 1 closes the read loop leaves by break/continue only; pclose then unlink @@ -317,7 +319,7 @@ infra/os.h spawn_sh open 1 closes in the forked child: dup2 onto stdin, close, t infra/os.h read_small open 1 closes one read, then ::close before the only return past the failed-open one (Linux /proc and /sys files) ingest_cache.h openOnce open 1 owned ReadFd's destructor closes it ingest_cache.h saveCache fdopen 1 closes adopts ExclTempFile's released fd; fclose on its own line; a failed fdopen ::closes the fd -ingest_crawl.h collectGitIgnored popen 1 closes the overflow break still reaches pclose +ingest_crawl.h runGitIgnoreProbe popen 1 closes the overflow break still reaches pclose ingest_docpass.h publishDocBridgeBlob fdopen 1 closes adopts ExclTempFile's released fd; fclose on its own line; a failed fdopen ::closes the fd lintrules.h loadLintRules fopen 1 closes skips only a failed open; fclose after the sized read main.cpp dispatchMain fopen 1 closes returns only on a failed open; fclose after the read loop diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index f210cde53..c5ed41950 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -231,14 +231,40 @@ srcRow="$( grep -n 'engine\.' "$TMP/w2rows" | tail -1 | cut -d: -f1 )" # TWIN on the default listing (lean-answers lane): the same def rows in the same order (source before the doc), the # doc-quoting kind="ref" row not dropped but COUNTED — refs count= equals the whole list's kind="ref" rows. -"$BIN" "$R" --whereis=computeBudget >"$TMP/w2d" 2>/dev/null +# +# FOLD UPDATE (lane whereis-defs-fix): the three branches all inherit base's `int computeBudget( int frames )` at +# engine.cpp:3, so the definitions page lists that line ONCE, on the first branch by name, with refs="3" +# (foldSharedBranchDefs); the whole list above (--whereis-listing=all, the untouched twin) still prints each +# branch's own row. The arm now asserts the fold's arithmetic instead of row-for-row equality: every def row of the +# default is a def row of the whole list, in the same order, and the refs= counts (1 when absent) add up to the +# whole list's def rows — nothing is dropped, it is counted. +# +# FIX ROUND 2 (rule 4, by design): a branch row is now a PARSER's label, and a branch holding HEAD's own engine.h blob +# takes HEAD's index labels: the index lists the prototype `int computeBudget( int frames );` (engine.h:2) as a +# definition site, so it is kind="def" on the three branches as it already was on HEAD (the lexical test called the +# same bytes a ref on the branches only, because the line ends in `;`). With 8 def rows against 3 refs the defs page +# is no longer the SHORTER one (2470 B vs 2439 B), so the default serves the whole list; this arm's subject is the +# defs page's fold arithmetic, so it asks for that page by name (--whereis-listing=defs). The default's choice keeps +# its own arms (L15 and pagingsweepcheck). +"$BIN" "$R" --whereis=computeBudget --whereis-listing=defs >"$TMP/w2d" 2>/dev/null tr '<' '\n' <"$TMP/w2d" | sed -n 's/^hit ref="\([^"]*\)".* p="\([^"]*\)" l="\([0-9]*\)" kind="\([a-z]*\)".*/\1 \4 \2:\3/p' >"$TMP/w2dall" +tr '<' '\n' <"$TMP/w2d" | grep '^hit ' | grep 'kind="def"' | awk '{ n = 1; if( match( $0, / refs="[0-9]+"/ ) ) { n = substr( $0, RSTART + 7, RLENGTH - 8 ) } print n }' >"$TMP/w2dweights" W2REFS="$( grep -c ' ref ' "$TMP/w2all" )" -{ [ "$( grep ' def ' "$TMP/w2dall" )" = "$( grep ' def ' "$TMP/w2all" )" ] && ! grep -q ' ref ' "$TMP/w2dall" \ +W2DEFS_ALL="$( grep -c ' def ' "$TMP/w2all" )" +W2DEFS_SUM="$( awk '{s+=$1} END{print s+0}' "$TMP/w2dweights" )" +{ [ "$( grep ' def ' "$TMP/w2dall" )" = "$( grep ' def ' "$TMP/w2all" | awk '{ k = ( $1 == "HEAD" ) ? "HEAD" NR : $2 " " $3 } !seen[k]++' )" ] && ! grep -q ' ref ' "$TMP/w2dall" \ + && [ "$W2DEFS_SUM" = "$W2DEFS_ALL" ] && [ "$W2DEFS_ALL" -ge 2 ] \ && [ "$( sed -n 1p "$TMP/w2dall" )" = "HEAD def engine.cpp:3" ] && [ "$W2REFS" -ge 1 ] \ && grep -q "" \ - || { no "whereis (default listing twin): the default lost a def row or miscounted the refs"; cat "$TMP/w2dall"; grep -o ']*>' "$TMP/w2d"; } + && ok "whereis (default listing twin): the def rows of the whole list, source first, shared branch lines folded (refs= sums to $W2DEFS_ALL def rows); the $W2REFS kind=\"ref\" row(s), the doc row among them, counted by " \ + || { no "whereis (default listing twin): the default lost a def row, folded wrongly (refs= sum $W2DEFS_SUM vs $W2DEFS_ALL def rows) or miscounted the refs"; cat "$TMP/w2dall"; grep -o ']*>' "$TMP/w2d"; } +# The fold's witness on this fixture: the three branches' identical engine.cpp:3 line is one row, refs="3", on the +# first branch by name, and the whole list keeps all three (a count, never a drop). Fix round 2: engine.h:2 (HEAD's own +# blob on every branch, the index's prototype definition) folds the same way — two refs="3" rows. +W2FOLDED="$( tr '<' '\n' <"$TMP/w2d" | grep -c '^hit ref="feat-merged" .*kind="def" refs="3" ' )" +[ "$W2FOLDED" -eq 2 ] && [ "$( grep -c ' def engine.cpp:3' "$TMP/w2all" )" -eq 4 ] && [ "$( grep -c ' def engine.h:2' "$TMP/w2all" )" -eq 4 ] && ! grep -q ' refs="' "$TMP/w2" \ + && ok 'whereis (fold): the branches'"'"' shared engine.cpp:3 and engine.h:2 definitions are one row each, refs="3" on feat-merged; listing=all keeps all eight rows and carries no refs=' \ + || { no "whereis (fold): expected two feat-merged def rows with refs=\"3\" (got $W2FOLDED) and 4+4 engine.cpp:3/engine.h:2 def rows in the whole list"; cat "$TMP/w2dall"; } # The finding's own repro, on this repo, when it is a git tree deep enough to answer. Skipped rather # than failed on a shallow/absent checkout: this arm is a bonus over the fixture arms above, which @@ -256,6 +282,130 @@ else ok "whereis: repo root is not a git tree — real-repo arm skipped" fi +# ── 5b) --whereis on the OTHER refs: kind="def" only where a parser finds the definition, and the definitions page +# lists a line that several refs hold ONCE (refs="N") ────────────────────────────────────────────────────── +# The 26b sweep (ideas-fable-26b #9): with the definitions page listing every kind="def" row of every ref, a defs="1" +# symbol listed 2113 "definitions" — 59 of the first 60 were call sites that merely ended in `}` or `)` +# (`if( !task.empty() ) { out += escapeXml( task, esc ); }`), a string literal's `impl `, a comment's `def `, and every +# ref's own copy of the one real line. Its own fixture: three branches hold the same shapes.cpp / calls.js (shape-b +# byte-identical to shape-a, shape-c with one extra comment line so its lines are numbered differently), beside the +# call shapes of C++, Python, JS, a string literal, a shell comment and a using-declaration. HEAD defines the name in +# budget.cpp, so HEAD's label comes from the index. +# FIX ROUND 2: every branch row is now a PARSER's label too (labelBranchRowsByParse): a branch holding HEAD's own blob of +# a path takes HEAD's index labels, any other blob holding a parse-worthy line is parsed by the index's own extraction. +SH="$TMP/shapes"; mkdir -p "$SH" +gs(){ git -C "$SH" "$@" >/dev/null 2>&1; } +gs init -q -b main +gs config commit.gpgsign false +printf '#pragma once\nint computeBudget( int frames );\n' > "$SH/budget.h" +printf 'int computeBudget( int frames )\n{\n return frames * 16;\n}\n' > "$SH/budget.cpp" +gs add budget.h budget.cpp +gs commit -qm base +write_shapes(){ # $1 = extra leading comment lines for shapes.cpp (shifts every line number) + { printf '%s' "$1"; cat <<'EOF' +#include "budget.h" +static int computeBudget( int frames ) { return frames * 32; } +auto computeBudget( long frames ) -> long { return frames; } +int notADef( int total ) +{ + if( total == 0 ) { total += computeBudget( 1 ); } + if( total == 1 ) { computeBudget( 2 ); } + return computeBudget( 3 ); +} +EOF + } > "$SH/shapes.cpp" + printf 'if computeBudget( 7 ):\n pass\nx = computeBudget( 8 )\n' > "$SH/calls.py" + printf 'await computeBudget( 9 )\ncomputeBudget( 10 ).then( x => x )\nclass Budget {\n computeBudget( frames ) {\n return frames\n }\n}\n' > "$SH/calls.js" + printf 'w.write( " "$SH/w.cpp" + printf '# 0 def rows in computeBudget( 12 ) hits\n' > "$SH/note.sh" +} +gs checkout -qb shape-a +write_shapes "" +gs add shapes.cpp calls.py calls.js w.cpp note.sh +gs commit -qm "the shapes" +gs checkout -qb shape-b +printf 'unrelated\n' > "$SH/other.txt" +gs add other.txt +gs commit -qm "shape-b: the same shapes, plus an unrelated file" +gs checkout -q main +gs checkout -qb shape-c +write_shapes "// shape-c: one more line above, so every line number differs from shape-a's +" +gs add shapes.cpp calls.py calls.js w.cpp note.sh +gs commit -qm "shape-c: the same lines, numbered differently" +gs checkout -q main + +"$BIN" "$SH" --whereis=computeBudget >"$TMP/sd" 2>/dev/null; rcSd=$? +"$BIN" "$SH" --whereis=computeBudget --whereis-listing=all >"$TMP/sa" 2>/dev/null; rcSa=$? +rows_of(){ tr '<' '\n' <"$1" | sed -n 's/^hit ref="\([^"]*\)".* p="\([^"]*\)" l="\([0-9]*\)" kind="\([a-z]*\)"\( test_local="1"\)\{0,1\}\( refs="[0-9]*"\)\{0,1\} t=.*/\1 \4 \2:\3\6/p'; } +rows_of "$TMP/sd" >"$TMP/sdrows"; rows_of "$TMP/sa" >"$TMP/sarows" +if [ $rcSd -ne 0 ] || [ $rcSa -ne 0 ] || ! grep -q ' root (rc $rcSd / $rcSa)" +else + # (i) RED on 93aeab1d: the nine call shapes read kind="def" on every branch. Every branch def row is one of the + # three definition shapes — the one-line body, the trailing return and the JS method — and no other line. + # Fix round 2 (rule 4, by design): plus budget.h's prototype `int computeBudget( int frames );` — HEAD's own + # budget.h blob on every branch takes HEAD's index label, and the index lists that prototype as a definition + # site (HEAD said def and the branches said ref for the same bytes; now both say def). + DEF_T="$( tr '<' '\n' <"$TMP/sa" | grep '^hit ref="shape-' | grep 'kind="def"' | sed -n 's/.* t="\(.*\)"\/>$/\1/p' | sort -u )" + EXPECT_T="$( printf '%s\n' 'computeBudget( frames ) {' 'auto computeBudget( long frames ) -> long { return frames; }' 'static int computeBudget( int frames ) { return frames * 32; }' 'int computeBudget( int frames )' 'int computeBudget( int frames );' | sort -u )" + [ "$DEF_T" = "$EXPECT_T" ] \ + && ok 'whereis (shapes): on the branches only the definition shapes read kind="def" (base'"'"'s budget.cpp, the one-line body, the trailing return, the JS method)' \ + || { no 'whereis (shapes): a call shape reads kind="def" on a branch, or a definition shape lost its label'; printf '%s\n' "$DEF_T"; } + # (ii) each call shape is a kind="ref" row (present, labelled ref): the fix relabels, it never drops. + missing="" + for shape in 'total += computeBudget( 1 ); }' '{ computeBudget( 2 ); }' 'return computeBudget( 3 );' 'if computeBudget( 7 ):' 'x = computeBudget( 8 )' \ + 'await computeBudget( 9 )' 'computeBudget( 10 ).then' '<impl n=' '0 def rows' 'using ns::computeBudget;'; do + tr '<' '\n' <"$TMP/sa" | grep '^hit ref="shape-a"' | grep 'kind="ref"' | grep -qF "$shape" || missing="$missing [$shape]" + done + [ -z "$missing" ] \ + && ok 'whereis (shapes): the C++, Python, JS, string-literal, comment and using-declaration call shapes are kind="ref" rows on shape-a' \ + || { no "whereis (shapes): call shapes not found as kind=\"ref\" rows:$missing"; grep '^shape-a' "$TMP/sarows"; } + # (iii) the fold: the definitions page lists each shared line once, on shape-a (first by name), refs="3" — shape-c + # holds the same text at other line numbers and still folds (the key is path + text, not the line). + # The base's budget.cpp:1 is on every branch too, so four shared lines: HEAD's two index rows + four shape-a rows. + # Fix round 2: five — budget.h:2 (HEAD's index label on HEAD's own blob, see (i)) folds as well: folded="10". + [ "$( grep -c '^shape-a def .* refs="3"' "$TMP/sdrows" )" -eq 5 ] && ! grep -q '^shape-[bc] ' "$TMP/sdrows" \ + && [ "$( sed -n 1p "$TMP/sdrows" )" = "HEAD def budget.cpp:1" ] && [ "$( grep -c ' def ' "$TMP/sdrows" )" -eq 7 ] \ + && grep -q '^shape-c def shapes.cpp:3$' "$TMP/sarows" && grep -q '^shape-a def shapes.cpp:2 refs="3"' "$TMP/sdrows" \ + && grep -o ']*>' "$TMP/sd" | grep -q ' listing="defs" folded="10" ' \ + && ok 'whereis (fold): the five shared definition lines are one row each on shape-a with refs="3" (folded="10" on the root); shape-c holds one at another line number and folds by text' \ + || { no 'whereis (fold): expected HEAD + five shape-a rows refs="3" and folded="10" on the definitions page'; cat "$TMP/sdrows"; grep -o ']*>' "$TMP/sd"; } + # (iv) the whole list is the untouched twin: every branch's own row, no refs=/folded= anywhere, rows == hits=. + SA_HITS="$( grep -o ']*' "$TMP/sa" | grep -o ' hits="[0-9]*"' | grep -o '[0-9]*' )" + SA_ROWS="$( grep -c . "$TMP/sarows" )" + # Fix round 2: 17 def rows = HEAD's 2 + 3 branches x 5 (budget.h:2 included, see (i)). + [ -n "$SA_HITS" ] && [ "$SA_ROWS" = "$SA_HITS" ] && ! grep -q ' refs="' "$TMP/sa" && ! grep -q ' folded="' "$TMP/sa" && [ "$( grep -c ' def ' "$TMP/sarows" )" -eq 17 ] \ + && ok "whereis (fold twin): --whereis-listing=all prints every row ($SA_ROWS == hits=$SA_HITS), 17 def rows, no refs= or folded=" \ + || { no "whereis (fold twin): rows $SA_ROWS vs hits=$SA_HITS, def rows $( grep -c ' def ' "$TMP/sarows" ) (want 17), or refs=/folded= leaked into the whole list"; grep -o ']*>' "$TMP/sa"; } + # (v) the counts agree across the two pages: hits= identical, and on the default equals the whole list's ref rows. + SD_HITS="$( grep -o ']*' "$TMP/sd" | grep -o ' hits="[0-9]*"' | grep -o '[0-9]*' )" + [ "$SD_HITS" = "$SA_HITS" ] && grep -q " equals the whole list's kind=\"ref\" rows" \ + || { no "whereis (fold): hits= or disagree between the pages"; grep -o ']*>' "$TMP/sd"; grep -o ']*>' "$TMP/sd"; } + # (vi) the legend defines refs= where it rides (full prose and compact dictionary), and the full page stays well-formed XML + # with the SHARED DEFINITIONS clause spliced into its comment (checklist 25: no `--` inside it). + "$BIN" "$SH" --whereis=computeBudget --legend=full >"$TMP/sdf" 2>/dev/null + "$BIN" "$SH" --whereis=computeBudget --legend=compact >"$TMP/sdc" 2>/dev/null + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$TMP/sdf" 2>/dev/null && grep -q 'SHARED DEFINITIONS: refs="N"' "$TMP/sdf" \ + && ok 'whereis (fold legend): the full legend defines refs= (SHARED DEFINITIONS) and the page is well-formed XML' \ + || no 'whereis (fold legend): the full page is malformed or its legend does not define refs=' + else + printf ' SKIP whereis (fold legend): xmllint absent — the full page'"'"'s well-formedness is not checked\n' # rv N2: a missing tool is a named SKIP + grep -q 'SHARED DEFINITIONS: refs="N"' "$TMP/sdf" && ok 'whereis (fold legend): the full legend defines refs= (SHARED DEFINITIONS)' \ + || no 'whereis (fold legend): the full legend does not define refs=' + fi + grep -q 'hit refs=N' "$TMP/sdc" \ + && ok 'whereis (fold legend): the compact dictionary defines hit refs=N on the page that carries it' \ + || no 'whereis (fold legend): the compact dictionary lacks the hit refs=N reading' + "$BIN" "$SH" --whereis=computeBudget --whereis-listing=all --legend=compact >"$TMP/sac" 2>/dev/null; rcSac=$? + # rv N1: an ABSENCE is read only off a run that succeeded and printed its root. + [ $rcSac -eq 0 ] && grep -q '/dev/null 2>&1; [ $? -eq 1 ]; then ok "bare --whereis refuses loudly (exit 1)"; else no "bare --whereis did not exit 1"; fi mkdir -p "$TMP/plain"; printf 'int main(){return 0;}\n' > "$TMP/plain/m.cpp" @@ -367,16 +517,25 @@ else no "§B8.2 whereis: --limit=1 produced no to check (fixture has too few hits)" fi # TWIN on the default listing: the same arithmetic, the counted refs included — shown + more + refs count == hits. -W1D="$( "$BIN" "$R" --whereis=computeBudget --limit=1 2>/dev/null )" +# Fix round 2 (by design, see the default listing twin in 5b): on this fixture the defs page is no longer the shorter +# one, so the default serves the whole list; the arm names the defs page (--whereis-listing=defs) whose arithmetic it pins. +W1D="$( "$BIN" "$R" --whereis=computeBudget --whereis-listing=defs --limit=1 2>/dev/null )" WD_SHOWN="$( printf '%s' "$W1D" | grep -oE ']*' | grep -oE 'shown="[0-9]+"' | grep -oE '[0-9]+' )" WD_HITS="$( printf '%s' "$W1D" | grep -oE ']*' | grep -oE ' hits="[0-9]+"' | grep -oE '[0-9]+' )" WD_MORE="$( printf '%s' "$W1D" | grep -oE '//' | grep -oE '/dev/null )" +# FOLD UPDATE (lane whereis-defs-fix): the branches' shared definition line is one row (refs="3"), and the root's folded= +# counts the two rows it stands for — shown + more + folded + refs count == hits. The whole-list arm above is the +# untouched twin (no folded= there: rows == hits). +WD_FOLDED="$( printf '%s' "$W1D" | grep -oE ']*' | grep -oE ' folded="[0-9]+"' | grep -oE '[0-9]+' )" +{ [ -n "$WD_MORE" ] && [ -n "$WD_REFS" ] && [ "$(( WD_SHOWN + WD_MORE + ${WD_FOLDED:-0} + WD_REFS ))" = "$WD_HITS" ] && [ "$WD_ROWS" = "$WD_SHOWN" ]; } \ + && ok "§B8.2 whereis (default listing twin): shown($WD_SHOWN) + more($WD_MORE) + folded(${WD_FOLDED:-0}) + refs count($WD_REFS) == hits($WD_HITS)" \ + || no "§B8.2 whereis (default listing twin): shown=$WD_SHOWN more=$WD_MORE folded=${WD_FOLDED:-0} refs=$WD_REFS hits=$WD_HITS rows=$WD_ROWS" +[ "$WD_FOLDED" = "4" ] \ + && ok '§B8.2 whereis (default listing twin): folded="4" on the root — the two branch copies each of engine.cpp:3 and engine.h:2 behind the two refs="3" rows' \ + || no "§B8.2 whereis (default listing twin): folded=\"${WD_FOLDED:-absent}\" on the root, want 4" +WDEND="$( "$BIN" "$R" --whereis=computeBudget --whereis-listing=defs --limit=1 --offset="$WD_HITS" 2>/dev/null )" if ! printf '%s' "$WDEND" | grep -q ' root" elif printf '%s' "$WDEND" | grep -q '/dev/null 2>&1; git -C "$d" config commit.gpgsign false printf 'int zqTie( int x ) { return x; }\n' >"$d/tie.c" - printf 'int useT( void ) { return zqTie( 1 ); } // %s\n' "$( head -c "$1" "$d/use.c" + # The padding is split over TWO ref rows (lane whereis-lean): a t= over 300 bytes prints a 200-byte preview, so one + # row padded by the whole overhead (~330 B) would stop growing a byte per pad byte; two rows of half each stay whole. + local h=$(( $1 / 2 )) + { printf 'int useT( void ) { return zqTie( 1 ); } // t%s\n' "$( head -c "$h" "$d/use.c" git -C "$d" add -A >/dev/null 2>&1; git -C "$d" commit -qm tie >/dev/null 2>&1; printf '%s' "$d"; } tw(){ "$BIN" "$1" --whereis=zqTie --no-cache "${@:2}" 2>/dev/null; } TP="$( mk_tie 1 )"; TD=$(( $( nbytes "$( tw "$TP" --whereis-listing=defs )" ) - $( nbytes "$( tw "$TP" --whereis-listing=all )" ) )) @@ -690,15 +853,24 @@ fi # explicit pages of the same binary, byte for byte. defs_in(){ hits_of def | wc -l | tr -d ' '; } dw(){ "$BIN" "$1" --whereis="$2" --no-cache "${@:3}" 2>/dev/null; } -# (L15) the cap bites: HEAD defines zqCap once and calls it 70 times; 64 branches sit at the previous commit (same files) -# and HEAD adds one unrelated commit, so every branch row carries tip=/date=. The all page lists HEAD's definition and 59 -# references (60 rows), the defs page 60 definitions, and the defs page is the LONGER one in bytes — the default is defs. +# (L15) the cap bites: HEAD defines zqCap once and calls it 70 times; 64 branches each hold their OWN definition line +# (`return x + N`: 64 distinct lines, so none folds into another — foldSharedBranchDefs lists a line several refs share +# once, which is the lane whereis-defs-fix twin below), and HEAD adds one unrelated commit, so every branch row carries +# tip=/date=. The all page lists HEAD's definition and 59 references (60 rows), the defs page 60 definitions, and the +# defs page is the LONGER one in bytes — the default is defs. D15="$TMP/cap15"; mkdir -p "$D15"; git -C "$D15" init -q -b main >/dev/null 2>&1; git -C "$D15" config commit.gpgsign false printf 'int zqCap( int x ) { return x; }\n' >"$D15/def.c" { printf 'int useAll( void )\n{\n int s = 0;\n'; i=0; while [ $i -lt 70 ]; do printf ' s += zqCap( %d );\n' $i; i=$((i+1)); done; printf ' return s;\n}\n'; } >"$D15/use.c" printf '// head\n' >"$D15/other.c" git -C "$D15" add -A >/dev/null 2>&1; git -C "$D15" commit -qm cap1 >/dev/null 2>&1 -i=0; while [ $i -lt 64 ]; do git -C "$D15" branch "b$i" >/dev/null 2>&1; i=$((i+1)); done +i=0; while [ $i -lt 64 ]; do + git -C "$D15" checkout -qb "b$i" >/dev/null 2>&1 + printf 'int zqCap( int x ) { return x + %d; }\n' $i >"$D15/def.c" + git -C "$D15" commit -qam "b$i" >/dev/null 2>&1 + git -C "$D15" checkout -q main >/dev/null 2>&1 + i=$((i+1)) +done +printf 'int zqCap( int x ) { return x; }\n' >"$D15/def.c" printf '// head, later\n' >"$D15/other.c"; git -C "$D15" add -A >/dev/null 2>&1 GIT_COMMITTER_DATE="2026-02-02T00:00:00Z" GIT_AUTHOR_DATE="2026-02-02T00:00:00Z" git -C "$D15" commit -qm cap2 >/dev/null 2>&1 C_D="$( dw "$D15" zqCap )"; C_A="$( dw "$D15" zqCap --whereis-listing=all )"; C_F="$( dw "$D15" zqCap --whereis-listing=defs )" @@ -711,6 +883,24 @@ if [ "$CD_F" -gt "$CD_A" ] 2>/dev/null && [ "$( nbytes "$C_F" )" -gt "$( nbytes else no "LEAN (L15): premises not met: defs shown $CD_F vs all $CD_A, bytes $( nbytes "$C_F" ) vs $( nbytes "$C_A" ), all capped?" fi +# (L15-FOLD, lane whereis-defs-fix) the same shape with the 64 branches at ONE commit: their 64 copies of the definition +# line are one row refs="64" (folded="63" on the root), the defs page is now the SHORTER page (2 rows) and is still the +# default — it lists more definitions (65 represented) than the capped all page (HEAD's one). RED on 93aeab1d: 60 rows, +# every one the same line, 4 more behind . +D15F="$TMP/cap15fold"; mkdir -p "$D15F"; git -C "$D15F" init -q -b main >/dev/null 2>&1; git -C "$D15F" config commit.gpgsign false +cp "$D15/def.c" "$D15/use.c" "$D15F/"; printf '// head\n' >"$D15F/other.c" +git -C "$D15F" add -A >/dev/null 2>&1; git -C "$D15F" commit -qm cap1 >/dev/null 2>&1 +i=0; while [ $i -lt 64 ]; do git -C "$D15F" branch "b$i" >/dev/null 2>&1; i=$((i+1)); done +printf '// head, later\n' >"$D15F/other.c"; git -C "$D15F" add -A >/dev/null 2>&1 +GIT_COMMITTER_DATE="2026-02-02T00:00:00Z" GIT_AUTHOR_DATE="2026-02-02T00:00:00Z" git -C "$D15F" commit -qm cap2 >/dev/null 2>&1 +C_FD="$( dw "$D15F" zqCap )" +if printf '%s' "$C_FD" | lroot | grep -q ' listing="defs" folded="63" ' && [ "$( printf '%s' "$C_FD" | defs_in )" = "2" ] \ + && [ "$( printf '%s' "$C_FD" | tr '<' '\n' | grep -c '^hit ref="b0" .*kind="def" refs="64" ' )" = "1" ] \ + && ! printf '%s' "$C_FD" | grep -q '' +else + no 'LEAN (L15-FOLD): the shared definition line is not folded into one refs="64" row on a 2-row defs page'; printf '%s\n' "$C_FD" | lroot; printf '%s' "$C_FD" | tr '<' '\n' | grep -c '^hit ' +fi # (L15-MCP) the same symbol over MCP (no `listing`): byte-identical to the CLI defs page. if command -v python3 >/dev/null 2>&1; then { [ -n "$C_F" ] && [ "$( mcpq "$D15" zqCap '' )" = "$C_F" ] && [ "$( mcpq "$D15" zqCap ',"listing":"all"' )" = "$C_A" ]; } \ @@ -745,6 +935,821 @@ else printf ' SKIP LEAN (L16-MCP) MCP twin (no python3)\n' fi +# ── 5c) PARSED BRANCH LABELS (lane whereis-defs-fix, fix round 2; crossref.h labelBranchRowsByParse) ────────────── +# A row of another ref is kind="def" only where a PARSER finds the definition: a branch holding HEAD's own blob of a path +# takes HEAD's index labels; any other blob holding a parse-worthy line is parsed by the index's own extraction +# (ingest.h definitionLinesInBlobs). The lexical shape test only CHOOSES blobs. One well-formed file per language family +# under head/ on main; byte-identical copies under sib/ on branch `sib` (another path: those blobs are PARSED, not +# mirrored); and under c1/ the definition shapes the round-1 lexical test lost (rv-whereis-defs-fix C1). Every line naming +# probeName carries @D (a definition) or @R (a reference). Two claims per marked line: the branch row says what the +# marker says, and it says what HEAD's index row says for the same bytes. RED on 93aeab1d (18 branch rows wrong: the C2 +# call shapes read def) and on f1f11a7b (22: the C1 definition shapes read ref). +PB="$TMP/parsedlabels"; mkdir -p "$PB" +gp(){ git -C "$PB" "$@" >/dev/null 2>&1; } +mkp(){ mkdir -p "$( dirname "$PB/$1" )"; cat >"$PB/$1"; } +gp init -q -b main; gp config commit.gpgsign false +mkp head/a.c <<'EOF' +#define probeName( a ) ( (a) + 1 ) /* @D */ +int caller( int a ) +{ + if( a ) { a = probeName( a ); } /* @R */ + return probeName( a ); /* @R */ +} +EOF +mkp head/b.cpp <<'EOF' +#include +struct T { int probeName( int a ) const; }; // @D +int T::probeName( int a ) const { return a; } // @D +void run( T& t, int a ) +{ + t.probeName( a ); // @R + if( a ) { a = t.probeName( a ); } // @R + auto f = [ & ]( int x ) { return t.probeName( x ); }; // @R + // probeName( a ) { // @R +} +EOF +mkp head/c.m <<'EOF' +@interface Box +- (int)probeName:(int)a; // @R (an @interface declaration its same-file @implementation shadows: one symbol, the definition) +@end +@implementation Box +- (int)probeName:(int)a { return a; } // @D +- (void)run { [self probeName:1]; } // @R +@end +EOF +mkp head/d.py <<'EOF' +def probeName(a): # @D + return a + + +class K: + async def other(self): + return probeName(1) # @R + + +@probeName # @R +def deco(): + x = probeName(2) # @R + print("probeName(x)") # @R +EOF +mkp head/e.ts <<'EOF' +export function probeName(a: number): number { // @D + return a; +} +export class Svc { + static probeName(a: number) { return a; } // @D + run(a: number) { + probeName(a).toString(); // @R + setTimeout(() => probeName(a), 0); // @R + } +} +EOF +mkp head/f.js <<'EOF' +function probeName(a) { // @D + return a; +} +describe('x', () => { + probeName('x', () => { // @R + return 1; + }); +}); +EOF +mkp head/G.java <<'EOF' +public class G { + public static int probeName(int a) { // @D + return a; + } + int run(int a) { + return probeName(a); // @R + } +} +EOF +mkp head/h.rb <<'EOF' +class H + def probeName(a) # @D + a + end + def run(a) + puts probeName(a) # @R + probeName(a) do |x| # @R + x + end + end +end +EOF +mkp head/i.go <<'EOF' +package p + +func probeName(a int) int { // @D + return a +} + +func run(a int) { + go probeName(a) // @R + defer probeName(a) // @R + if v := probeName(a); v > 0 { // @R + } +} +EOF +mkp head/j.rs <<'EOF' +pub fn probe_other() {} +pub fn probeName(a: i32) -> i32 { // @D + a +} +fn run(a: i32) -> i32 { + let v = probeName(a); // @R + probeName(v) // @R +} +EOF +mkp head/k.swift <<'EOF' +func probeName(_ a: Int) throws -> Int { // @D + return a +} +func run() throws { + let v = try probeName(1) // @R + _ = v +} +EOF +mkp head/l.cs <<'EOF' +public class L +{ + public static int probeName(int a) { return a; } // @D + public int Run(int a) + { + return probeName(a); // @R + } +} +EOF +mkp head/m.sh <<'EOF' +probeName() { # @D + echo "$1" +} +run() { + probeName x # @R +} +EOF +mkp head/n.cpp <<'EOF' +struct N { int probeName; }; // @R (a field: not a symbol in the index, so never a def site on HEAD either) +EOF +mkp head/o.md <<'EOF' +# probeName + +Call `probeName()` from the loop. +EOF +# The C2 call shapes and the review's checklist-29 near-misses (rv-whereis-defs-fix2 X3), each beside a true definition +# of the same name in a well-formed file of its language, so each file reads both ways: the definition def, the call ref. +# The call lines carry NO marker (an unmarked line wants ref): a trailing comment would change the shape a lexical reading +# sees, and the premise below is the bare shape. +# RED on 93aeab1d (its lexical reading calls several of these calls definitions). +mkp head/q.py <<'EOF' +def probeName(n): # @D + def wrap(f): + return f + return wrap + + +@probeName(1) +def other(): + return 2 +EOF +mkp head/P.java <<'EOF' +public class P { + public static int probeName(int a) { // @D + return a; + } + @probeName(value = 1) + int run(int a) { + return a; + } +} +EOF +mkp head/t.c <<'EOF' +#define probeName( x ) ( (x) + 1 ) /* @D */ +#define WRAP( x ) probeName( x ) +int useA( int y ) +{ + if( probeName( y ) > 2 ) { + return WRAP( y ); + } + return 0; +} +EOF +mkp head/u.cpp <<'EOF' +template int probeName( int a, F f ) { return f( a ); } // @D +/** + * @see probeName( a ) + */ +int run( int y ) +{ + return probeName( y, [&]( int x ) { + return x; + } ); +} +EOF +mkp head/v.cpp <<'EOF' +struct probeName { int v; }; // @D +class Foo : public probeName { +}; +EOF +mkp head/w.js <<'EOF' +export function probeName(a) { // @D + return a; +} +function suite() { + probeName(function () { + return 1; + }); + probeName('y', () => { + return 2; + }); +} +export default probeName(3); +EOF +mkp head/x.ts <<'EOF' +export function probeName(n: number) { // @D + return (t: any, k: string) => {}; +} +export class Box { + @probeName(1) handle() { + return 3; + } +} +EOF +mkp head/y.rb <<'EOF' +def probeName(a) # @D + a +end +def other(probeName) + 1 +end +EOF +gp add -A; gp commit -qm base +gp checkout -qb sib +mkdir -p "$PB/sib"; cp -R "$PB/head/." "$PB/sib/" +mkp c1/m1.h <<'EOF' +#define probeName( a ) ( (a) + 1 ) // @D +EOF +mkp c1/m2.h <<'EOF' +#define probeName( a ) do { f( a ); } while( 0 ) // @D +EOF +mkp c1/d1.cpp <<'EOF' +decltype(auto) probeName( int a ) { return a; } // @D +EOF +mkp c1/d2.cpp <<'EOF' +__attribute__((noinline)) static int probeName( int a ) { return a; } // @D +EOF +mkp c1/d3.cpp <<'EOF' +#include +std::function probeName( int a ) { return [](){}; } // @D +EOF +mkp c1/d4.cpp <<'EOF' +template T probeName( T a ) { return a; } // @D +EOF +mkp c1/T.java <<'EOF' +public class T { + @Test(expected = Foo.class) public void probeName() { // @D + } +} +EOF +mkp c1/c.ts <<'EOF' +export class C { + @HostListener('click') probeName() { // @D + } +} +EOF +mkp c1/a.cs <<'EOF' +public class A +{ + [HttpGet("x")] public IActionResult probeName() { return null; } // @D +} +EOF +mkp c1/s.rs <<'EOF' +#[derive(Debug)] pub struct probeName { // @D + a: i32, +} +EOF +mkp c1/e.js <<'EOF' +module.exports = function probeName(req, res) { // @D + return req; +}; +EOF +mkp c1/h.js <<'EOF' +const handler = function probeName() { // @D + return 1; +}; +EOF +# the reviewer's branch-only repro (rv C1 repro 1): names HEAD never holds, defined only on this branch +printf '#define clampBudget( x ) ( (x) > 9 ? 9 : (x) )\n' >"$PB/c1/b.h" +printf 'int use( int a ) { return clampBudget( a ); }\n' >"$PB/c1/u.c" +printf 'public class T2 {\n @Test(timeout = 10) public void checkBudget() {\n }\n}\n' >"$PB/c1/T2.java" +gp add -A; gp commit -qm sib; gp checkout -q main +"$BIN" "$PB" --whereis=probeName --whereis-listing=all --limit=1000 --no-cache >"$TMP/pb.all" 2>/dev/null; rcPb=$? +if [ $rcPb -ne 0 ] || ! grep -q ' root (rc $rcPb)" +else + sed 's///' "$TMP/pb.all" | tr '<' '\n' | sed -n 's/^hit ref="\([^"]*\)".* p="\([^"]*\)" l="\([0-9]*\)" kind="\([a-z]*\)".*/\1 \2:\3 \4/p' >"$TMP/pb.kinds" + PB_WRONG=""; PB_SPLIT=""; PB_N=0 + for f in $( git -C "$PB" ls-tree -r --name-only sib -- head c1 | grep -v '^c1/\(b\.h\|u\.c\|T2\.java\)$' ); do + case "$f" in head/*) bp="sib/${f#head/}"; hp="$f";; *) bp="$f"; hp="";; esac + ln=0 + while IFS= read -r line || [ -n "$line" ]; do + ln=$((ln+1)) + case "$line" in *probeName*) ;; *) continue;; esac + want=ref; case "$line" in *@D*) want=def;; esac + case "$f:$line" in *.md:"# probeName") want=def;; esac # a markdown heading is a section symbol (no room for a marker) + bk="$( awk -v k="$bp:$ln" '$1=="sib" && $2==k {print $3}' "$TMP/pb.kinds" | head -1 )" + PB_N=$((PB_N+1)) + [ "$bk" = "$want" ] || PB_WRONG="$PB_WRONG [$bp:$ln $bk want $want]" + if [ -n "$hp" ]; then + hk="$( awk -v k="$hp:$ln" '$1=="HEAD" && $2==k {print $3}' "$TMP/pb.kinds" | head -1 )" + [ "$hk" = "$bk" ] || PB_SPLIT="$PB_SPLIT [$bp:$ln branch $bk HEAD $hk]" + fi + done < <( git -C "$PB" show "sib:$f" ) + done + { [ "$PB_N" -ge 50 ] && [ -z "$PB_WRONG" ]; } \ + && ok "whereis (parsed labels): $PB_N marked lines over C, C++, ObjC, Python, TS, JS, Java, Ruby, Go, Rust, Swift, C#, Bash and Markdown, plus the 12 C1 definition shapes — every branch row says what its marker says" \ + || no "whereis (parsed labels): branch rows mislabelled (of $PB_N):$PB_WRONG" + [ -z "$PB_SPLIT" ] \ + && ok 'whereis (parsed labels): every sib/ branch row says what HEAD'"'"'s index row says for the same bytes (the same way HEAD is labelled)' \ + || no "whereis (parsed labels): the branch parse and HEAD's index disagree on the same bytes:$PB_SPLIT" + grep -o ']*>' "$TMP/pb.all" | grep -q ' ref_labels="parsed" ' && ! grep -q ' (every parse-worthy blob was read)' \ + || no 'whereis (parsed labels): ref_labels= missing, or an element on an answer the guard never touched' +fi +# the branch-only definitions (rv C1 repro 1): the definition is the answer, and the call beside it is not +CB="$( "$BIN" "$PB" --whereis=clampBudget --whereis-listing=all --no-cache 2>/dev/null )" +KB="$( "$BIN" "$PB" --whereis=checkBudget --whereis-listing=all --no-cache 2>/dev/null )" +{ printf '%s' "$CB" | grep -q ']* on-head="0"' && printf '%s' "$CB" | grep -q ']*p="c1/b.h" l="1" kind="def"' \ + && printf '%s' "$CB" | grep -q ']*p="c1/u.c" l="1" kind="ref"' \ + && printf '%s' "$KB" | grep -q ']*p="c1/T2.java" l="2" kind="def"'; } \ + && ok 'whereis (branch-only): `#define clampBudget( x )` and `@Test(timeout = 10) public void checkBudget()` read kind="def" on the only ref holding them; the call beside the macro reads kind="ref"' \ + || { no 'whereis (branch-only): a branch-only definition lost its label, or its call site gained one'; printf '%s\n%s\n' "$CB" "$KB" | sed 's///'; } +# "One the parser cannot finish" (CHANGELOG, the UNPARSED clause): a branch .astro that opens a frontmatter fence and never +# closes it confirms nothing: its parse-worthy line reads kind="text", never def, and counts +# it with no next= (the guard left nothing, so there is nothing to lift). Its closed twin reads def. RED on 93aeab1d (no +# ; the unclosed line read def). +AS="$TMP/astro"; mkdir -p "$AS"; git -C "$AS" init -q -b main >/dev/null 2>&1; git -C "$AS" config commit.gpgsign false +printf 'x\n' >"$AS/a.txt"; git -C "$AS" add -A >/dev/null 2>&1; git -C "$AS" commit -qm base >/dev/null 2>&1; git -C "$AS" checkout -qb feat >/dev/null 2>&1 +printf -- '---\nfunction astroName(a) { return a; }\n---\n

{astroName(1)}
\n' >"$AS/ok.astro" +printf -- '---\nfunction astroOpen(a) { return a; }\n
{astroOpen(1)}
\n' >"$AS/p.astro" +git -C "$AS" add -A >/dev/null 2>&1; git -C "$AS" commit -qm feat >/dev/null 2>&1; git -C "$AS" checkout -q main >/dev/null 2>&1 +AO="$( "$BIN" "$AS" --whereis=astroOpen --whereis-listing=all --no-cache 2>/dev/null )"; rcAo=$? +AN="$( "$BIN" "$AS" --whereis=astroName --whereis-listing=all --no-cache 2>/dev/null )"; rcAn=$? +if [ $rcAo -ne 0 ] || [ $rcAn -ne 0 ] || ! printf '%s' "$AO" | grep -q ' root (rc $rcAo / $rcAn)" +else + { printf '%s' "$AO" | grep -q '' && printf '%s' "$AO" | grep -q ']*p="p.astro" l="2" kind="text"' \ + && ! printf '%s' "$AO" | grep -q 'kind="def"' \ + && printf '%s' "$AN" | grep -q ']*p="ok.astro" l="2" kind="def"' && ! printf '%s' "$AN" | grep -q ' (no next=); its closed twin reads def' \ + || { no 'whereis (parser could not finish): the unclosed .astro blob was labelled, or not disclosed, or the closed twin lost its def'; printf '%s\n%s\n' "$AO" "$AN" | sed 's///'; } +fi +# ref_labels= rides only an answer with another ref's row: a repo with no other ref answers as before +NR="$TMP/norefs"; mkdir -p "$NR"; git -C "$NR" init -q -b main >/dev/null 2>&1; git -C "$NR" config commit.gpgsign false +printf 'int lonelyName( int a ) { return a; }\nint z( void ) { return lonelyName( 1 ); }\n' >"$NR/x.c" +git -C "$NR" add -A >/dev/null 2>&1; git -C "$NR" commit -qm one >/dev/null 2>&1 +NRO="$( "$BIN" "$NR" --whereis=lonelyName --no-cache 2>/dev/null )"; rcNr=$? +[ $rcNr -eq 0 ] && printf '%s' "$NRO" | grep -q ' rides an answer without a branch row' +# CLI = MCP: the MCP whereis default is byte-identical to the CLI default on the parsed fixture +if command -v python3 >/dev/null 2>&1; then + PBD="$( "$BIN" "$PB" --whereis=probeName --no-cache 2>/dev/null )" + PBM="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whereis","arguments":{"path":"%s","symbol":"probeName"}}}\n' "$PB" \ + | "$BIN" --mcp 2>/dev/null | python3 -c 'import sys,json +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line); c=d.get("result",{}).get("content") + if c: print(c[0].get("text",""),end="")' )" + [ -n "$PBD" ] && [ "$PBD" = "$PBM" ] \ + && ok 'whereis (parsed labels): MCP whereis serves the CLI default byte for byte (one labeller on both surfaces)' \ + || no 'whereis (parsed labels): the MCP answer differs from the CLI default on the parsed fixture' +else + printf ' SKIP whereis (parsed labels) MCP twin (no python3)\n' +fi + +# ── 5d) the RUNAWAY GUARD on the parse batch (kWhereisParseMaxBlobs = 256) and what it leaves ───────────────────────── +# 300 distinct blobs on one branch, each a one-line definition of guardName (g.h is HEAD's own blob there: mirrored, never +# in the batch): the batch takes 256 (most rows first, then path: f000..f255), the other 44 confirm NOTHING — their parse-worthy rows read kind="text", never kind="def", and +# says so. --detail=1 lifts the guard: all 300 are definitions and the +# element is gone. The full legend carries the UNPARSED clause on exactly that answer and stays well-formed XML. +# u.c (HEAD's, 20 calls) gives the answer kind="ref" rows, so the default listing (defs) and listing=all are different +# pages: the next= replay arm below tells "the same question" from "the default question". +GG="$TMP/guard"; mkdir -p "$GG"; git -C "$GG" init -q -b main >/dev/null 2>&1; git -C "$GG" config commit.gpgsign false +printf 'int guardName( int a );\n' >"$GG/g.h" +i=0; : >"$GG/u.c"; while [ $i -lt 20 ]; do printf 'int use%d( void ) { return guardName( %d ); }\n' $i $i >>"$GG/u.c"; i=$((i+1)); done +git -C "$GG" add -A >/dev/null 2>&1; git -C "$GG" commit -qm base >/dev/null 2>&1 +git -C "$GG" checkout -qb many >/dev/null 2>&1 +i=0; while [ $i -lt 300 ]; do n=$( printf '%03d' $i ); printf 'int guardName( int a ) { return a + %d; }\n' $i >"$GG/f$n.c"; i=$((i+1)); done +git -C "$GG" add -A >/dev/null 2>&1; git -C "$GG" commit -qm many >/dev/null 2>&1; git -C "$GG" checkout -q main >/dev/null 2>&1 +GA="$( "$BIN" "$GG" --whereis=guardName --whereis-listing=all --limit=1000 --no-cache 2>/dev/null )"; rcGa=$? +GD="$( "$BIN" "$GG" --whereis=guardName --whereis-listing=all --limit=1000 --detail=1 --no-cache 2>/dev/null )"; rcGd=$? +GT="$( printf '%s' "$GA" | sed 's///' | tr '<' '\n' | grep '^hit ref="many" .* p="f' | grep -c 'kind="text"' )" +GF="$( printf '%s' "$GA" | sed 's///' | tr '<' '\n' | grep '^hit ref="many" .* p="f' | grep -c 'kind="def"' )" +GTL="$( printf '%s' "$GA" | sed 's///' | tr '<' '\n' | grep '^hit ref="many" ' | grep 'kind="text"' | sed -n 's/.* p="\([^"]*\)".*/\1/p' | sort | sed -n '1p;$p' | tr '\n' ' ' )" +if [ $rcGa -ne 0 ] || [ $rcGd -ne 0 ] || ! printf '%s' "$GA" | grep -q ' root (rc $rcGa / $rcGd)" +else + { [ "$GT" = "44" ] && [ "$GF" = "256" ] && [ "$GTL" = "f256.c f299.c " ] \ + && printf '%s' "$GA" | grep -q ''; } \ + && ok 'whereis (guard): 256 parsed (f000..f255, most rows first then path), 44 left: their rows read kind="text" (never def) and discloses them' \ + || { no "whereis (guard): want 256 def + 44 text rows (f256..f299) and the element; got def=$GF text=$GT [$GTL]"; printf '%s' "$GA" | grep -o ']*>'; } + { [ "$( printf '%s' "$GD" | sed 's///' | tr '<' '\n' | grep '^hit ref="many" .* p="f' | grep -c 'kind="def"' )" = "300" ] \ + && ! printf '%s' "$GD" | grep -q 'kind="text"' && ! printf '%s' "$GD" | grep -q '' \ + || no 'whereis (guard): --detail=1 did not parse every blob' + # X1 (rule 5, recoverable via next=): the next= REPLAYS the question. Its argv, run as written, answers the same + # listing with the guard lifted — byte for byte the explicit --whereis-listing=all --detail=1 page ($GD), not the + # default (defs) page — and draws no "IGNORED" verb-precedence note. The default page's next= names no listing. + GN="$( printf '%s' "$GA" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p' )" + GR="$( "$BIN" "$GG" $GN --limit=1000 --no-cache 2>"$TMP/guard.replay.err" )"; rcGr=$? + { [ -n "$GN" ] && [ $rcGr -eq 0 ] && printf '%s' "$GR" | grep -q ' of a listing=all answer ($GN) replays it: the same listing=all page with the guard lifted" \ + || { no "whereis (guard): next='$GN' (rc $rcGr) does not replay the listing=all question with the guard lifted"; grep -o ']*>' <<<"$GR"; cat "$TMP/guard.replay.err"; } + GDN="$( "$BIN" "$GG" --whereis=guardName --no-cache 2>/dev/null | grep -o ']*>' )" + [ "$GDN" = '' ] \ + && ok 'whereis (guard): the default page'"'"'s next= names no listing (the default stays the default)' \ + || no "whereis (guard): the default page's is '$GDN'" + if command -v python3 >/dev/null 2>&1; then + # The MCP `kind` ref filter has no CLI whereis spelling: next= drops it (as does) instead of naming + # --stray-content=, a different verb that would take precedence over --whereis. + GM="$( mcpq "$GG" guardName ',"kind":"man","listing":"all"' )" + GMN="$( printf '%s' "$GM" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p' )" + GMR="$( "$BIN" "$GG" $GMN --limit=1000 --no-cache 2>"$TMP/guard.mcpreplay.err" )"; rcGmr=$? + { printf '%s' "$GM" | grep -q ']* filter="man"' && [ "$GMN" = '--whereis=guardName --whereis-listing=all --detail=1' ] \ + && [ $rcGmr -eq 0 ] && printf '%s' "$GMR" | grep -q '/dev/null )" + GC="$( "$BIN" "$GG" --whereis=guardName --legend=compact --no-cache 2>/dev/null )" + { printf '%s' "$GL" | grep -q 'UNPARSED: the unparsed element' && printf '%s' "$GC" | grep -q 'unparsed blobs=N' && printf '%s' "$GC" | grep -q 'unparsed rows=N' \ + && printf '%s' "$GC" | grep -q 'ref_labels=parsed' && ! printf '%s' "$GD" | grep -q 'UNPARSED'; } \ + && ok 'whereis (guard): the full legend explains and the compact dictionary reads blobs=/rows=/ref_labels= on the answer that carries them (and not on the --detail=1 answer)' \ + || no 'whereis (guard): a legend lacks the / ref_labels= readings, or carries them where the element is absent' + if command -v xmllint >/dev/null 2>&1; then + printf '%s' "$GL" | xmllint --noout - 2>/dev/null \ + && ok 'whereis (guard): the full-legend page with the UNPARSED clause spliced in is well-formed XML (checklist 25)' \ + || no 'whereis (guard): the full-legend page with the UNPARSED clause is malformed XML' + else + printf ' SKIP whereis (guard): xmllint absent — the UNPARSED clause'"'"'s well-formedness is not checked\n' + fi +fi + +# ── 5e) every ref's tree from content-addressed tree objects (crossref.h listTreesOfRefs): ls-tree's own spelling ──── +# One branch holds files whose NAMES git must quote under core.quotepath=false (a double quote, a backslash, a tab) and +# ones it must not (a space, UTF-8, a leading dash). The p= of every row equals `git ls-tree -r` for that ref, path for +# path (XML-unescaped). RED on any walk that spells a path its own way. +OD="$TMP/oddnames"; mkdir -p "$OD/d"; git -C "$OD" init -q -b main >/dev/null 2>&1; git -C "$OD" config commit.gpgsign false +printf 'int oddName( int a );\n' >"$OD/d/plain.h"; git -C "$OD" add -A >/dev/null 2>&1; git -C "$OD" commit -qm base >/dev/null 2>&1 +git -C "$OD" checkout -qb odd >/dev/null 2>&1 +for nm in 'q"uote.h' 'back\slash.h' "$( printf 'ta\tb.h' )" 'sp ace.h' 'caf'"$( printf '\303\251' )"'.h' -- '-lead.h'; do + [ "$nm" = "--" ] && continue + printf 'int oddName( int a );\n' >"$OD/d/$nm" +done +git -C "$OD" add -A >/dev/null 2>&1; git -C "$OD" commit -qm odd >/dev/null 2>&1; git -C "$OD" checkout -q main >/dev/null 2>&1 +OO="$( "$BIN" "$OD" --whereis=oddName --whereis-listing=all --no-cache 2>/dev/null )"; rcOo=$? +OGOT="$( printf '%s' "$OO" | sed 's///' | tr '<' '\n' | grep '^hit ref="odd" ' | sed -n 's/.* p="\([^"]*\)" l=.*/\1/p' \ + | sed 's/"/"/g; s/'/'"'"'/g; s/<//g; s/&/\&/g' | sort )" +OWANT="$( git -c core.quotepath=false -C "$OD" ls-tree -r --name-only odd | sort )" +{ [ $rcOo -eq 0 ] && [ "$( printf '%s\n' "$OWANT" | grep -c . )" -ge 6 ] && [ "$OGOT" = "$OWANT" ]; } \ + && ok 'whereis (tree walk): every row'"'"'s p= is git ls-tree'"'"'s own spelling — quoted (", \, tab) and unquoted (space, UTF-8, leading dash) alike' \ + || { no "whereis (tree walk): p= differs from git ls-tree's spelling (rc $rcOo)"; printf 'got:\n%s\nwant:\n%s\n' "$OGOT" "$OWANT"; } + +# ── 5f) train 26c (#339 x the parsed branch labels): a branch's Rails schema column reads as HEAD's index reads it ──── +# HEAD's index makes a db/schema.rb column a definition (#339: captureRubyDefs, the schema recognized by content), and another +# ref's blob is parsed by the SAME extraction in its DefinitionsOnly posture (ingest_sidecap.h: the Ruby def walk runs before +# that posture's early return). So a column a branch adds reads kind="def" once its blob earns a parse (a parse-worthy hit line +# in it), as HEAD's own copy of a column does. RED if the Ruby def walk sat behind the DefinitionsOnly return (measured: the +# column row reads kind="ref"); the premise is HEAD's index labelling its own column a definition. +RS="$TMP/rbschema"; mkdir -p "$RS/db" "$RS/other"; git -C "$RS" init -q -b main >/dev/null 2>&1; git -C "$RS" config commit.gpgsign false +printf 'ActiveRecord::Schema[7.1].define(version: 2024_01_01_000000) do\n create_table "users", force: :cascade do |t|\n t.string "email"\n end\nend\n' >"$RS/db/schema.rb" +printf 'ActiveRecord::Schema[7.1].define(version: 2024_01_01_000000) do\n create_table "accounts", force: :cascade do |t|\n t.string "nickname"\n end\nend\n' >"$RS/other/schema.rb" +git -C "$RS" add -A >/dev/null 2>&1; git -C "$RS" commit -qm base >/dev/null 2>&1; git -C "$RS" checkout -qb feat >/dev/null 2>&1 +printf 'ActiveRecord::Schema[7.1].define(version: 2024_02_01_000000) do\n create_table "users", force: :cascade do |t|\n t.string "email"\n t.string "nickname"\n end\nend\ndef nickname( x )\n x\nend\n' >"$RS/db/schema.rb" +git -C "$RS" commit -qam feat >/dev/null 2>&1; git -C "$RS" checkout -q main >/dev/null 2>&1 +RSO="$( "$BIN" "$RS" --whereis=nickname --whereis-listing=all --no-cache 2>/dev/null )"; rcRs=$? +RSHEAD="$( printf '%s' "$RSO" | grep -oE ']*p="db/schema\.rb" l="4" kind="[a-z]+"' | head -1 )" +if [ $rcRs -ne 0 ] || [ "${RSHEAD##*kind=}" != '"def"' ]; then + no "whereis (#339 x parsed labels): premise — HEAD's index labels its own schema column a def (rc $rcRs, got: ${RSHEAD:-no row})" +elif [ "${RSCOL##*kind=}" = '"def"' ]; then + ok 'whereis (#339 x parsed labels): a column a branch adds to db/schema.rb reads kind="def", as HEAD'"'"'s index reads its own' +else + no "whereis (#339 x parsed labels): the branch's schema column is not a def (got: ${RSCOL:-no row})" +fi + +# ── 5g) train 26c (cli-perf-068 x the tree-object walk): the ls-tree fallback lists each DISTINCT tip once ───────────── +# A root below the top of the work tree cannot use the tree-object walk (listTreesOfRefs), so the trees come from +# `git ls-tree -r`, one per distinct tip (crossref.h listDistinctTips): HEAD + b2 + b3 sit on 2 tips (b3 = b2's tip; b1, at +# HEAD's tip, is not swept), so 2 listings, not one per ref, and the same 8 rows. RED on a per-ref fallback (3 listings). +LT="$TMP/lstips"; mkdir -p "$LT/sub"; git -C "$LT" init -q -b main >/dev/null 2>&1; git -C "$LT" config commit.gpgsign false +printf 'int tipProbe( int x ) { return x; }\nint tipCaller( void ) { return tipProbe( 1 ); }\n' >"$LT/sub/a.c"; printf 'top\n' >"$LT/top.txt" +git -C "$LT" add -A >/dev/null 2>&1; git -C "$LT" commit -qm one >/dev/null 2>&1; git -C "$LT" branch b1 >/dev/null 2>&1 +git -C "$LT" checkout -qb b2 >/dev/null 2>&1; printf 'int tipProbe( int x, int y ) { return x + y; }\n' >"$LT/sub/b.c" +git -C "$LT" add -A >/dev/null 2>&1; git -C "$LT" commit -qm two >/dev/null 2>&1; git -C "$LT" branch b3 >/dev/null 2>&1; git -C "$LT" checkout -q main >/dev/null 2>&1 +: >"$TMP/lstips.trace" +LTO="$( GIT_TRACE="$TMP/lstips.trace" "$BIN" "$LT/sub" --whereis=tipProbe --whereis-listing=all --no-cache 2>/dev/null )"; rcLt=$? +LTN="$( grep -c 'built-in: git ls-tree' "$TMP/lstips.trace" )" +LTH="$( printf '%s' "$LTO" | grep -oE ']* hits="[0-9]+"' | grep -oE 'hits="[0-9]+"' )" +LTR="$( printf '%s' "$LTO" | grep -oE ']* refs_scanned="[0-9]+"' | grep -oE 'refs_scanned="[0-9]+"' )" +{ [ $rcLt -eq 0 ] && [ "$LTR" = 'refs_scanned="2"' ] && [ "$LTH" = 'hits="8"' ] && [ "$LTN" = 2 ]; } \ + && ok "whereis (ls-tree fallback): a root below the work-tree top lists each distinct tip once (2 listings for HEAD + 2 refs on 2 tips), all 8 rows" \ + || no "whereis (ls-tree fallback): want rc 0, refs_scanned=\"2\", hits=\"8\" and 2 ls-tree listings — got rc $rcLt, $LTR, $LTH, $LTN listing(s)" + +# ── 5h) the t= PREVIEW (lane whereis-lean; crossref.h kWhereisTextPreviewCapBytes = 200, writeWhereisRows / writeWhereisTextCut) ── +# A row's t= is a preview of a LONG line: a line over 300 bytes (kWhereisTextWholeMaxBytes, so a cut always drops 100+ +# bytes) prints its first bytes, cut on a UTF-8 boundary, plus an ellipsis — 200 bytes at most, the ellipsis included — +# and the row carries t_bytes= (the whole line's bytes); counts the page's cut rows and next= replays +# THIS page with --whereis-full-text (same listing, window, rows and labels; every line whole). A page prints previews only +# when that makes it shorter than the same page whole (writeWhereisPage; the 5h-C2 arms below). Arms: a ~7 KB def line (HEAD, the defs +# page), lines of exactly 200, 201 and 300 bytes (whole), one of 301 (cut to 200), a 3-byte character straddling the kept +# prefix (cut before it), a short line (unchanged, no t_bytes=), a branch row (tip= date= beside t_bytes=), the refs and +# all listings, the MCP twin, paging (rows= counts this page), and an answer of short lines (no t_cut, no clause). +# RED on the pre-lane binary (4c38d363): every t= is the whole line and no row carries t_bytes=. +PV="$TMP/preview"; mkdir -p "$PV"; git -C "$PV" init -q -b main >/dev/null 2>&1; git -C "$PV" config commit.gpgsign false +pvRep(){ i=0; s=''; while [ $i -lt "$2" ]; do s="$s$1"; i=$((i+1)); done; printf '%s' "$s"; } +PV_LONG="int previewName( int a ) { return a$( pvRep ' + 1' 1750 ); }" # 7,000+ bytes, the definition +PV_200="X = previewName + $( pvRep a 182 )" # 18 + 182 = exactly 200 bytes +PV_201="Y = previewName + $( pvRep b 183 )" # 201 bytes +PV_300="V = previewName + $( pvRep e 282 )" # 300 bytes +PV_301="W = previewName + $( pvRep f 283 )" # 301 bytes +PV_EUR="Z = previewName + $( pvRep c 178 )€$( pvRep d 140 )" # 339 bytes, € at bytes 196-198 straddles 197 +printf '%s\n' "$PV_LONG" >"$PV/p.c" +printf '%s\n%s\n%s\n%s\n%s\npreviewName( 1 )\n' "$PV_200" "$PV_201" "$PV_300" "$PV_301" "$PV_EUR" >"$PV/q.py" +printf 'int shortName( int a ) { return a; }\nint shortUse( void ) { return shortName( 1 ); }\n' >"$PV/s.c" +git -C "$PV" add -A >/dev/null 2>&1; git -C "$PV" commit -qm base >/dev/null 2>&1 +git -C "$PV" checkout -qb side >/dev/null 2>&1 +PV_BR="int previewName( long a ) { return a$( pvRep ' - 2' 400 ); }" +printf '%s\n' "$PV_BR" >"$PV/b.c"; git -C "$PV" add -A >/dev/null 2>&1; git -C "$PV" commit -qm side >/dev/null 2>&1 +git -C "$PV" checkout -q main >/dev/null 2>&1 +pvRow(){ printf '%s' "$1" | sed 's///' | tr '<' '\n' | grep "^hit ref=\"$2\" .*p=\"$3\" l=\"$4\" " | head -1; } +pvT(){ printf '%s' "$1" | sed -n 's/.* t="\(.*\)"\/>*$/\1/p' | sed 's/"\/$//'; } +pvB(){ LC_ALL=C; printf '%s' "$1" | wc -c | tr -d ' '; } +# pvSame PAGE REPLAY: "same" when the replay lists the page's rows (every attribute but t= and t_bytes=, in order, so the +# same refs, paths, lines and kind= labels) and each row's t= is whole: the length of a cut row's t_bytes=, else the page's +# own t= length; and the replay is no longer than the page plus the bytes its cuts dropped (rule 5: about the page's bytes, +# never a different, larger answer). Else the first difference. The fixtures' long lines hold no XML-escaped character. +pvSig(){ printf '%s' "$1" | sed 's///' | tr '<' '\n' | grep '^hit ' | sed -E 's/ t_bytes="[0-9]+"//; s/ t=".*//'; } +pvLen(){ printf '%s' "$1" | sed 's///' | tr '<' '\n' | grep '^hit ' \ + | LC_ALL=C awk '{ tb = "-"; if( match( $0, / t_bytes="[0-9]+"/ ) ) { tb = substr( $0, RSTART + 10, RLENGTH - 11 ) } + t = $0; sub( /^.* t="/, "", t ); sub( /"\/>?$/, "", t ); print tb, length( t ) }'; } +pvSame(){ + local a b + a="$( pvSig "$1" )"; b="$( pvSig "$2" )" + [ -n "$a" ] || { printf 'no rows on the page'; return; } + [ "$a" = "$b" ] || { printf 'rows differ: page %s, replay %s' "$( printf '%s\n' "$a" | grep -c . )" "$( printf '%s\n' "$b" | grep -c . )"; return; } + paste -d' ' <( pvLen "$1" ) <( pvLen "$2" ) | LC_ALL=C awk -v pb="$( pvB "$1" )" -v rb="$( pvB "$2" )" ' + { want = ( $1 == "-" ) ? $2 : $1; if( $3 != "-" || $4 != want ) { bad = bad " row " NR ": t_bytes " $1 " page " $2 " replay " $4 } + if( $1 != "-" ) { dropped += $1 - $2 } } + END { if( bad != "" ) { print "lines not whole:" bad } else if( rb > pb + dropped ) { print "replay " rb " B > page " pb " + dropped " dropped } + else { print "same" } }' +} +PA="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=all --no-cache 2>/dev/null )"; rcPa=$? +PD="$( "$BIN" "$PV" --whereis=previewName --no-cache 2>/dev/null )"; rcPd=$? +PR="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=refs --no-cache 2>/dev/null )"; rcPr=$? +if [ $rcPa -ne 0 ] || [ $rcPd -ne 0 ] || [ $rcPr -ne 0 ] || ! printf '%s' "$PA" | grep -q ' root (rc $rcPa / $rcPd / $rcPr)" +else + ELL='…' + r="$( pvRow "$PD" HEAD p.c 1 )"; t="$( pvT "$r" )" + { printf '%s' "$r" | grep -q "kind=\"def\" t_bytes=\"$( pvB "$PV_LONG" )\" t=" && [ "$( pvB "$t" )" -le 200 ] && [ "${t%$ELL}" != "$t" ] \ + && [ "${PV_LONG#"${t%$ELL}"}" != "$PV_LONG" ] && [ "$( pvB "$t" )" = 200 ]; } \ + && ok "whereis (preview): the $( pvB "$PV_LONG" )-byte definition line prints a 200-byte t= (its first 197 bytes + …) and t_bytes=\"$( pvB "$PV_LONG" )\" on the defs page" \ + || no "whereis (preview): the long def row is '${r:0:300}' ($( pvB "$t" ) B t=)" + for pv in "1|$PV_200|200" "2|$PV_201|201" "3|$PV_300|300"; do + r="$( pvRow "$PA" HEAD q.py "${pv%%|*}" )"; w="${pv#*|}"; w="${w%|*}" + { [ "$( pvT "$r" )" = "$w" ] && [ "$( pvB "$w" )" = "${pv##*|}" ] && ! printf '%s' "$r" | grep -q 't_bytes='; } \ + && ok "whereis (preview): a line of exactly ${pv##*|} bytes prints whole, no ellipsis, no t_bytes=" \ + || no "whereis (preview): the ${pv##*|}-byte line's row is '${r:0:300}'" + done + r="$( pvRow "$PA" HEAD q.py 4 )"; t="$( pvT "$r" )" + { printf '%s' "$r" | grep -q 't_bytes="301"' && [ "$( pvB "$t" )" = 200 ] && [ "$t" = "${PV_301:0:197}$ELL" ]; } \ + && ok 'whereis (preview): a 301-byte line prints its first 197 bytes + … (200 bytes, the ellipsis included) and t_bytes="301"' \ + || no "whereis (preview): the 301-byte line's row is '${r:0:300}' ($( pvB "$t" ) B)" + r="$( pvRow "$PA" HEAD q.py 5 )"; t="$( pvT "$r" )"; pre="$( LC_ALL=C; printf '%s' "$PV_EUR" | head -c 196 )" + { printf '%s' "$r" | grep -q "t_bytes=\"$( pvB "$PV_EUR" )\"" && [ "$t" = "$pre$ELL" ] && [ "$( pvB "$t" )" = 199 ] \ + && printf '%s' "$t" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1; } \ + && ok 'whereis (preview): a 3-byte € straddling the kept prefix is not split: the cut backs off to byte 196, + … (199 bytes, valid UTF-8)' \ + || no "whereis (preview): the straddle row is '${r:0:300}' ($( pvB "$t" ) B)" + r="$( pvRow "$PA" HEAD q.py 6 )" + { [ "$( pvT "$r" )" = 'previewName( 1 )' ] && ! printf '%s' "$r" | grep -q 't_bytes='; } \ + && ok 'whereis (preview): a short line prints unchanged, with no t_bytes=' \ + || no "whereis (preview): the short row is '$r'" + r="$( pvRow "$PA" side b.c 1 )"; t="$( pvT "$r" )" + { printf '%s' "$r" | grep -q " tip=\"[0-9a-f]\{9\}\" date=\"2026-01-01\" .*kind=\"def\" t_bytes=\"$( pvB "$PV_BR" )\" t=" && [ "$( pvB "$t" )" -le 200 ] \ + && [ "${PV_BR#"${t%$ELL}"}" != "$PV_BR" ]; } \ + && ok 'whereis (preview): a branch row (tip= date=) is cut the same way and carries t_bytes=' \ + || no "whereis (preview): the branch row is '${r:0:300}'" + nA="$( printf '%s' "$PA" | sed 's///' | grep -o ' t_bytes="' | wc -l | tr -d ' ' )" + # HEAD: p.c, the 301 and the € lines; side holds the same two blobs (3 more) and b.c: 7 previews on the all page. + { [ "$nA" = 7 ] && printf '%s' "$PA" | grep -q ''; } \ + && ok 'whereis (preview): counts exactly the page'"'"'s 7 t_bytes= rows' \ + || { no "whereis (preview): want 7 t_bytes= rows and their ; got $nA"; printf '%s' "$PA" | grep -o ']*>'; } + # (C2, review whereis-lean) the refs page's only long lines are the 301- and 339-byte ones (twice each): cutting them drops + # 2 x (101 + 140) bytes and its disclosure (t_bytes=, , the clause or the readings) costs more, so the page prints + # whole, byte-identical to its --whereis-full-text twin, under both legends. RED when every page with a long line cuts. + PRF="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=refs --whereis-full-text --no-cache 2>/dev/null )" + PRL="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=refs --legend=full --no-cache 2>/dev/null )" + PRLF="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=refs --legend=full --whereis-full-text --no-cache 2>/dev/null )" + r="$( pvRow "$PR" HEAD q.py 4 )" + { [ "$( pvT "$r" )" = "$PV_301" ] && ! printf '%s' "$PR" | grep -q 't_bytes=\|]*>'; } + # Rule 5 (recoverable via next=): the next= argv, run as written, is THIS page with every line whole — the same rows in + # the same order with the same labels (pvSame), each cut row's line whole (its length is the page's t_bytes=), no t_bytes=, + # no . + PN="$( printf '%s' "$PA" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p' )" + PX="$( "$BIN" "$PV" $PN --no-cache 2>"$TMP/preview.replay.err" )"; rcPx=$? + { [ -n "$PN" ] && [ $rcPx -eq 0 ] && printf '%s' "$PX" | grep -q ' ($PN) is the same page, every line whole (the 7 KB def, the branch def), no t_bytes= and no " \ + || no "whereis (preview): next='$PN' (rc $rcPx) is not the same page with whole lines: $( pvSame "$PA" "$PX" )" + P1="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=all --limit=1 --offset=0 --no-cache 2>/dev/null )" + # A paged page's next= carries its own window: --limit (and --offset when it is past 0), so the replay is that page. + PK="$( printf '%s' "$PA" | sed 's///' | tr '<' '\n' | grep '^hit ' | grep -n ' p="b.c" ' | head -1 | cut -d: -f1 )"; PK=$(( ${PK:-1} - 1 )) + P3="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=all --limit=1 --offset=$PK --no-cache 2>/dev/null )" + P3N="$( printf '%s' "$P3" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p' )" + P3X="$( "$BIN" "$PV" $P3N --no-cache 2>/dev/null )" + { [ "$PK" -gt 0 ] && [ "$P3N" = "--whereis=previewName --whereis-listing=all --limit=1 --offset=$PK --whereis-full-text" ] \ + && [ "$( pvSame "$P3" "$P3X" )" = same ] && [ "$( pvT "$( pvRow "$P3X" side b.c 1 )" )" = "$PV_BR" ] \ + && [ "$( printf '%s' "$P1" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p' )" = '--whereis=previewName --whereis-listing=all --limit=1 --whereis-full-text' ]; } \ + && ok "whereis (preview): a paged page's carries its window (--limit=1 --offset=$PK; offset 0 is the default) and replays that one row whole" \ + || no "whereis (preview): the paged next= is '$P3N' (offset $PK): $( pvSame "$P3" "$P3X" )" + P5="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=all --limit=1 --offset=2 --no-cache 2>/dev/null )" + { printf '%s' "$P1" | grep -q ', no t_bytes= and no reading' \ + || { no 'whereis (preview): the paged or its absence is wrong'; printf '%s\n%s' "$P1" "$P5" | grep -o ']*>\|]*l="[0-9]*"'; } + PS="$( "$BIN" "$PV" --whereis=shortName --whereis-listing=all --legend=full --no-cache 2>/dev/null )"; rcPs=$? + { [ $rcPs -eq 0 ] && printf '%s' "$PS" | grep -q ' and no PREVIEW clause (byte-identical to before the cap)' \ + || no "whereis (preview): the short-lines answer (rc $rcPs) carries a preview disclosure" + PL="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=all --legend=full --no-cache 2>/dev/null )" + PC="$( "$BIN" "$PV" --whereis=previewName --whereis-listing=all --legend=compact --no-cache 2>/dev/null )" + { printf '%s' "$PL" | grep -q 'PREVIEW: t= is the row'"'"'s trimmed source line when it fits 300 bytes; a longer line prints a preview of 200 bytes' \ + && printf '%s' "$PL" | grep -q 'its next= replays THIS page (its listing, window, rows and labels) with the whereis full text flag (MCP full_text)' \ + && printf '%s' "$PC" | grep -q 'hit t_bytes=N: t= previews an N-byte line (over 300 B): 200 B at most' \ + && printf '%s' "$PC" | grep -q 't_cut rows=N: rows of this page whose t= is a preview (each carries t_bytes=); next= replays this page' \ + && ! printf '%s' "$PX" | grep -q 'PREVIEW: \|t_bytes=N'; } \ + && ok 'whereis (preview): the full legend carries the PREVIEW clause and the compact one reads t_bytes= and t_cut rows= (300 B / 200 B, the binary'"'"'s constants), only where a row was cut' \ + || no 'whereis (preview): a legend lacks the PREVIEW clause / readings, or carries them on the uncut --detail=1 answer' + if command -v xmllint >/dev/null 2>&1; then + printf '%s' "$PL" | xmllint --noout - 2>/dev/null && printf '%s' "$PC" | xmllint --noout - 2>/dev/null \ + && ok 'whereis (preview): the full-legend page with the PREVIEW clause spliced in, and the compact page, are well-formed XML (checklist 25)' \ + || no 'whereis (preview): a page carrying the PREVIEW clause is malformed XML' + else + printf ' SKIP whereis (preview): xmllint absent — the PREVIEW clause'"'"'s well-formedness is not checked\n' + fi + if command -v python3 >/dev/null 2>&1; then + PM="$( mcpq "$PV" previewName ',"listing":"all"' )" + { [ "$PM" = "$PA" ]; } \ + && ok 'whereis (preview): the MCP twin serves the CLI page byte for byte (t_bytes= rows and )' \ + || { no 'whereis (preview): the MCP whereis page differs from the CLI one'; diff <( printf '%s' "$PA" | tr '<' '\n' ) <( printf '%s' "$PM" | tr '<' '\n' ) | head -5; } + # full_text, the MCP twin of --whereis-full-text: the same whole-line page byte for byte; false is the default page. + PMF="$( mcpq "$PV" previewName ',"listing":"all","full_text":true' )"; PMN="$( mcpq "$PV" previewName ',"listing":"all","full_text":false' )" + { [ -n "$PX" ] && [ "$PMF" = "$PX" ] && [ "$PMN" = "$PA" ]; } \ + && ok 'whereis (preview): MCP full_text:true serves the CLI --whereis-full-text page byte for byte; full_text:false the default page' \ + || no 'whereis (preview): MCP full_text differs from the CLI --whereis-full-text page' + # Checklist 26: full_text is read with the other typed arguments, before the shared shape gate, so a wrong-shaped value + # refuses -32602 and the refusal echoes what was SENT (a read inside the whereis arm would decode "yes" to false and answer). + for arm in '"yes"|yes' '1|1' '["true"]|["true"]'; do + val="${arm%%|*}"; echo_="${arm#*|}" + m="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whereis","arguments":{"path":"%s","symbol":"previewName","full_text":%s}}}\n' "$PV" "$val" \ + | "$BIN" --mcp 2>/dev/null | python3 -c 'import sys,json +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line) + if "error" in d: print("ERROR",d["error"].get("code"),d["error"].get("message",""),end="") + elif d.get("result",{}).get("content"): print("ANSWER",end="")' )" + case "$m" in + "ERROR -32602 invalid value for field: full_text"*"got '$echo_'"*) ok "whereis (preview): MCP full_text:$val refuses -32602 and echoes '$echo_'" ;; + *) no "whereis (preview): MCP full_text:$val — want a -32602 refusal echoing '$echo_', got: ${m:-nothing}" ;; + esac + done + else + printf ' SKIP whereis (preview): MCP twin arms (no python3)\n' + fi + "$BIN" "$PV" --whereis-full-text --no-cache >/dev/null 2>"$TMP/preview.lone.err"; rcLone=$? + { [ $rcLone -ne 0 ] && grep -q -- '--whereis-full-text modifies --whereis=SYM' "$TMP/preview.lone.err"; } \ + && ok 'whereis (preview): --whereis-full-text alone refuses (rc 1) and names --whereis=SYM' \ + || no "whereis (preview): --whereis-full-text alone gave rc $rcLone: $( head -c 200 "$TMP/preview.lone.err" )" +fi + +# ── 5h-C2) a page prints previews only when that makes it SHORTER (review whereis-lean C2; crossref.h writeWhereisPage) ── +# One short definition and one reference line of L bytes per symbol. The cut page pays for its disclosure (t_bytes=, , +# and the full PREVIEW clause or the compact readings), so a line just past 300 bytes saves less than that: the page must +# print whole, byte-identical to its --whereis-full-text twin, and a 2 KB line still cuts. For every L and listing, under +# both legends: the page is its whole twin or strictly shorter than it, it cuts under one legend exactly when it cuts under +# the other, and the default listing is the whole twin's (a cut never flips it: L=345 is review C2b, where the cut page +# used to tip the default from all to defs, fewer rows for more bytes). RED on 9f51cca7, which cut every long line. +CW="$TMP/cutwidth"; mkdir -p "$CW"; git -C "$CW" init -q -b main >/dev/null 2>&1; git -C "$CW" config commit.gpgsign false +for L in 302 345 395 501 601 701 2000; do + printf 'int zqW%s( int a ) { return a; }\n' "$L" >"$CW/d$L.c" + pre="int u$L( void ) { return zqW$L( 1 ); } // " + printf '%s%s\n' "$pre" "$( pvRep r $(( L - ${#pre} )) )" >"$CW/u$L.c" +done +git -C "$CW" add -A >/dev/null 2>&1; git -C "$CW" commit -qm widths >/dev/null 2>&1 +cwBad=''; cwRuns=0 +for L in 302 345 395 501 601 701 2000; do + for lst in '' --whereis-listing=all; do + cut='' + for leg in full compact; do + pg="$( "$BIN" "$CW" --whereis=zqW$L $lst --legend=$leg --no-cache 2>/dev/null )"; rcG=$? + wh="$( "$BIN" "$CW" --whereis=zqW$L $lst --legend=$leg --whereis-full-text --no-cache 2>/dev/null )"; rcW=$? + cwRuns=$(( cwRuns + 1 )) + { [ $rcG -eq 0 ] && [ $rcW -eq 0 ] && printf '%s' "$pg" | grep -q ']*>' | grep -o ' listing="[a-z]*"' )" = "$( printf '%s' "$wh" | grep -o ']*>' | grep -o ' listing="[a-z]*"' )" ] \ + || cwBad="$cwBad [L=$L $lst $leg: the listing differs from the whole twin's]" + if [ $c -eq 0 ]; then + [ "$pg" = "$wh" ] || cwBad="$cwBad [L=$L $lst $leg: uncut but not byte-identical to the whole twin]" + else + [ "$( pvB "$pg" )" -lt "$( pvB "$wh" )" ] || cwBad="$cwBad [L=$L $lst $leg: cut page $( pvB "$pg" ) B >= whole $( pvB "$wh" ) B]" + fi + case "$L|$lst|$c" in 302\|*\|1|345\|*\|1|395\|*\|1) cwBad="$cwBad [L=$L $lst $leg: a near-threshold line was cut]" ;; 2000\|--whereis-listing=all\|0) cwBad="$cwBad [L=2000 all $leg: a 2 KB line was not cut]" ;; esac + done + case "$cut" in 00|11) ;; *) cwBad="$cwBad [L=$L $lst: cut under one legend only ($cut)]" ;; esac + done +done +C2B="$( "$BIN" "$CW" --whereis=zqW345 --no-cache 2>/dev/null | grep -o ']*>' )" +{ [ -z "$cwBad" ] && [ "$cwRuns" = 28 ] && printf '%s' "$C2B" | grep -q ' shown="2"' && ! printf '%s' "$C2B" | grep -q ' listing='; } \ + && ok 'whereis (preview, C2): 7 widths x 2 listings x 2 legends: each page is its whole twin or strictly shorter, cuts under both legends or neither, keeps the twin'"'"'s listing; 302/345/395 B print whole, a 2 KB line cuts; the L=345 default stays the all page (2 rows)' \ + || no "whereis (preview, C2): $cwRuns runs;$cwBad; L=345 default root: $C2B" +CWR="$( "$BIN" "$CW" --whereis=zqW2000 --whereis-listing=refs --no-cache 2>/dev/null )" +CWN="$( printf '%s' "$CWR" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p' )" +CWX="$( "$BIN" "$CW" $CWN --no-cache 2>/dev/null )" +{ [ "$CWN" = '--whereis=zqW2000 --whereis-listing=refs --whereis-full-text' ] && [ "$( pvSame "$CWR" "$CWX" )" = same ]; } \ + && ok 'whereis (preview, C2): the refs listing cuts a 2 KB line and its names listing=refs and replays the row whole' \ + || no "whereis (preview, C2): the refs page's next= is '$CWN': $( pvSame "$CWR" "$CWX" )" + +# ── 5h-X2) the is THE SAME PAGE, every line whole (review whereis-lean C1; crossref.h whereisFullTextNext) ── +# The guard binds (300 distinct blobs on one branch, each a 400-byte call line at column 0, parse-worthy): 256 are parsed +# (kind="ref", counted on the defs page), 44 are left and read kind="text" — listed on the defs page as cut previews. The +# replay must keep the guard (the same 44 kind="text" rows, now whole: --detail=1 lifted it, relabelled them kind="ref" +# and dropped all 44 from the defs page), the row cap and the window (a listing=all page of 60 rows replays 60, not the +# 302-row hit list; a --limit/--offset page replays that page). RED on 9f51cca7 (next= was --detail=1). +XG="$TMP/xguard"; mkdir -p "$XG"; git -C "$XG" init -q -b main >/dev/null 2>&1; git -C "$XG" config commit.gpgsign false +printf 'def zqText( a ):\n return a\n' >"$XG/a.py"; git -C "$XG" add -A >/dev/null 2>&1; git -C "$XG" commit -qm base >/dev/null 2>&1 +git -C "$XG" checkout -qb many >/dev/null 2>&1; XPAD="$( pvRep x 400 )" +i=0; while [ $i -lt 300 ]; do printf 'zqText( %s, %d )\n' "$XPAD" $i >"$XG/t$( printf '%03d' $i ).py"; i=$((i+1)); done +git -C "$XG" add -A >/dev/null 2>&1; git -C "$XG" commit -qm many >/dev/null 2>&1; git -C "$XG" checkout -q main >/dev/null 2>&1 +xNext(){ printf '%s' "$1" | sed -n 's/.*]* next="\([^"]*\)".*/\1/p'; } +xKind(){ printf '%s' "$1" | sed 's///' | grep -o 'kind="text"' | wc -l | tr -d ' '; } +XD="$( "$BIN" "$XG" --whereis=zqText --no-cache 2>/dev/null )"; XDN="$( xNext "$XD" )"; XDX="$( "$BIN" "$XG" $XDN --no-cache 2>/dev/null )" +{ [ "$XDN" = '--whereis=zqText --whereis-listing=defs --whereis-full-text' ] && printf '%s' "$XD" | grep -q ' $( xKind "$XDX" ): $( pvSame "$XD" "$XDX" )" +XA="$( "$BIN" "$XG" --whereis=zqText --whereis-listing=all --no-cache 2>/dev/null )"; XAN="$( xNext "$XA" )"; XAX="$( "$BIN" "$XG" $XAN --no-cache 2>/dev/null )" +{ [ "$XAN" = '--whereis=zqText --whereis-listing=all --whereis-full-text' ] && printf '%s' "$XA" | grep -q ']* shown="60" capped="1"' \ + && [ "$( pvSig "$XAX" | grep -c . )" = 60 ] && [ "$( pvSame "$XA" "$XAX" )" = same ]; } \ + && ok "whereis (full text): a capped listing=all page (60 of 302 rows) replays the same 60 rows whole, $( pvB "$XAX" ) B for a $( pvB "$XA" ) B page (not the whole hit list)" \ + || no "whereis (full text): the listing=all page's next='$XAN' replays $( pvSig "$XAX" | grep -c . ) rows: $( pvSame "$XA" "$XAX" )" +XP="$( "$BIN" "$XG" --whereis=zqText --whereis-listing=all --limit=5 --offset=7 --no-cache 2>/dev/null )"; XPN="$( xNext "$XP" )"; XPX="$( "$BIN" "$XG" $XPN --no-cache 2>/dev/null )" +{ [ "$XPN" = '--whereis=zqText --whereis-listing=all --limit=5 --offset=7 --whereis-full-text' ] && [ "$( pvSig "$XPX" | grep -c . )" = 5 ] \ + && [ "$( pvSame "$XP" "$XPX" )" = same ]; } \ + && ok 'whereis (full text): a --limit=5 --offset=7 page replays those 5 rows whole (its next= carries the window)' \ + || no "whereis (full text): the paged next='$XPN': $( pvSame "$XP" "$XPX" )" + +# ── 5h-D1) a page the MCP kind ref filter bounded DISCLOSES that its drops the filter (review whereis-lean D1) ── +# The CLI has no whereis ref filter, so the next= of a kind-filtered page replays the unfiltered listing (other rows). The +# page says so: , the full PREVIEW clause names the recovery (MCP whereis with full_text:true), and +# that recovery IS the page's rows whole. An unfiltered page carries neither. RED on a4884e41 (no uncarried=, no clause). +if command -v python3 >/dev/null 2>&1; then + git -C "$XG" checkout -qb zz >/dev/null 2>&1; XLONG="$( pvRep y 1000 )" + i=0; while [ $i -lt 10 ]; do printf 'zqText( %s, %d )\n' "$XLONG" $i >"$XG/z$i.py"; i=$((i+1)); done + git -C "$XG" add -A >/dev/null 2>&1; git -C "$XG" commit -qm zz >/dev/null 2>&1; git -C "$XG" checkout -q main >/dev/null 2>&1 + XF="$( mcpq "$XG" zqText ',"kind":"zz","listing":"all","legend":"full"' )"; XFN="$( xNext "$XF" )" + XFR="$( "$BIN" "$XG" $XFN --no-cache 2>/dev/null )"; XFT="$( mcpq "$XG" zqText ',"kind":"zz","listing":"all","legend":"full","full_text":true' )" + XU="$( mcpq "$XG" zqText ',"listing":"all","legend":"full"' )" + { printf '%s' "$XF" | grep -q ']* filter="zz"' && printf '%s' "$XF" | grep -q ']* uncarried="zz"/>' \ + && printf '%s' "$XF" | grep -q 'the CLI replay has no kind filter, so that next= lists the unfiltered refs and other rows; call MCP whereis with full_text:true to recover this page' \ + && [ "$( pvSame "$XF" "$XFR" )" != same ] && [ "$( pvSame "$XF" "$XFT" )" = same ] \ + && printf '%s' "$XU" | grep -q ' and the PREVIEW recovery (MCP full_text:true replays its rows whole; the CLI next= lists other rows); an unfiltered page carries neither' \ + || no "whereis (full text): kind-filtered page next='$XFN' uncarried=$( printf '%s' "$XF" | grep -c 'uncarried=' ), CLI replay: $( pvSame "$XF" "$XFR" ), full_text: $( pvSame "$XF" "$XFT" )" + # Compact dialect: the uncarried= reading rides the page that carries the attribute, and only it. + XC="$( mcpq "$XG" zqText ',"kind":"zz","listing":"all","legend":"compact"' )"; XCU="$( mcpq "$XG" zqText ',"listing":"all","legend":"compact"' )" + { printf '%s' "$XC" | grep -q 't_cut uncarried=K: this page was bounded by the ref filter K (MCP kind); the CLI replay has no kind filter, so next= replays other rows; call MCP whereis with full_text:true to recover this page' \ + && printf '%s' "$XCU" | grep -q '/dev/null 2>&1; then if printf '%s' "$SW" | xmllint --noout - 2>/dev/null; then ok "capped stray-content still G4 clean"; else no "capped stray-content XML malformed"; fi if printf '%s' "$W1" | xmllint --noout - 2>/dev/null; then ok "paged whereis still G4 clean"; else no "paged whereis XML malformed"; fi diff --git a/test/crossrefdegradecheck.sh b/test/crossrefdegradecheck.sh index f22196105..30cd16845 100755 --- a/test/crossrefdegradecheck.sh +++ b/test/crossrefdegradecheck.sh @@ -22,6 +22,8 @@ # exactly one; and at exactly cap+1 items `size > shown` went false, so the element vanished ENTIRELY # and one row disappeared unmarked. Both the general case and that boundary are pinned here — nothing # in test/ pinned any more= count before, which is why the bug survived. +# 6) (cli-perf-068) A blob stream that never started (an unusable cache dir for its sha list) is refused by +# --whereis on both hosts, not printed as on-head="0" hits="0" with a near-miss retrying the same name. # 5) Two adjacent ways a real hit is reported as no hit at all: a symbol defined on an UNTERMINATED final # line (the scan only evaluated on '\n', so hits="0" — which the help text says means "this repo never # had the name"), and a branch whose name legally contains '|' (the for-each-ref parse split on it and @@ -37,6 +39,7 @@ 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" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT @@ -60,6 +63,64 @@ count_of(){ tr '<' '\n' | grep -v '^!--' | grep -c "^$1 "; } echo "crossrefdegradecheck: BIN=$BIN" +# ══ K75: --stray-content, --abi, --plan and --eval-stray REFUSE when the blob batch cannot run ═══════════════ +# +# Every cross-branch verdict is built from ONE `git cat-file --batch` whose sha list is written under the cache directory. With +# TMPDIR naming a directory that does not exist, that list cannot be written, the batch never starts, every blob reads as absent, +# and the answer was merged="1" refs="1" (a branch holding unmerged work), --abi exit 0 with no drift: a measured-looking zero made +# of nothing (the whereis hits="0" bug's siblings). The refusal is the whereis mechanism: exit 1, the cause named on stderr, no +# document on stdout. Arms: a control (the same repo answers when the directory exists, which proves the refusal is about the +# missing directory and not about the fixture), then the refusal on each verb and on the MCP twin. +K75="$TMP/k75repo"; mkdir -p "$K75" +k(){ git -C "$K75" "$@" >/dev/null 2>&1; } +k init -q -b main +k config commit.gpgsign false +printf 'int base( void )\n{\n return 1;\n}\n' > "$K75/a.c" +printf '#pragma once\nstruct Uniforms\n{\n float x;\n float y;\n};\nstatic_assert( sizeof( Uniforms ) == 8, "uniform GPU contract" );\n' > "$K75/shader.h" +k add -A; k commit -qm base +k checkout -qb side +printf 'int base( void )\n{\n return 1;\n}\n\nint sideOnlyThing( void )\n{\n return 42;\n}\n' > "$K75/a.c" +printf '#pragma once\nstruct Uniforms\n{\n float x;\n float y;\n float z;\n};\nstatic_assert( sizeof( Uniforms ) == 8, "uniform GPU contract" );\n' > "$K75/shader.h" +k commit -qam "side work" +k checkout -q main +printf 'int base( void )\n{\n return 1;\n}\n\nint mainWork( void )\n{\n return 7;\n}\n' > "$K75/a.c" +k commit -qam "live-line work" +K75GOOD="$TMP/k75tmp"; mkdir -p "$K75GOOD" +K75BAD="$TMP/k75-no-such-dir" # never created: TMPDIR names a directory that does not exist +printf 'side\tunmerged\n' > "$TMP/k75labels.tsv" +k75(){ local out="$1" tmpd="$2"; shift 2; env TMPDIR="$tmpd" "$BIN" "$K75" "$@" --no-cache --legend=full >"$out" 2>"$out.err"; } + +k75 "$TMP/k75_s_good.out" "$K75GOOD" --stray-content; rcS=$? +[ "$rcS" = 0 ] && grep -q 'v="unmerged"' "$TMP/k75_s_good.out" \ + && ok "K75 control: --stray-content answers the branch with work as unmerged when the cache dir works" \ + || no "K75 control: --stray-content (rc=$rcS) did not report v=\"unmerged\" - the refusal arms below would prove nothing" +k75 "$TMP/k75_a_good.out" "$K75GOOD" --stray-content --abi; rcA=$? +[ "$rcA" = 2 ] \ + && ok "K75 control: --abi reports the break (exit 2) when the cache dir works" \ + || no "K75 control: --abi exited $rcA, not 2" + +for v in "--stray-content" "--stray-content --abi" "--stray-content --plan" "--eval-stray=$TMP/k75labels.tsv"; do + # shellcheck disable=SC2086 + k75 "$TMP/k75_bad.out" "$K75BAD" $v; rcB=$? + if [ "$rcB" = 1 ] && [ ! -s "$TMP/k75_bad.out" ] && grep -q "could not read the branches' blobs" "$TMP/k75_bad.out.err" && grep -q 'TMPDIR' "$TMP/k75_bad.out.err"; then + ok "K75: $v with TMPDIR missing refuses (exit 1, no document, the cause and TMPDIR named on stderr)" + else + no "K75: $v with TMPDIR missing did not refuse (rc=$rcB, stdout $( wc -c <"$TMP/k75_bad.out" | tr -d ' ' ) bytes): $( grep -v 'math degraded\|compact legend' "$TMP/k75_bad.out.err" | head -c 200 )" + fi +done +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stray_content","arguments":{"path":"'"$K75"'"}}}' \ + | env TMPDIR="$K75BAD" "$BIN" --mcp 2>/dev/null | tail -1 >"$TMP/k75_mcp.json" +grep -q '"code":-32603' "$TMP/k75_mcp.json" && grep -q "could not read the branches" "$TMP/k75_mcp.json" \ + && ok "K75: the MCP stray_content twin answers -32603 naming the cause, not a verdict or 'not a git repository'" \ + || no "K75: the MCP twin did not refuse with -32603: $( head -c 240 "$TMP/k75_mcp.json" )" +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stray_content","arguments":{"path":"'"$K75"'"}}}' \ + | env TMPDIR="$K75GOOD" "$BIN" --mcp 2>/dev/null | tail -1 >"$TMP/k75_mcp_good.json" +grep -q 'unmerged' "$TMP/k75_mcp_good.json" && ! grep -q '"error"' "$TMP/k75_mcp_good.json" \ + && ok "K75 control: the MCP twin answers when the cache dir works" || no "K75 control: the MCP twin did not answer on a good TMPDIR" + + # ══ fixture A: a SHALLOW clone — the CI default, and the case that reported every branch as merged ═══════ # # `side` carries two lines of genuinely unmerged work. The shallow clone keeps one commit per ref, so the @@ -273,6 +334,69 @@ REFS_BEFORE="$( git -C "$C" for-each-ref --format='%(refname) %(objectname)' 2>/ [ "$( git -C "$C" for-each-ref --format='%(refname) %(objectname)' 2>/dev/null | cksum )" = "$REFS_BEFORE" ] \ && ok "read-only: no ref was created, moved or deleted" || no "read-only: a crossref verb wrote a ref" +# ══ 6) a blob stream that never ran is not a measured zero (cli-perf-068; the cli-floor §4 finding) ═════════ +# --whereis reads every blob through ONE `git cat-file --batch` whose sha list is a file under the cache dir. With that +# dir unusable (TMPDIR naming a directory that does not exist) the batch never started, and the answer still printed +# on-head="0" hits="0" plus a near-miss note whose retry= was the very name asked — on a repository whose HEAD defines +# it. Now both hosts REFUSE (CLI: exit 1 naming the cause; MCP: an error) instead of printing a count nothing measured. +# 6c is the near-miss negative: a GENUINE zero over a working cache dir must still answer hits="0", exit 0. +H="$TMP/blobless"; mkdir -p "$H" "$TMP/goodtmp" +printf 'int whereisProbeName( int x ) { return x; }\nint whereisProbeCaller( void ) { return whereisProbeName( 1 ); }\n' > "$H/a.c" +git -C "$H" init -q -b main && git -C "$H" add a.c && git -C "$H" commit -qm one +TMPDIR="$TMP/goodtmp/" "$BIN" "$H" --whereis=whereisProbeName >"$TMP/b6a.xml" 2>"$TMP/b6a.err"; rc6a=$? +H6A="$( attr hits < "$TMP/b6a.xml" )" +if [ "$rc6a" = 0 ] && [ -n "$H6A" ] && [ "$H6A" -ge 2 ] 2>/dev/null; then ok "6a control: a working cache dir finds the name ($H6A hits, exit 0)" +else no "6a control: expected exit 0 and >=2 hits, got rc=$rc6a hits=${H6A:-none}"; fi +TMPDIR="$TMP/no-such-dir/" "$BIN" "$H" --whereis=whereisProbeName >"$TMP/b6b.out" 2>"$TMP/b6b.err"; rc6b=$? +if [ "$rc6b" != 0 ] && ! grep -q '"$TMP/b6c.xml" 2>/dev/null; rc6c=$? +if [ "$rc6c" = 0 ] && [ "$( attr hits < "$TMP/b6c.xml" )" = "0" ]; then ok "6c negative: a genuine zero over a working cache dir still answers hits=\"0\", exit 0" +else no "6c negative: a genuine zero was refused or miscounted (rc=$rc6c, hits=$( attr hits < "$TMP/b6c.xml" ))"; fi +if command -v python3 >/dev/null 2>&1; then + M6="$( TMPDIR="$TMP/no-such-dir/" python3 - "$BIN" "$H" <<'PY' +import json, subprocess, sys +msgs = [ { "jsonrpc": "2.0", "id": 1, "method": "initialize" }, + { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "whereis", "arguments": { "path": sys.argv[ 2 ], "symbol": "whereisProbeName" } } } ] +p = subprocess.run( [ sys.argv[ 1 ], "--mcp" ], input = "".join( json.dumps( m ) + "\n" for m in msgs ), capture_output = True, text = True, timeout = 300 ) +lines = [ l for l in p.stdout.splitlines() if l.strip() ] +d = json.loads( lines[ -1 ] ) if lines else {} +res = d.get( "result", {} ) +text = ( res.get( "content" ) or [ {} ] )[ 0 ].get( "text", "" ) +if "error" in d: + print( "ERROR " + d[ "error" ].get( "message", "" ) ) +elif res.get( "isError" ): + print( "ERROR " + text ) +else: + print( "ANSWER " + text[ :200 ] ) +PY +)" + case "$M6" in + "ERROR "*"could not read the trees"*) ok "6d MCP twin: the same unreadable stream is an error naming the cause, not a hit count" ;; + *) no "6d MCP twin: expected an error naming the unread blobs, got: $( printf '%s' "$M6" | head -c 200 )" ;; + esac +fi + +# 6e (train 26c: the K75 x cli-perf-068 one-renderer item): --whereis and the cross-branch sweep refuse in ONE sentence that +# differs only in its noun (crossref::blobsUnreadCause), so the --whereis refusal names TMPDIR and its MCP twin names the cache +# directory, as the sweep's refusal (the K75 loop above, its last stderr) always did. Red before the fold: the --whereis CLI +# sentence never asked about TMPDIR, and its MCP twin never named the directory the sha list is written under. +K75WORDS="(the one \`git cat-file --batch\` did not start or stopped early; its sha list is written under " +K75ASK=" - is TMPDIR a writable directory?)" +if grep -qF "could not read the trees' blobs $K75WORDS" "$TMP/b6b.err" && grep -qF "$K75ASK" "$TMP/b6b.err" \ + && grep -qF "could not read the branches' blobs $K75WORDS" "$TMP/k75_bad.out.err" && grep -qF "$K75ASK" "$TMP/k75_bad.out.err"; then + ok "6e one renderer: the --whereis and cross-branch refusals share every word but the noun, TMPDIR named in both" +else + no "6e one renderer: the two refusals differ beyond the noun — whereis: $( grep 'could not read' "$TMP/b6b.err" | head -c 260 ) | sweep: $( grep 'could not read' "$TMP/k75_bad.out.err" | head -c 260 )" +fi +if [ -n "${M6:-}" ]; then + case "$M6" in + "ERROR "*"could not read the trees' blobs $K75WORDS"*"$K75ASK"*) ok "6e MCP: the whereis error carries the same sentence (the directory and TMPDIR named)" ;; + *) no "6e MCP: the whereis error is not the shared sentence: $( printf '%s' "$M6" | head -c 260 )" ;; + esac +fi + # G4: every emitted document must survive xmllint if command -v xmllint >/dev/null 2>&1; then g4=0 diff --git a/test/declinecheck.sh b/test/declinecheck.sh index e006b02c2..efe3e1847 100755 --- a/test/declinecheck.sh +++ b/test/declinecheck.sh @@ -423,7 +423,7 @@ else # py_undefined() at module level. RED on the pre-#60 binary, which emits neither the row nor the edge. rw --callers=py_undefined >"$TMP/modscope.xml" MS="$( root_tag "$TMP/modscope.xml" callers )" - grep -q '' "$TMP/modscope.xml" \ + grep -q '' "$TMP/modscope.xml" \ && ok '(F) the module-level call to py_undefined is owned by a t="modscope" row (#60)' \ || no "(F) --callers=py_undefined has no modscope row: $MS" [ "$( attr "$MS" count )" = 1 ] \ diff --git a/test/docmdcachecheck.sh b/test/docmdcachecheck.sh index 7b516f9ce..cc40e00cb 100755 --- a/test/docmdcachecheck.sh +++ b/test/docmdcachecheck.sh @@ -10,6 +10,8 @@ # 4. changing the doc's bytes re-invokes (the key is the content, not the path) # 5. an EMPTY extraction is never cached (machine fact, not a byte fact): a no-output fake is # re-invoked on the next run rather than wedging "" into the cache +# 6-9. (cli-perf-068) no shell is started for a markitdown that cannot run, and that answer is never memoized: +# absent-then-installed, a directory ahead on PATH, a non-executable file, an empty PATH entry (the cwd) # Does NOT edit test/regression.sh (the orchestrator wires it). # # RIPWIRE_BIN=build/ripwire bash test/docmdcachecheck.sh @@ -74,5 +76,52 @@ PATH="$TMP/bin:$PATH" "$BIN" "$TMP/corpus" >/dev/null 2>&1 PATH="$TMP/bin:$PATH" "$BIN" "$TMP/corpus" >/dev/null 2>&1 if [ "$( count )" = "5" ]; then ok "empty extraction not cached (silent bridge re-invoked each run)"; else no "empty-extraction arm: expected 5 total invocations, got $( count )"; fi +# 6-9 (cli-perf-068): the post-pass asks the shell's own question — could `markitdown` start at all? — before it +# forks one, so a machine without markitdown no longer pays a /bin/sh per bridge doc per run. Absence is not memoized: +# each arm below would go red if the answer were cached, mis-walked PATH, or ruled the shell out when it could run. +# The PATH is pinned to the fake's dirs plus /usr/bin:/bin so a markitdown installed on the host cannot answer. +SYS_PATH="/usr/bin:/bin" +mkdir -p "$TMP/b6" "$TMP/b7dir/markitdown" "$TMP/b7" "$TMP/b8" "$TMP/b9" +cat > "$TMP/fake" <<'FAKE' +#!/bin/bash +echo "invoked $1" >> "$FAKE_LOG" +echo "# Extracted Deck" +echo "hello from fake markitdown" +FAKE +chmod +x "$TMP/fake" +newdoc(){ printf 'arm %s doc bytes %s $$=%s' "$1" "$( date +%s%N 2>/dev/null || date +%s )" "$$" > "$TMP/corpus/deck.docx"; } + +# 6. absent, then installed between two runs: the first run invokes nothing and carries no doc Section; the second +# sees the new program (a memoized "absent" would keep it at zero) +newdoc 6; before="$( count )" +PATH="$TMP/b6:$SYS_PATH" "$BIN" "$TMP/corpus" > "$TMP/absent.xml" 2>/dev/null +if [ "$( count )" = "$before" ] && ! grep -q 'p="deck.docx"' "$TMP/absent.xml" && grep -q 'a.cpp' "$TMP/absent.xml"; then + ok "absent bridge: no invocation, no doc Section, map still produced" +else no "absent bridge: expected $before invocations and no Section, got $( count ) (map: $( wc -c < "$TMP/absent.xml" ) B)"; fi +cp "$TMP/fake" "$TMP/b6/markitdown" +PATH="$TMP/b6:$SYS_PATH" "$BIN" "$TMP/corpus" > "$TMP/installed.xml" 2>/dev/null +if [ "$( count )" = "$(( before + 1 ))" ] && grep -q 'p="deck.docx"' "$TMP/installed.xml"; then + ok "bridge installed after an absent run is invoked on the next run (absence is not memoized)" +else no "installed-after-absent: expected $(( before + 1 )) invocations and a Section, got $( count )"; fi + +# 7. a DIRECTORY named markitdown earlier on PATH is not a program (sh skips it): the real one after it still runs +newdoc 7; before="$( count )"; cp "$TMP/fake" "$TMP/b7/markitdown" +PATH="$TMP/b7dir:$TMP/b7:$SYS_PATH" "$BIN" "$TMP/corpus" >/dev/null 2>&1 +if [ "$( count )" = "$(( before + 1 ))" ]; then ok "a directory named markitdown ahead on PATH does not hide the program behind it" +else no "dir-ahead arm: expected $(( before + 1 )) invocations, got $( count )"; fi + +# 8. a NON-executable file named markitdown is no program either (sh: rc 126 -> ""): same map as the absent run +cp "$TMP/fake" "$TMP/b8/markitdown"; chmod -x "$TMP/b8/markitdown" +newdoc 8; before="$( count )" +PATH="$TMP/b8:$SYS_PATH" "$BIN" "$TMP/corpus" > "$TMP/noexec.xml" 2>/dev/null +if [ "$( count )" = "$before" ] && ! grep -q 'p="deck.docx"' "$TMP/noexec.xml"; then ok "a non-executable markitdown is treated as absent, as sh treats it" +else no "non-executable arm: expected $before invocations and no Section, got $( count )"; fi + +# 9. an EMPTY PATH entry is the current directory to sh: a markitdown there must still run +newdoc 9; before="$( count )"; cp "$TMP/fake" "$TMP/b9/markitdown" +( cd "$TMP/b9" && PATH=":$SYS_PATH" "$BIN" "$TMP/corpus" >/dev/null 2>&1 ) +if [ "$( count )" = "$(( before + 1 ))" ]; then ok "an empty PATH entry (the cwd) reaches a markitdown there, as sh does" +else no "empty-PATH-entry arm: expected $(( before + 1 )) invocations, got $( count )"; fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/donelegendcheck.sh b/test/donelegendcheck.sh index b4c8e9d72..6678aaaf8 100755 --- a/test/donelegendcheck.sh +++ b/test/donelegendcheck.sh @@ -191,7 +191,10 @@ run_budget qd_dirty_scope 5300 10512 "$FX" --quality-delta "--scope=src/*" # re-inflate under the new headroom. run_budget sd_uses 4100 4112 "$FX" --safe-delete=classifyWidth run_budget sd_none 3800 4112 "$FX" --safe-delete=tangle -# twin of the pre-2026-10-07 sd_uses ceiling: the sd_uses legend with the sites_l= sentence cut out still fits 3800 B. +# twin of the pre-2026-10-07 sd_uses ceiling: the sd_uses legend with the call-site sentence cut out still fits 3800 B. +# CALLSITE-AT (2026-10-09): the row attribute is sites_at= (file:line tokens) where it was sites_l= (bare lines), and the +# sentence is the shared callers/safe-delete one (graphlegend.h kCallSitesAtLegend, 4086 -> 4002 B measured: it is shorter); +# the twin cuts THAT sentence now, from its first word to its last, under the same premises. # The cut is only taken when the row really carries sites_l= AND the sentence is found once, start to end; # any other premise FAILs (a cut that silently removes nothing, or removes the wrong text, would prove nothing). sdRest="$( python3 - "$WORK/sd_uses.xml" <<'PY' @@ -200,9 +203,9 @@ doc = open( sys.argv[1], 'rb' ).read().decode( 'utf-8', 'replace' ) m = re.match( r'\A(?:\s*)+', doc, re.S ) leg = m.group( 0 ) if m else '' body = doc[ len( leg ): ] -if not leg or 'sites_l="' not in body: - print( 'NOPREMISE row-without-sites_l' ); sys.exit( 0 ) -a, z = 'sites_l= on a c row', 'Two calls on one line are one site. ' +if not leg or 'sites_at="' not in body: + print( 'NOPREMISE row-without-sites_at' ); sys.exit( 0 ) +a, z = 'sites_at= on a caller row', '(an aliased import). ' i = leg.find( a ); j = leg.find( z, i + 1 ) if i >= 0 else -1 if i < 0 or j < 0 or leg.count( a ) != 1: print( 'NOPREMISE sentence-not-found-once' ); sys.exit( 0 ) @@ -211,14 +214,14 @@ print( len( rest.encode() ), len( leg.encode() ) - len( rest.encode() ) ) PY )" case "$sdRest" in - NOPREMISE*) no "(a) sd_uses twin (old 3800 ceiling minus the sites_l= sentence) cannot measure: ${sdRest#NOPREMISE }" ;; + NOPREMISE*) no "(a) sd_uses twin (old 3800 ceiling minus the sites_at= sentence) cannot measure: ${sdRest#NOPREMISE }" ;; *) read -r restB cutB </dev/null && [ "$restB" -le 3800 ]; then - ok "(a) sd_uses twin: legend minus the sites_l= sentence ($cutB B) is $restB B <= 3800 B (the pre-CALLSITE-LINE ceiling)" + ok "(a) sd_uses twin: legend minus the sites_at= sentence ($cutB B) is $restB B <= 3800 B (the pre-CALLSITE-LINE ceiling)" else - no "(a) sd_uses twin: legend minus the sites_l= sentence ($cutB B) is ${restB:-?} B > 3800 B — the rest of the essay re-inflated" + no "(a) sd_uses twin: legend minus the sites_at= sentence ($cutB B) is ${restB:-?} B > 3800 B — the rest of the essay re-inflated" fi ;; esac # tg_empty 1200 -> 1400 (2026-09-04, capture-audit wave-1 close, lane L4 M15): the zero-row root now carries @@ -287,9 +290,11 @@ present sd_uses 'untested-radius: callers or uses exist' absent sd_uses 'none-found: zero callers AND zero uses' present sd_none 'none-found: zero callers AND zero uses' absent sd_none 'untested-radius: callers or uses exist' -# sites_l= (CALLSITE-LINE, 2026-10-07) is defined exactly where a caller row carries it: sd_uses has one caller row, sd_none none. -present sd_uses 'sites_l= on a c row' -absent sd_none 'sites_l=' +# sites_l= (CALLSITE-LINE, 2026-10-07; sites_at= since CALLSITE-AT 2026-10-09) is defined exactly where a caller row carries +# it: sd_uses has one caller row, sd_none none. +present sd_uses 'sites_at= on a caller row' +absent sd_none 'sites_at=' +absent sd_uses 'sites_l=' echo echo "=== (d) COVERAGE — every attribute the root emits is DEFINED in that document's own legend ==========" diff --git a/test/editcheckcheck.sh b/test/editcheckcheck.sh index f871fb230..49532a696 100755 --- a/test/editcheckcheck.sh +++ b/test/editcheckcheck.sh @@ -40,7 +40,7 @@ echo "editcheckcheck: BIN=$BIN (temp git repo)" # L1 (2026-09-19): the CLI default legend is compact (its root also carries schema= before sym=); these arms read the FULL # legend's prose and pin the full-default /dev/null ); } +ec(){ _s="$1"; shift; ( cd "$WORK" && "$BIN" . --edit-check="$_s" --no-cache --legend=full "$@" 2>/dev/null ); } ecrc(){ ( cd "$WORK" && "$BIN" . --edit-check="$1" --no-cache >/dev/null 2>&1; echo $? ); } # ── (1) clean tree -> unchanged, exit 0, and lists the one known caller ──────────────────────────────── @@ -51,13 +51,128 @@ OUT1="$( ec helper )" # pull just the caller rows out of the ELEMENT (not the leading , which itself # prose-describes the incompatible="1" attribute and would false-positive a naive whole-output grep). rows(){ printf '%s' "$1" | grep -oE ']*/>'; } +# lane lean-trio fix round 1 (owner ruling 5, rv-lean-trio item 5): a fold ships only when it PAYS (compactlegend.h +# foldPays: a quarter of the bytes, in both postures). One caller's row costs less than the verdict line's own legend, so +# this answer is main's full document again — the useit() row listed, in EVERY posture (L1). RED on 33873861: both postures +# folded the one row into rows_next=. +OUT1C="$( cd "$WORK" && "$BIN" . --edit-check=helper --no-cache 2>/dev/null )" +OUT1L="$( ec helper --limit=1 )" printf '%s' "$OUT1" | grep -q ' "$FOLD/src/util.ts" +printf '%s\n' 'import { blend } from "./util";' 'export function go(): number { return blend(5, 6); }' > "$FOLD/src/main.ts" +echo 'int compute(int a, int b);' > "$FOLD/src/calc.h" +{ + echo '#include "calc.h"' + echo 'int compute(int a, int b) { return a + b; }' + echo 'int use1(void) { return compute(1, 2); }' + for i in 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16; do echo "int use$i(void) { return compute($i, 1); }"; done +} > "$FOLD/src/calc.c" +( cd "$FOLD" && git init -q && git config user.email t@t && git config user.name t && git add -A && git commit -qm init >/dev/null 2>&1 ) +fec(){ _s="$1"; shift; ( cd "$FOLD" && "$BIN" . --edit-check="$_s" --no-cache "$@" 2>/dev/null ); } +fold_verdict(){ # LABEL OUT CALLERS — the verdict alone: callers= counted, no rows, rows_next= naming the --limit call + if printf '%s' "$2" | grep -q "status=\"unchanged\" defs=\"1\" callers=\"$3\"" && [ -z "$( rows "$2" )" ] \ + && printf '%s' "$2" | grep -q "rows_next=\"--edit-check=$4 --limit=$3\""; then ok "$1"; else no "$1"; printf '%s\n' "$2"; fi +} +fold_rows(){ # LABEL OUT ROW — the full document: the named row listed, no rows_next= + if rows "$2" | grep -qF "$3" && ! printf '%s' "$2" | grep -q 'rows_next='; then ok "$1"; else no "$1"; printf '%s\n' "$2"; fi +} +F_TS="$( fec blend --legend=full )"; F_TSC="$( fec blend )"; F_C="$( fec compute )" +fold_verdict "(1v-a) clean tree, 16 callers, --legend=full: the unchanged verdict alone" "$F_TS" 16 blend +fold_verdict "(1v-a) clean tree, 16 callers, default (compact): the same verdict alone" "$F_TSC" 16 blend +fold_verdict "(1v-a) clean tree, C compute (16 callers): the verdict alone" "$F_C" 16 compute +if command -v xmllint >/dev/null 2>&1; then + if printf '%s' "$F_TSC" | xmllint --noout - 2>/dev/null && printf '%s' "$F_TS" | xmllint --noout - 2>/dev/null; then + ok "(1e) the verdict-only answer is well-formed XML in both postures" + else + no "(1e) the verdict-only answer is not well-formed XML" + fi +fi +F_TSL="$( fec blend --limit=16 )" +[ "$( rows "$F_TSL" | wc -l | tr -d ' ' )" = 16 ] && rows "$F_TSL" | grep -qF '' \ + && ok "(1v-b) the rows_next= call lists all 16 callers, main.ts:go among them" \ + || { no "(1v-b) the rows_next= call does not list the 16 callers"; printf '%s\n' "$F_TSL"; } +# ruling 5: 3 callers' rows cost about what the verdict line's legend costs, so the rows ride in both postures +fold_rows "(1v-c) clean tree, 3 callers (default): the fold does not pay, m1 listed" "$( fec mix )" ' src/util.ts.new && mv src/util.ts.new src/util.ts ) +F_D="$( fec blend )" +printf '%s' "$F_D" | grep -q 'status="unchanged"' && printf '%s' "$F_D" | grep -q 'at="[0-9a-f]*+dirty' \ + && fold_rows "(1v-d) dirty tree (TS export dropped): every caller rides, main.ts:go listed" "$F_D" '' \ + || { no "(1v-d) premise: the TS export drop should read unchanged on a +dirty tree"; printf '%s\n' "$F_D"; } +( cd "$FOLD" && git checkout -q src/util.ts \ + && sed 's/^int use1(void) { return compute(1, 2); }/int use1(void) { return compute(1, 2, 3); }/' src/calc.c > src/calc.c.new && mv src/calc.c.new src/calc.c ) +F_D="$( fec compute )" +printf '%s' "$F_D" | grep -q 'status="unchanged"' && ! printf '%s' "$F_D" | grep -q 'incompatible="1"' \ + && fold_rows "(1v-e) dirty tree (C caller at the wrong arity, unflagged): use1 listed" "$F_D" '' \ + || { no "(1v-e) premise: the wrong-arity caller should be unflagged under unchanged"; printf '%s\n' "$F_D"; } +( cd "$FOLD" && git checkout -q src/calc.c && echo 'int stray;' > src/stray.c ) +# the near miss: an UNTRACKED file alone is a working-tree change too (at= says +dirty), so the rows ride +fold_rows "(1v-f) an untracked file alone (+dirty): the rows ride" "$( fec compute )" '' +rm -f "$FOLD/src/stray.c" +# (1v-h) fix round 2 (rv-lean-trio2 nit b): an edit git status cannot see — the caller's file marked skip-worktree, or +# assume-unchanged, then edited to the wrong arity — leaves at= clean, so the stamp alone would fold the unflagged use1 row. +# The fold also refuses on any such index bit (quality.h indexHidesTrackedContent, the probe the qd identity basis uses). +# RED on 9bd8528a: the verdict alone, use1 folded into rows_next=. +for bit in skip-worktree assume-unchanged; do + ( cd "$FOLD" && git update-index --"$bit" src/calc.c \ + && sed 's/^int use1(void) { return compute(1, 2); }/int use1(void) { return compute(1, 2, 3); }/' src/calc.c > src/calc.c.new \ + && cat src/calc.c.new > src/calc.c && rm -f src/calc.c.new ) + F_H="$( fec compute )" + if [ -n "$( cd "$FOLD" && git status --porcelain )" ] || ! printf '%s' "$F_H" | grep -q 'status="unchanged"' \ + || printf '%s' "$F_H" | grep -q 'at="[0-9a-f]*+dirty' || ! grep -q 'compute(1, 2, 3)' "$FOLD/src/calc.c"; then + no "(1v-h) premise ($bit): git status should be silent, at= clean and the edit on disk"; printf '%s\n' "$F_H" + else + fold_rows "(1v-h) $bit + an edited caller (git status silent): the rows ride, use1 listed" "$F_H" '' + fi + ( cd "$FOLD" && git update-index --no-"$bit" src/calc.c && git checkout -q src/calc.c ) +done +fold_verdict "(1v-g) back at HEAD: the verdict again" "$( fec compute )" 16 compute +# fix round 2 (rv-lean-trio2 nit d, checklist 14): an ABSENCE is read only off a run that answered — rc 0, the helper root, +# and its useit row — never off an empty or refused OUT1. +if [ "$( ecrc helper )" != 0 ] || ! printf '%s' "$OUT1" | grep -q ' unchanged (the contract is params+publicness, NOT the body) ────────────────── cat > "$WORK/src/a.cpp" <<'EOF' @@ -94,9 +209,14 @@ printf '%s' "$OUTC" | grep -q 'status="new-symbol"' \ && ok "(c) brand-new symbol -> status=new-symbol" || { no "(c) expected new-symbol"; printf '%s\n' "$OUTC"; } printf '%s' "$OUTC" | grep -q 'callers="0"' \ && ok "(c) brand-new symbol has zero callers" || { no "(c) unexpected callers on a brand-new symbol"; printf '%s\n' "$OUTC"; } -printf '%s' "$OUTC" | grep -q 'params_was=' \ - && no "(c) new-symbol should not report was/now (nothing to compare against)" \ - || ok "(c) new-symbol correctly omits was/now" +# train 27 (checklist 14, the (e) sibling): the absence is read only off the brandnew root, never off an empty answer. +if ! printf '%s' "$OUTC" | grep -qE ']*sym="brandnew"'; then + no "(c) premise: no answer (an empty answer must not read as 'omits was/now')" +elif printf '%s' "$OUTC" | grep -q 'params_was='; then + no "(c) new-symbol should not report was/now (nothing to compare against)" +else + ok "(c) new-symbol correctly omits was/now" +fi # ── (d) unknown SYM -> refuses loudly (nonzero exit, a stderr message, never a crash / empty map) ────── ERR_OUT="$( cd "$WORK" && "$BIN" . --edit-check=totallyNoSuchSymbol --no-cache 2>&1 >/dev/null )" @@ -165,9 +285,15 @@ printf '%s' "$OUTE" | grep -q 'params_was="1" params_now="3"' \ && defrows "$OUTE" | grep -q 'params="1"' && defrows "$OUTE" | grep -q 'params="3"'; } \ && ok "(e) additive overload: both folded definitions listed, one still at params=\"1\"" \ || { no "(e) def rows missing — an additive overload is indistinguishable from a widened definition"; printf '%s\n' "$OUTE"; } -rows "$OUTE" | grep -q 'incompatible="1"' \ - && no "(e) additive overload wrongly flags a caller (no existing definition changed)" \ - || ok "(e) additive overload: no caller flagged (precision)" +# train 27 (lean-trio signoff, checklist 14): the ABSENCE is read only off a run that answered — rc 0, the folded root and its +# callsit row — never off an empty or refused OUTE (a refusing binary printed nothing here and this arm said ok). +if [ "$( ecrc folded )" != 0 ] || ! printf '%s' "$OUTE" | grep -qE ']*sym="folded"' || ! rows "$OUTE" | grep -q 'n="callsit"'; then + no "(e) additive overload: precision premise — folded should answer (rc 0) with its callsit row"; printf '%s\n' "$OUTE" +elif rows "$OUTE" | grep -q 'incompatible="1"'; then + no "(e) additive overload wrongly flags a caller (no existing definition changed)" +else + ok "(e) additive overload: no caller flagged (precision)" +fi # the CONTRAST shape: one definition genuinely widened. Same status, and everything else must differ. cat > "$WORK/ovl/o.cpp" <<'EOF' @@ -406,9 +532,14 @@ if printf '%s' "$OUTI_F" | grep -q 'status="no-baseline"'; then else no "(h2) non-git root did not answer no-baseline: $( printf '%s' "$OUTI_F" | grep -o 'status="[^"]*"' )" fi -printf '%s' "$OUTI_F" | grep -q 'status="new-symbol"' \ - && no "(h2) non-git root still calls an existing symbol new-symbol" \ - || ok "(h2) non-git root never says new-symbol" +# train 27 (checklist 14, the (e) sibling): the absence is read only off the freefn root, never off an empty answer. +if ! printf '%s' "$OUTI_F" | grep -qE ']*sym="freefn"'; then + no "(h2) premise: no answer (an empty answer must not read as 'never says new-symbol')" +elif printf '%s' "$OUTI_F" | grep -q 'status="new-symbol"'; then + no "(h2) non-git root still calls an existing symbol new-symbol" +else + ok "(h2) non-git root never says new-symbol" +fi # both selectors must resolve, or the two arms below assert nothing (trap #20: a pin that stops measuring). if ! printf '%s%s' "$OUTI_M" "$OUTI_F" | grep -q '/dev/null 2>&1 ) # the arity widening: both callers become provably incompatible, one at two sites and one at a single site. +# train 27: `fine` is a NEW caller written to the widened arity, so the document carries one UNFLAGGED row — the row the +# (k) counterpart arm below reads its absence off (before it, every row was flagged and that arm could not fail). cat > "$SFIX/src/sites.cpp" <<'EOF' int scored( int a, int b ) { return a * b; } @@ -538,6 +676,11 @@ int once( int n ) { return scored( n ); } + +int fine( int n ) +{ + return scored( n, 1 ); +} EOF OUTK="$( cd "$SFIX" && "$BIN" . --edit-check=scored --no-cache 2>/dev/null )" USESK="$( cd "$SFIX" && "$BIN" . --uses=scored --no-cache 2>/dev/null )" @@ -575,9 +718,14 @@ EOF fi # the counterpart rule: a row NOT flagged incompatible carries no sites= — the attribute is the flagged # row's own evidence, not a fourth spelling of the caller list (grep the ELEMENT, never the legend prose). -printf '%s' "$OUTK" | grep -oE ']*/>' | grep -v 'incompatible="1"' | grep -q 'sites_l="' \ - && no "(k) sites_l= leaked onto a row that is not flagged incompatible" \ - || ok "(k) sites_l= rides only on the flagged rows" +# train 27 (checklist 14, the (e) sibling): the absence is read only off an answer that HAS unflagged rows to read. +if ! printf '%s' "$OUTK" | grep -qE ']*sym="scored"' || ! printf '%s' "$OUTK" | grep -oE ']*/>' | grep -qv 'incompatible="1"'; then + no "(k) premise: no answer with an unflagged row (nothing to read the absence off)"; printf '%s\n' "$OUTK" +elif printf '%s' "$OUTK" | grep -oE ']*/>' | grep -v 'incompatible="1"' | grep -q 'sites_l="'; then + no "(k) sites_l= leaked onto a row that is not flagged incompatible" +else + ok "(k) sites_l= rides only on the flagged rows" +fi rm -rf "$SFIX" # ── WARM-TIME budget: <= 100 ms on ripwire's OWN src/, after the qheadsnap/qsnap cache is primed ──────── diff --git a/test/editpreviewcheck.sh b/test/editpreviewcheck.sh index 52ea31e9c..30a99865a 100755 --- a/test/editpreviewcheck.sh +++ b/test/editpreviewcheck.sh @@ -98,8 +98,9 @@ statusOf(){ printf '%s' "$1" | sed -e 's///' | grep -oE 'status="[a-z-] # L1 (2026-09-19): the CLI default legend is compact; (D)/(R) read the FULL legend's prose and (N) compares against an MCP # document asked for in "legend": "full", so the preview (and the applied document it is swept against) ask for the full legend. -preview(){ # $1 selector $2 payload-path - ( cd "$BASE" && "$BIN" . --edit-check="$1" --edit-payload="$2" --dry-run --no-cache --legend=full 2>/dev/null ) +preview(){ # $1 selector $2 payload-path [$3 ...] extra flags + local sel="$1" pay="$2"; shift 2 + ( cd "$BASE" && "$BIN" . --edit-check="$sel" --edit-payload="$pay" --dry-run --no-cache --legend=full "$@" 2>/dev/null ) } previewrc(){ ( cd "$BASE" && "$BIN" . --edit-check="$1" --edit-payload="$2" --dry-run --no-cache >/dev/null 2>&1; echo $? ) @@ -109,7 +110,10 @@ applied(){ # $1 name $2 file $3 payload-path → the POST-apply --edit-check local w="$WORK/apply.$$.$RANDOM" mkcorpus "$w" ( cd "$w" && "$BIN" . --replace-symbol-body="$1" --edit-target-file="$2" --edit-payload="$3" ) >/dev/null 2>&1 - ( cd "$w" && "$BIN" . --edit-check="$2:$1" --no-cache --legend=full 2>/dev/null ) + # lane lean-trio (idea #11): an unchanged, unflagged post-hoc contract answers with its verdict line alone; an explicit + # --limit (the verdict's rows_next= call, at the verb's own 40-row cap) is the FULL document, so the comparison below + # hands the SAME --limit=40 to both sides (an explicit window adds the same window attributes to each). + ( cd "$w" && "$BIN" . --edit-check="$2:$1" --no-cache --legend=full --limit=40 2>/dev/null ) rm -rf "$w" } @@ -134,7 +138,7 @@ while IFS=$'\t' read -r id cls name file pay note; do valid=$(( valid + 1 )) [ "$cls" = "change" ] && nchange=$(( nchange + 1 )) [ "$cls" = "preserve" ] && npreserve=$(( npreserve + 1 )) - pre="$( preview "$file:$name" "$P" )" + pre="$( preview "$file:$name" "$P" --limit=40 )" post="$( applied "$name" "$file" "$P" )" [ -z "$firstPreview" ] && firstPreview="$pre" [ "$id" = "chg01" ] && changePreview="$pre" diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 05117459d..755f0b456 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -23,6 +23,14 @@ # 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. +# # 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. @@ -127,7 +135,23 @@ for entry in "${SHAPES[@]}"; do done # ── #2: MONOTONE charging — adding a payload to the same map can never LOWER the reported estimate. ──── -E_MAP="$( est_of "$TMP/map.out" )" +# D7 (2026-10-08, by-design break, PROCESS rule 4): the bare map now carries the inventory tier (, the files its +# ranked rows leave out), and a payload verb's ride-along map does not — so "the same map" is the bare map WITHOUT that +# tier. --no-inventory is that map (the same rows, no tier; #2-rows proves both premises). The old comparison is kept +# as #2-tier: the bare map with its tier must charge at least what the rows alone do. +"$BIN" src --top-k=10 --no-inventory --no-cache >"$TMP/map_rows.out" 2>/dev/null +E_MAP="$( est_of "$TMP/map_rows.out" )" +E_TIER="$( est_of "$TMP/map.out" )" +rows_of(){ sed 's/ (est $E_MAP)" +else + no "#2-rows premise broken: the rows-only twin is not the bare map minus its tier (est ${E_MAP:-?}) — #2/#3 compare against the wrong map" +fi +[ -n "$E_TIER" ] && [ -n "$E_MAP" ] && [ "$E_TIER" -ge "$E_MAP" ] 2>/dev/null \ + && ok "#2-tier: the bare map with its inventory reports est_tokens=$E_TIER >= the rows alone ($E_MAP)" \ + || no "#2-tier: the bare map with its inventory reports ${E_TIER:-?} < the rows alone (${E_MAP:-?}) — the tier is uncharged" mono=1 for label in metrics sigs src bodies outline "bodies+sigs"; do E="$( est_of "$TMP/$label.out" )" @@ -141,7 +165,8 @@ done # streamed 12 KB / 67 KB. Budget is set from the bare map's own estimate (so the map alone always # fits) and each payload arm must fail it. ──────────────────────────────────────────────────────────── TB=$(( E_MAP + 20 )) -"$BIN" src --top-k=10 --token-budget=$TB --no-cache >"$TMP/tb_map.out" 2>"$TMP/tb_map.err" +# D7: the control is the rows-only twin above (the bare map's tier is charged and need not fit this budget) +"$BIN" src --top-k=10 --no-inventory --token-budget=$TB --no-cache >"$TMP/tb_map.out" 2>"$TMP/tb_map.err" rc_map=$? [ "$rc_map" -eq 0 ] \ && ok "#3 control: the bare map fits --token-budget=$TB (exit 0)" \ @@ -580,17 +605,48 @@ 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). 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}" )" + # shellcheck disable=SC2086 + "$BIN" src --for="$FOR_TASK" --token-budget=$tw_n $tw_args --no-cache >"$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" +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=). +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' +rows_of(){ grep -aoE ']*>' "$1" | sed -nE 's/.* n="([^"]*)".*/\1/p'; } "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --no-cache >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? +"$BIN" src --for="$FOR_TASK" --token-budget=$TBF --no-cache >"$TMP/f_tbp.out" 2>/dev/null FTB="$( bytes_of "$TMP/f_tb.out" )" # THE UNPAID TWIN (knob-honesty-068; orchestrator rulings 2026-10-07, PROCESS rules 4/5). A capped under a # hard ceiling carries next= — the call that recovers its cut — and pays for it from its rows ONLY where that makes the # answer fit (ruling C3). Where it cannot (the rank 1..4 floor has nothing left to shed, or the answer lands past its ceiling # paid or not) no row is dropped for it: the recovery handle ALWAYS ships, unpaid, and the document may then land past the # allowance, and only if it says so. This is not a wider allowance: every condition of the old bound still holds for -# everything except the disclosed handle. sigs_unpaid_twin FILE ALLOW → exit 0 iff ALL of: the root carries +# 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 — @@ -611,19 +667,67 @@ 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: the header floor' in d +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 } -if { [ "$rc_ftb" -eq 0 ] && [ "$FTB" -le "$ALLOW" ]; } 2>/dev/null; then - ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B within the $ALLOW B allowance (exit 0, SHAPED)" -elif [ "$rc_ftb" -eq 0 ] && a7why="$( sigs_unpaid_twin "$TMP/f_tb.out" "$ALLOW" )"; then - ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: the unpaid twin — $a7why, labelled over_ceiling=\"1\" with next= (exit 0, SHAPED)" +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_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)" +a7b_b1="$( grep -aoE ']*>' "$TMP/f_tb.out" | head -1 )" +{ [ -n "$a7b_b1" ] && { [ "${a7b_b1#* truncated=\"1\"}" = "$a7b_b1" ] || [ "${a7b_b1#* next=\"--expand=}" != "$a7b_b1" ]; }; } \ + || a7b_why="$a7b_why first-body-cut-without-next" +[ -z "$a7b_why" ] \ + && ok "#11 A7B (by-design, owner option B) --token-budget=$TBF --detail=20 --with-graph: $FTB B past the $ALLOW B allowance, over_ceiling=\"1\", keeps all $a7b_pn rows of the flagless budget answer ($a7b_n rows, the uncapped answer's top), reason names the kept rows (header ${a7b_hdr} B fits alone)" \ + || no "#11 A7B (by-design, owner option B) --token-budget=$TBF --detail=20 --with-graph —$a7b_why" +# A7B-X (fix round 6, checklist 25): the kept-rows note rides INSIDE the header XML comment, so the answers that carry it +# must be well-formed XML — its first spelling held two flag names with their leading double hyphens and every such +# answer failed xmllint ("Double hyphen within comment"), on the compact default and on --legend=full alike. +if command -v xmllint >/dev/null 2>&1; then + "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --legend=full --no-cache >"$TMP/f_tbl.out" 2>/dev/null + a7x_bad="" + 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)" + 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" else - no "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B vs the $ALLOW B allowance, exit $rc_ftb — the budget does not bound the appended sections${a7why:+ (unpaid twin: $a7why)}" + 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). +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())' )" +"$BIN" src --for="$A7N_TASK" --token-budget=300 --detail=3 --with-graph --no-cache >"$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" +[ -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" # 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)" \ @@ -685,11 +789,20 @@ for spec in "default:760:1500:10:serialize the map:" "detail_graph:2880:3080:10: s_b="$( bytes_of "$A7S/o.xml" )" s_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" s_root="$( grep -aoE '^]*>' "$A7S/o.xml" | head -1 )" - # past the allowance: the ladder's last rung says so, or (knob-honesty-068) the unpaid twin above holds in full - if [ "$s_rc" -eq 0 ] && [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/o.xml" \ + # 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: the header floor' "$A7S/o.xml"; }; then + 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 @@ -705,18 +818,27 @@ done [ "$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. -( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=780 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? -fl_a="$( awk 'BEGIN{printf "%d", 780*2.36*1.15}' )" +( cd "$A7S" && "$BIN" 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 @780: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" + ok "#11 A7 unpaid twin @900: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" else - no "#11 A7 unpaid twin @780 (exit $fl_rc): ${fl_why:-the ladder last rung fired, so the twin is not the only disclosure} — the floor did not ship its next= with a labelled, honestly priced overshoot" + 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" 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); @@ -724,30 +846,248 @@ fi # own rows — at least 255dc199's 5 (re-pin from a pre-continuation binary if row bytes change; smaller rows only raise it) — # and MORE than the paid near miss at 850 serves (4 rows: there paying is what makes the answer fit), with next= riding # unpaid and over_ceiling="1" + the unpaid clause on the root. RED on dd6e4c8e (4 rows). -( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=790 --no-cache ) >"$A7S/c3.xml" 2>/dev/null; c3_rc=$? -( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=850 --no-cache ) >"$A7S/paid.xml" 2>/dev/null; pd_rc=$? +# 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. +mkdir -p "$A7S/c3/corpus" +python3 - "$A7S/c3/corpus" <<'PYG' +import os, sys +out = sys.argv[1] +for i in range( 4 ): + lines = [] + for j in range( 8 ): + nxt = f"serializeRow{i}_{j + 1}( map, row )" if j + 1 < 8 else "0" + lines += [ f"int serializeRow{i}_{j}( int map, int row )", "{", + f" int acc = map + row + {i * 7 + j};", + f" for( int k = 0; k < {j + 3}; ++k )", " {", + f" acc += k * {i + 1} - row;", " }", + f" return acc + {nxt};", "}", "" ] + with open( os.path.join( out, f"mod{i}.cpp" ), "w" ) as fh: + fh.write( "\n".join( lines ) ) +PYG +( cd "$A7S/c3" && "$BIN" corpus --for="map row serializer" --token-budget=745 --no-cache ) >"$A7S/c3.xml" 2>/dev/null; c3_rc=$? +( cd "$A7S/c3" && "$BIN" 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 @790: over its ceiling either way, the answer keeps the cut's own $c3_sh rows (>= 255dc199's 5, > the paid 850 answer's $pd_sh) — next= unpaid, over_ceiling=\"1\" + the unpaid clause" + 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 @790 (exit $c3_rc): shown=${c3_sh:-?} (want >= 5 and > the paid 850 answer's ${pd_sh:-?}), next= + over_ceiling=\"1\" + the unpaid clause — rows were dropped to pay for a handle that cannot make the answer fit" + 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, 850 — the PAYABLE case, unchanged: paying is what makes the answer fit (255dc199: 5 rows, est_tokens=911, +# 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", 850*2.36*1.15}' )" +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 @850: the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no unpaid clause" + 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 @850 (exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" + no "#11 A7 unpaid twin near miss @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 +# ── #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). +A7R="$TMP/a7reserve" +mkdir -p "$A7R/corpus" +python3 - "$A7R/corpus" <<'PYG' +import os, sys +out = sys.argv[1] +for i in range( 6 ): + lines = [] + for j in range( 12 ): + nxt = f"serializeRow{i}_{j + 1}( map, row )" if j + 1 < 12 else "0" + body = [ f" int acc = map + row + {i * 7 + j};" ] + [ f" acc += flushMapRow( map, row, {k} ) * {i + 1};" for k in range( 30 + j ) ] + lines += [ f"// serializeRow{i}_{j}: serialize one map row into the output buffer the map writer flushes, keeping the row order stable", + f"int serializeRow{i}_{j}( int map, int row, int column, int stride, int width, int height, int depth, int offset, int flags, int mode )", + "{" ] + body + [ f" return acc + {nxt};", "}", "" ] + with open( os.path.join( out, f"mod{i}.cpp" ), "w" ) as fh: + fh.write( "\n".join( lines ) ) +PYG +A7R_Q="serialize the map row" +a7r_out(){ ( cd "$A7R" && "$BIN" 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_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 + # 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_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) + 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" + [ "${r_b1#* over_ceiling=}" = "$r_b1" ] || r_why="$r_why first-body-stub" + { [ "${r_b1#* truncated=\"1\"}" = "$r_b1" ] || [ "${r_b1#* next=\"--expand=}" != "$r_b1" ]; } || r_why="$r_why cut-without-next" + fi + [ -z "$r_why" ] || a7r_bad="$a7r_bad $r_label@$N:[${r_why# }]" + 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=. +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 )" + 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" + [ -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). +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 + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$A7R/k2.xml" 2>/dev/null || k2_why="$k2_why xmllint-rejected" + fi + k2_next="$( grep -aoE ']*>' "$A7R/k2.xml" | head -1 | sed -nE 's/.* truncated="1".* next="--expand=([^"]*)".*/\1/p' )" + if [ -z "$k2_next" ]; then + k2_why="$k2_why first-body-not-head-cut-with-next" + else + ( cd "$A7R" && "$BIN" corpus --expand="$k2_next" --no-cache ) >"$A7R/k2n.xml" 2>/dev/null + python3 - "$A7R/k2.xml" "$A7R/k2n.xml" "$A7R/corpus" <<'PYR' || k2_why="$k2_why next=-does-not-recover-the-cut" +import re, sys, os +cut = open( sys.argv[1] ).read(); nxt = open( sys.argv[2] ).read() +m1 = re.search( r']* p="([^"]+)" n="[^"]+" lines="1-(\d+)/(\d+)" truncated="1"[^>]*>', cut, re.S ) +l1 = re.search( r']* l="(\d+)" p="', cut ) +m2 = re.search( r']* lines="(\d+)-(\d+)/(\d+)"[^>]*>', nxt, re.S ) +if not ( m1 and l1 and m2 ): sys.exit( 1 ) +k, t = int( m1.group( 2 ) ), int( m1.group( 3 ) ) +if ( int( m2.group( 1 ) ), int( m2.group( 2 ) ), int( m2.group( 3 ) ) ) != ( k + 1, t, t ): sys.exit( 1 ) +src = open( os.path.join( sys.argv[3], m1.group( 1 ) ) ).read().split( "\n" ) +start = int( l1.group( 1 ) ) - 1 +want = "\n".join( src[ start : start + t ] ) +got = m1.group( 4 ).rstrip( "\n" ) + "\n" + m2.group( 4 ) +sys.exit( 0 if got.rstrip( "\n" ) == want.rstrip( "\n" ) else 1 ) +PYR + fi + [ -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" +# 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" +r_b1="$( grep -aoE ']*>' "$A7R/f.xml" | head -1 )" +r_hi="$( printf '%s' "$r_b1" | sed -nE 's/.* lines="1-([0-9]+)\/[0-9]+".*/\1/p' )" +{ [ "${r_b1#* truncated=\"1\"}" != "$r_b1" ] && [ "${r_b1#* over_ceiling=}" = "$r_b1" ] && [ "${r_hi:-0}" -ge 2 ] 2>/dev/null \ + && [ "${r_b1#* next=\"--expand=}" != "$r_b1" ]; } \ + && ok "#11 A7R floor: the first --detail body at 1500 tokens is head-cut at the floor with lines= and next= ($r_b1)" \ + || 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" +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" \ + || no "#11 A7R no ceiling: differs with --with-graph and no --token-budget (or the arm's is not cut) — the reserve leaked into the default regime" +# A7R-N2 (near-miss negative, a WIDE explicit ceiling in the auto bundle): the sig side is already frozen at the default +# share (forSigSideCeiling) and the bundle holds that share AND the graph, so the reserve does not bind — +# byte-identical with and without --with-graph at 8000 tokens (forbudgetmonotoncheck's invariant, seen from this side). +a7r_out --token-budget=8000 >"$A7R/w0.xml"; a7r_out --token-budget=8000 --with-graph >"$A7R/w1.xml" +w0="$( a7r_sigs "$A7R/w0.xml" )"; w1="$( a7r_sigs "$A7R/w1.xml" )" +{ [ -n "$w0" ] && [ "$w0" = "$w1" ] && [ "${w0#*capped=\"1\"}" != "$w0" ]; } \ + && ok "#11 A7R wide ceiling: (capped=1) byte-identical with and without --with-graph at --token-budget=8000 — the reserve binds only when the bundle cannot hold both" \ + || no "#11 A7R wide ceiling: differs with --with-graph at --token-budget=8000 (or is not cut) — the reserve shrank a sig share that had room" + +# ── #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" +h_tag="$( grep -aoE ']*>' "$A7R/h.xml" | head -1 )" +h_root="$( grep -aoE '^]*>' "$A7R/h.xml" | head -1 )" +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_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}…')" +# 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" ] \ + && 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" +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" + # 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 # (kBytesPerTokenDefault). Pre-fix the est_tokens attribute (~19 B) and weak="1" (9 B) sat outside the sum, diff --git a/test/expandbodyfirstcheck.sh b/test/expandbodyfirstcheck.sh index 8c39d11fc..c382ed4a4 100755 --- a/test/expandbodyfirstcheck.sh +++ b/test/expandbodyfirstcheck.sh @@ -36,13 +36,17 @@ TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT # L1 (2026-09-19): the CLI default legend is compact and spells / inside its comment; these arms measure the # byte offsets and counts of the REAL elements, so the three runs ask for the full legend. -"$BIN" "$FIX" --expand=dupTarget --pack-budget-bytes=10 --no-cache --legend=full >"$TMP/dup.xml" 2>"$TMP/dup.err" +# expand-lean-k64 (2026-10-08): an --expand whose definitions are all served no longer carries a map by itself (see +# expandtopk0check (H)); the map still rides — body-first reorder, note=, stderr note — wherever a composed payload owns +# it. --outline beside --expand is that shape, so this gate's probe composes the two. The ride-along mechanics under test +# are unchanged; only the way to reach them moved. +"$BIN" "$FIX" --expand=dupTarget --outline=uniqueTarget --pack-budget-bytes=10 --no-cache --legend=full >"$TMP/dup.xml" 2>"$TMP/dup.err" -# ── sanity: the fixture really landed in bundle mode (map+bodies both ride) — otherwise every arm below -# proves nothing (identical to expandtopk0check.sh (B)'s own sanity arm, same fixture, same forcing flag). -grep -q 'mode="bundle"' "$TMP/dup.xml" \ - && ok "(A) sanity: the ambiguous probe landed in bundle mode (map+bodies both ride)" \ - || no "(A) sanity failed: --pack-budget-bytes=10 did not force bundle mode — arm proves nothing" +# ── sanity: the map really rides beside the bodies (a composed --outline owns it) — otherwise every arm below +# proves nothing. +grep -q ' starts before the ranked map's , or no map rides at all. ─────────── bodiesOff="$( grep -bo '"$TMP/ambig.xml" 2>/dev/null ambTotal="$( wc -c <"$TMP/ambig.xml" | tr -d ' ' )" amb_num="$( reason_num "$TMP/ambig.xml" bundle )" -if ! grep -q '"$TMP/ambig5.xml" 2>/dev/null +if grep -q ']*>' "$TMP/ambig5.xml" )" fi [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" diff --git a/test/expandtopk0check.sh b/test/expandtopk0check.sh index 6cb00e2b1..70216185e 100755 --- a/test/expandtopk0check.sh +++ b/test/expandtopk0check.sh @@ -58,27 +58,33 @@ else ok "(A) no ride-along stderr note (nothing is riding along to warn about)" fi -# ── (B) ambiguous name (2 defs), no --top-k: the ORDINARY default applies — map rides, note fires ──────── -# --pack-budget-bytes=10 forces M6's whole-file candidate over budget (dupA.c+dupB.c together are well -# over 10 B), so chooseExpandServe picks mode="bundle" deterministically regardless of this tiny fixture's -# absolute byte counts — the note only ever fires in bundle mode ("never in whole-file mode" is the -# pre-existing, correct rule; this arm needs bundle mode to observe it at all). +# ── (B) ambiguous name (2 defs), the ORDINARY default where the lean default does not apply ──────────────── +# expand-lean-k64 (2026-10-08): a multi-definition name whose definitions are ALL served now drops the map too — see (H) +# below. This TWIN pins the path that stays: wherever the lean default is excluded the caller's ordinary default applies, the +# ranked map rides along and the pre-existing "ranked top-N map rides along" stderr note still fires, with NO topk_default=. +# --max-tokens=3000 is such a shape (it sizes the map, so the exact-name/lean defaults both stand aside); the explicit +# --top-k=5 twin right below pins the other half (the caller asked for the map). "$BIN" "$FIX" --expand=dupTarget --top-k=5 --no-cache >"$TMP/dup5.xml" 2>/dev/null grep -q '"$TMP/dup.xml" 2>"$TMP/dup.err" -grep -q 'mode="bundle"' "$TMP/dup.xml" \ - && ok "(B) sanity: the ambiguous probe landed in bundle mode (the note's only firing ground)" \ - || no "(B) sanity failed: --pack-budget-bytes=10 did not force bundle mode — arm proves nothing" -if grep -q 'topk_default="0"' "$TMP/dup.xml"; then - no "(B) ambiguous --expand (2 matches) wrongly got the exact-name default" +if grep -qE 'topk_default=|map_next=' "$TMP/dup5.xml"; then + no "(B) explicit --top-k=5 on a multi-def name carries lean-default decoration — the caller chose, not the tool" else - ok "(B) ambiguous --expand carries NO topk_default= — the multi-match case is untouched" + ok "(B) explicit --top-k=5 on a multi-def name keeps the classic shape (no topk_default=, no map_next=)" +fi +"$BIN" "$FIX" --expand=dupTarget --max-tokens=3000 --pack-budget-bytes=10 --no-cache >"$TMP/dup.xml" 2>"$TMP/dup.err" +grep -q '"$TMP/tk5.xml" 2>/dev/null @@ -180,9 +186,240 @@ else no "(G-b) no reason=\"bundle NNNB ...\" clause found on the real-repo default root — unexpected mode, see (G) above: $( grep -oE ']*>' "$TMP/real_default.xml" )" fi +# ── (H) expand-lean-k64: a multi-definition name, EVERY definition served — no ride-along map ───────────────────────── +# `subtokens` has three definitions (one C++ header, two Python fixtures) and the default budget serves all of them in +# bundle mode (the header is far over the pack budget as a whole file). The root discloses the default (topk_default="0"), +# carries the recoverable pointer to the map it did not ship (map_next=), the bodies keep p= l= for each def, and no / +# rows ride. (The tiny dupTarget fixture serves whole-file instead — (J2) pins that.) +"$BIN" "$ROOT" --expand=subtokens --no-cache --legend=full >"$TMP/mdef.xml" 2>"$TMP/mdef.err" +grep -q 'topk_default="0"' "$TMP/mdef.xml" && grep -q 'map_next="ripwire ' "$TMP/mdef.xml" \ + && ok "(H) multi-def, all served: root carries topk_default=\"0\" and map_next=" \ + || no "(H) multi-def default lacks topk_default=/map_next=: $( grep -oE ']*>' "$TMP/mdef.xml" )" +if grep -qE '"$TMP/mapnext.xml" 2>/dev/null + grep -q '"$TMP/mdef_tk.xml" 2>/dev/null +bodiesOf(){ perl -0777 -ne 'print $1 if /()/s' "$1"; } +[ -n "$( bodiesOf "$TMP/mdef.xml" )" ] && [ "$( bodiesOf "$TMP/mdef.xml" )" = "$( bodiesOf "$TMP/mdef_tk.xml" )" ] \ + && ok "(H2) the lean answer's are byte-identical to the explicit --top-k=3 twin's" \ + || no "(H2) the lean answer's differ from the explicit twin's (every body must still be served)" +nb=$( grep -oE ', shown==total)" \ + || no "(H2) the lean answer serves $nb located , expected >= 3 with shown==total" + +# ── (I) bodies CUT: a symbol-scoped aid (the unserved defs), never the generic map ───────────────────────────────── +"$BIN" "$FIX" --expand=dupTarget --pack-budget-bytes=10 --no-cache >"$TMP/cut.xml" 2>/dev/null +if grep -qE ']*>' "$TMP/cut.xml" )" +unNext=$( grep -oE 'unserved_next="[^"]*"' "$TMP/cut.xml" | head -1 | sed 's/^unserved_next="//;s/"$//' ) +case "$unNext" in + --expand=*dupB.c:*:dupTarget) + if grep -q 'dupB.c:2:dupTarget' <<<"$unNext"; then + "$BIN" "$FIX" "$unNext" --no-cache >"$TMP/cut2.xml" 2>/dev/null + grep -q 'return 2' "$TMP/cut2.xml" \ + && ok "(I) unserved_next serves exactly the cut definition (dupB.c's body)" \ + || no "(I) unserved_next did not serve dupB.c's body" + grep -q 'return 1' "$TMP/cut2.xml" \ + && no "(I) unserved_next re-served the definition that was already served" \ + || ok "(I) unserved_next names only the unserved def (dupA.c's body is not re-served)" + else + no "(I) unserved_next names the wrong line: $unNext" + fi ;; + *) no "(I) unserved_next missing or names the wrong def: '$unNext'" ;; +esac +grep -q ']*p="dupA.c"' "$TMP/cut.xml" \ + && ok "(I) the served def (dupA.c) is still in the cut answer" || no "(I) the first def is missing from the cut answer" + +# ── (J) the whole-file comparison prices the PAYLOAD, not a map ───────────────────────────────────────────────────── +# (J1) real repo: emitTo (2 overloads, one file) used to serve the whole 24 KB file because the bundle was priced with the +# ~19 KB map. Now bundle mode, the priced bundle is the served document, and the document is far under the file. +"$BIN" "$ROOT" --expand=emitTo --no-cache --legend=full >"$TMP/emit.xml" 2>/dev/null +if grep -q 'mode="bundle"' "$TMP/emit.xml"; then + pb=$( grep -oE 'reason="bundle [0-9]+B' "$TMP/emit.xml" | grep -oE '[0-9]+' ) + sz=$( wc -c < "$TMP/emit.xml" | tr -d ' ' ) + [ -n "$pb" ] && [ "$pb" = "$sz" ] && [ "$sz" -lt 12000 ] \ + && ok "(J1) emitTo: bundle mode, priced bundle ${pb}B == served ${sz}B (< 12000B; was the whole 24788B file)" \ + || no "(J1) emitTo bundle priced '${pb}' vs served ${sz}B" +else + no "(J1) emitTo is not in bundle mode: $( grep -oE ']*>' "$TMP/emit.xml" )" +fi +# (J2) near-miss: where the file really is cheaper (tiny fixture) whole-file STILL wins, with the lean disclosure intact +"$BIN" "$FIX" --expand=dupTarget --no-cache --legend=full >"$TMP/small.xml" 2>/dev/null +grep -q 'mode="whole-file"' "$TMP/small.xml" && grep -q 'topk_default="0"' "$TMP/small.xml" && ! grep -q ']*>' "$TMP/small.xml" )" +if grep -q 'unserved_' "$TMP/small.xml"; then + no "(J2) whole-file serving names unserved defs though the file carries every one" +else + ok "(J2) whole-file serving carries no unserved_* (the file has every definition)" +fi + +# ── (K) --outline follows the same rule ────────────────────────────────────────────────────────────────────────────── +"$BIN" "$FIX" --outline=dupTarget --no-cache >"$TMP/ol.xml" 2>/dev/null +if grep -qE '"$TMP/ol5.xml" 2>/dev/null +grep -q '"$TMP/olcut.xml" 2>/dev/null +grep -q 'unserved_total=' "$TMP/olcut.xml" && grep -q 'unserved_next="--outline=' "$TMP/olcut.xml" && ! grep -q ']*>' "$TMP/olcut.xml" )" + +# ── (L) near-miss negatives: the lean default must NOT apply where the caller or a composed verb owns the map ───────── +"$BIN" "$FIX" --expand=dupTarget --top-k=0 --no-cache >"$TMP/mtk0.xml" 2>/dev/null +if grep -qE 'topk_default=|map_next=|mode="' "$TMP/mtk0.xml"; then + no "(L) explicit --top-k=0 on a multi-def name picked up lean decoration — must stay the undecorated lean form" +else + ok "(L) explicit --top-k=0 on a multi-def name stays undecorated" +fi +"$BIN" "$FIX" --expand=dupTarget --pack-top-n=1 --no-cache >"$TMP/mpack.xml" 2>/dev/null +if grep -qE 'topk_default=|map_next=' "$TMP/mpack.xml"; then + no "(L) --expand composed with --pack-top-n wrongly got the lean default (the composed verb owns the map)" +else + ok "(L) --expand composed with --pack-top-n keeps its own shape (no lean decoration)" +fi +"$BIN" "$FIX" --expand=uniqueTarget,dupTarget --pack-budget-bytes=1000000 --no-cache >"$TMP/mtok.xml" 2>/dev/null +if grep -q ']*>' "$TMP/mtok.xml" )" +else + ok "(L) a multi-token --expand with every def served is lean too" +fi +"$BIN" "$FIX" --expand=uniqueTarget --no-cache >"$TMP/u1.xml" 2>/dev/null +if grep -q 'map_next=' "$TMP/u1.xml"; then + no "(L) the single-definition exact-name answer changed shape (map_next= appeared)" +else + ok "(L) the single-definition exact-name answer is unchanged (topk_default only, no map_next=)" +fi + +# ── (M) fix round 1: map_next= replays EVERY root and the crawl-scope flags, shell-quoted (nextFlag) ───────────────── +# A multi-root call's map is the map of all its roots; the pointer used to fall back to "ripwire ." (the parent tree), and +# it dropped --ignore-tests/--exclude, so pasting it printed a map of a different corpus. Exact pointers, then pasted. +xmlUnesc(){ sed -e 's/'/'"'"'/g' -e 's/"/"/g' -e 's/<//g' -e 's/&/\&/g'; } +attrOf(){ grep -oE "$1=\"[^\"]*\"" "$2" | head -1 | sed "s/^$1=\"//;s/\"\$//"; } +MR="$TMP/mr"; mkdir -p "$MR/r1" "$MR/r2" +printf 'int multiWork(void) { return 1; }\n' >"$MR/r1/w.c" +printf 'int multiWork(void) { return 2; }\n' >"$MR/r2/w.c" +( cd "$MR" && "$BIN" r1 r2 --expand=multiWork --no-cache ) >"$TMP/mroot.xml" 2>/dev/null +mrNext="$( attrOf map_next "$TMP/mroot.xml" )" +[ "$mrNext" = "ripwire r1 r2" ] \ + && ok "(M) multi-root: map_next names BOTH roots (\"$mrNext\")" \ + || no "(M) multi-root: map_next='$mrNext', want 'ripwire r1 r2': $( grep -oE ']*>' "$TMP/mroot.xml" | head -1 )" +grep -q 'return 1' "$TMP/mroot.xml" && grep -q 'return 2' "$TMP/mroot.xml" \ + && ok "(M) multi-root: both definitions are served (premise of the lean answer)" \ + || no "(M) multi-root: the two definitions are not both served" +( cd "$MR" && "$BIN" r1 r2 --no-cache --top-k=3 ) >"$TMP/mroot_map.xml" 2>/dev/null +( cd "$MR" && eval "\"\$BIN\" ${mrNext#ripwire } --no-cache --top-k=3" ) >"$TMP/mroot_paste.xml" 2>/dev/null +grep -q '"$SC/fix/tests/test_dup.c" +( cd "$SC" && "$BIN" fix --expand=dupTarget --ignore-tests '--exclude=nomatch*' --no-cache ) >"$TMP/scope.xml" 2>/dev/null +scNext="$( attrOf map_next "$TMP/scope.xml" )" +[ "$scNext" = "ripwire fix --exclude='nomatch*' --ignore-tests" ] \ + && ok "(M) crawl scope: map_next replays --exclude (quoted) and --ignore-tests" \ + || no "(M) crawl scope: map_next='$scNext', want the root plus --exclude='nomatch*' --ignore-tests" +scShell="$( printf '%s' "$scNext" | xmlUnesc )" +( cd "$SC" && eval "\"\$BIN\" ${scShell#ripwire } --no-cache --top-k=50" ) >"$TMP/scope_paste.xml" 2>/dev/null +( cd "$SC" && "$BIN" fix --ignore-tests '--exclude=nomatch*' --no-cache --top-k=50 ) >"$TMP/scope_map.xml" 2>/dev/null +( cd "$SC" && "$BIN" fix --no-cache --top-k=50 ) >"$TMP/scope_wide.xml" 2>/dev/null +cmp -s "$TMP/scope_map.xml" "$TMP/scope_wide.xml" \ + && no "(M) crawl scope premise: --ignore-tests does not change this tree's map — the paste arm would prove nothing" \ + || ok "(M) crawl scope premise: --ignore-tests changes this tree's map" +grep -q '"$TMP/plainptr.xml" 2>/dev/null +[ "$( attrOf map_next "$TMP/plainptr.xml" )" = "ripwire test/expandtopk0fix" ] \ + && ok "(M) negative: with no crawl-scope flag map_next is just the root" \ + || no "(M) negative: plain map_next='$( attrOf map_next "$TMP/plainptr.xml" )', want 'ripwire test/expandtopk0fix'" + +# ── (N) fix round 1: unserved_next= is shell-quoted, and past 16 cut definitions it says how many it lists ────────── +# Each file under "sub dir/" holds a 62-line (~2.8 KB) `def work`; a 3500 B pack budget serves one. With 4 files the 3 cut ones are +# all listed (no unserved_listed=); with 20 files the 19 cut ones exceed the 16-selector guard: unserved_listed="16", the +# paste (default budget: 16 bodies fit) serves exactly those 16, and the same call with a larger budget serves all 20 (the +# legend's "the rest"). Served = rows in bundle mode or blocks in whole-file mode, whichever the call picks. +rootOf(){ grep -oE ']*>' "$1" | head -1; } +mkwork(){ local d="$1" n="$2" i j; mkdir -p "$d/sub dir" + for i in $( seq -w 1 "$n" ); do + { echo "def work():"; for j in $( seq 1 61 ); do echo " value_$j = 'padding to about forty-five bytes $j'"; done; } >"$d/sub dir/m$i.py" + done; } +selectorsOf(){ printf '%s' "$1" | xmlUnesc | sed -e "s/^--expand=//" -e "s/^'//" -e "s/'\$//" | tr ',' '\n' | sort; } +servedOf(){ grep -oE '"$TMP/sp4.xml" 2>/dev/null +un4="$( attrOf unserved_next "$TMP/sp4.xml" )" +grep -q 'unserved_total="3"' "$TMP/sp4.xml" && case "$un4" in "--expand='sub dir/"*) true ;; *) false ;; esac \ + && ok "(N) space path: unserved_total=\"3\" and unserved_next is single-quoted for the shell" \ + || no "(N) space path: $( grep -oE ']*>' "$TMP/sp4.xml" | head -1 )" +rootOf "$TMP/sp4.xml" | grep -q 'unserved_listed=' \ + && no "(N) negative: unserved_listed= appeared though all 3 cut definitions are listed" \ + || ok "(N) negative: no unserved_listed= when every cut definition is listed" +( cd "$SP4" && eval "\"\$BIN\" . --no-cache $( printf '%s' "$un4" | xmlUnesc )" ) >"$TMP/sp4_paste.xml" 2>/dev/null +[ "$( selectorsOf "$un4" | wc -l | tr -d ' ' )" = 3 ] && [ "$( servedAnyOf "$TMP/sp4_paste.xml" )" = "$( selectorsOf "$un4" )" ] \ + && ok "(N) space path: the pasted unserved_next serves exactly the 3 cut definitions" \ + || no "(N) space path: pasted unserved_next served [$( servedAnyOf "$TMP/sp4_paste.xml" | tr '\n' ' ' )], listed [$( selectorsOf "$un4" | tr '\n' ' ' )]" +SP20="$TMP/sp20"; mkwork "$SP20" 20 +( cd "$SP20" && "$BIN" . --expand=work --pack-budget-bytes=3500 --no-cache ) >"$TMP/sp20.xml" 2>/dev/null +tot20="$( attrOf unserved_total "$TMP/sp20.xml" )"; un20="$( attrOf unserved_next "$TMP/sp20.xml" )" +served20="$( servedOf "$TMP/sp20.xml" | wc -l | tr -d ' ' )" +[ -n "$tot20" ] && [ "$tot20" -gt 16 ] && [ "$(( tot20 + served20 ))" = 20 ] && rootOf "$TMP/sp20.xml" | grep -q 'unserved_listed="16"' \ + && ok "(N) >16 cut: unserved_total=\"$tot20\" (+$served20 served = 20) and unserved_listed=\"16\"" \ + || no "(N) >16 cut: total='$tot20' served=$served20: $( grep -oE ']*>' "$TMP/sp20.xml" | head -1 | cut -c1-300 )" +( cd "$SP20" && eval "\"\$BIN\" . --no-cache $( printf '%s' "$un20" | xmlUnesc )" ) >"$TMP/sp20_paste.xml" 2>/dev/null +[ "$( selectorsOf "$un20" | wc -l | tr -d ' ' )" = 16 ] && [ "$( servedAnyOf "$TMP/sp20_paste.xml" )" = "$( selectorsOf "$un20" )" ] \ + && rootOf "$TMP/sp20_paste.xml" | grep -q 'schema=' && ! rootOf "$TMP/sp20_paste.xml" | grep -q 'unserved_' \ + && ok "(N) >16 cut: the pasted unserved_next serves exactly the 16 it lists, and nothing is cut in that answer" \ + || no "(N) >16 cut: pasted unserved_next served $( servedAnyOf "$TMP/sp20_paste.xml" | wc -l | tr -d ' ' ): $( rootOf "$TMP/sp20_paste.xml" | cut -c1-200 )" +( cd "$SP20" && "$BIN" . --expand=work --pack-budget-bytes=1000000 --no-cache ) >"$TMP/sp20_all.xml" 2>/dev/null # (deprecated knob, still the one that cut) +[ "$( servedAnyOf "$TMP/sp20_all.xml" | wc -l | tr -d ' ' )" = 20 ] && rootOf "$TMP/sp20_all.xml" | grep -q 'schema=' && ! rootOf "$TMP/sp20_all.xml" | grep -q 'unserved_' \ + && [ -z "$( comm -23 <( servedOf "$TMP/sp20_paste.xml" ) <( servedAnyOf "$TMP/sp20_all.xml" ) )" ] \ + && ok "(N) >16 cut: the rest are recoverable — a larger --pack-budget-bytes serves all 20, no unserved_*" \ + || no "(N) >16 cut: the larger-budget call did not serve all 20: $( grep -oE ']*>' "$TMP/sp20_all.xml" | head -1 | cut -c1-200 )" + +# ── (O) fix round 1 (review N1): several --outline tokens keep the ordinary map (the lean rule is for ONE token) ───── +"$BIN" "$FIX" --outline=uniqueTarget,dupTarget --no-cache >"$TMP/ol2.xml" 2>/dev/null +grep -q ']*>' "$TMP/ol2.xml" | head -1 )" + # ── (F) well-formedness + determinism ───────────────────────────────────────────────────────────────── if command -v xmllint >/dev/null 2>&1; then - for f in uniq dup5 dup tk5 tk0 big real_default real_tk0; do + for f in uniq dup5 dup tk5 tk0 big real_default real_tk0 mdef cut ol olcut emit mroot scope sp4 sp20 sp20_paste ol2; do if xmllint --noout "$TMP/$f.xml" 2>/dev/null; then ok "(F) $f.xml well-formed"; else no "(F) $f.xml fails xmllint"; fi done else diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index 3e760a6bd..e4f04c655 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -254,7 +254,7 @@ NUMERIC_ONLY = { ( "src/partition.h", "nb" ): 1, ( "src/quality.h", "b" ): 1, ( "src/quality.h", "cidHex" ): 1, - ( "src/quality.h", "hex" ): 7, + ( "src/quality.h", "hex" ): 6, # cli-perf-068: blobShardHex and ackMapKey now share hex16 (one buffer for two) ( "src/quality.h", "name" ): 1, ( "src/recall.h", "scoreText" ): 1, ( "src/serialize.h", "...)" ): 1, @@ -617,7 +617,10 @@ if not bad: # lines NAME rw::formatTo without calling it: the format-arity family table and two comments in # src/defectshape.h, the defect-shape legend sentence (verbs_quality.h) and the --help paragraph (cli.h). # train 26b (cr-qd-kinds merged onto the train's 358/248/248/109): re-derived on the merged tree. -EXPECTED = { "mentions": 363, "calls": 248, "sites": 248, "rows": 109, "widthforms": 0 } +# 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 } # 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/fordisclosurecheck.sh b/test/fordisclosurecheck.sh index 537b01889..5893c5821 100755 --- a/test/fordisclosurecheck.sh +++ b/test/fordisclosurecheck.sh @@ -98,9 +98,35 @@ CT="$TMP/c400" grep -q 'bundle="compact" bodies="0" reason="budget"' "$CT" \ && ok "#1b compact route, ceiling exhausted: bodies=\"0\" reason=\"budget\" disclosed" \ || no "#1b compact route, ceiling exhausted: no reason=\"budget\" disclosure (the silent shape, compact copy)" -grep -q ' element leaked past a spent ceiling" \ - || ok "#1b compact route, ceiling exhausted: no element (the attribute IS the disclosure there)" +# BY-DESIGN CHANGE (lego-charge fix round 4, review rv-wrap-recipe-k29 F1, rule 5 "never a silent cut"; reported under +# PROCESS rule 4): this arm used to require NO element at all, which made the cut silent — the hop rows vanished +# with nothing naming them. What it protected still holds and is asserted: no hop ROW () and no hops section legend +# leaks past a spent ceiling. The one allowed is the counted marker: empty, shown="0", capped="1", next= the same +# question without the budget. +ct_hops="$( grep -oE ']*>' "$CT" | head -1 )" +if [ -z "$ct_hops" ]; then + no "#1b compact route, ceiling exhausted: no element — the cut hop rows are silent (rule 5)" +elif printf '%s' "$ct_hops" | grep -qE "^\$" && ! grep -q ' is the counted marker only, no hop row leaked ($ct_hops)" +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). +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=$? +cp_tot="$( grep -oE '^]*>' "$TMP/cpage" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" +{ printf '%s' "$ct_tail" | grep -qE "capped=\"1\" next=\"--for='$CONCTASK' --limit=40\">\$" && [ "$cp_rc" -eq 0 ] \ + && [ -n "$cp_tot" ] && [ -n "$ct_ttot" ] && [ "$cp_tot" -ge "$ct_ttot" ] 2>/dev/null; } \ + && ok "#1c compact route, ceiling exhausted: the cut names the file page (next=), which pages $cp_tot files >= the tail's $ct_ttot" \ + || 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)" diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index d0697b819..f4f5c8e31 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -231,7 +231,12 @@ jsonRows(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" --json 2>/dev/nul # ── arm 1: est_tokens must fit the ceiling the user asked for, in BOTH dialects ──────────────────── # (tight budget 1100, re-anchored 2026-09-11 — see the CEILING MARGIN block above for the arithmetic) -for tb in 1100 1640 3000; do +# 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" @@ -242,7 +247,7 @@ 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. -for tb in 1100 1640 3000; do +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 if [ "$xr" -le $(( jr * 13 / 10 + 1 )) ] && [ "$xr" -ge $(( jr * 7 / 10 )) ]; then @@ -365,7 +370,10 @@ fi # ── arm 4: L3 inertness — a tree with no notes is unaffected by any of this ──────────────────────── rm -f "$CORPUS/.ripwire_notes" -bare="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=800 2>/dev/null )" +# RE-ANCHORED 2026-10-10 (train 26c): 800 -> 900 for the no-notes arms below (same cause as arm 1's rung: at 800 the merged +# binary serves the 4-row floor with its unpaid next=, est_tokens 988 default / 951 full, over and labelled; from 850 both +# 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 @@ -374,20 +382,20 @@ case "$bare" in *"/dev/null )" +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 800 ]; 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 800 (full legend)"; fi +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 800 ]; then ok "no-notes tree at the default posture fits (est_tokens=$defEst)" +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 800 (est_tokens=$defEst) and SAYS so (over_ceiling=\"1\")" ;; - *) no "no-notes tree at the default posture is over 800 (est_tokens=$defEst) in SILENCE" ;; esac + 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 # ── arm 5: still deterministic and well-formed after the accounting change ───────────────────────── -if [ "$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=800 2>/dev/null )" = "$bare" ]; then ok "output is byte-identical run-to-run" +if [ "$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 2>/dev/null )" = "$bare" ]; then ok "output is byte-identical run-to-run" else no "output is not deterministic"; fi if command -v xmllint >/dev/null 2>&1; then if printf '%s' "$OUT800" | xmllint --noout - 2>/dev/null; then ok "note-bearing XML is well-formed (G4)"; else no "note-bearing XML fails xmllint"; fi diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index f260451e1..9f0faf355 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -138,7 +138,13 @@ 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 10080 10102 9880 9397 10009 10070 9966 10379 ) +REPO_BASE=( 10134 10029 10642 10102 9880 9397 10009 10070 9966 10379 ) +# 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 +# question's owner hop row with its callees. Measured on this tree: the 26c parent 9,716 B / 25 rows, the seven phase-A lanes +# 10,198 B / 25 rows, the lego-charge lane alone 10,356 B / 27 rows, the merged binary 10,642 B / 27 rows. The other nine stay +# at their bases: +0.25%, -2.64%, -7.65%, +2.00%, +1.86%, -2.25%, -1.50%, -2.88%, -6.06%. # q10 RE-PINNED 2026-09-19 (lane/r1-for-sections-stub, L2 round-1 lever B1): 9861 -> 10379 B (+5.25%), an # OUTPUT change, the q5 precedent's pattern. This query's ranked head reaches an interface with # implementors — the ONLY one of the ten reference queries that does — so it is the one query in this @@ -588,6 +594,32 @@ 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 +# 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. +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 )" +cat > "$GSHIM/git" <> "$GSHIM/calls.log" +exec "$REALGIT" "\$@" +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 )" +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" +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 @@ -595,6 +627,10 @@ fi # 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 @@ -608,7 +644,7 @@ c11check(){ # LABEL BUDGET WANT ROWS CAPPED HANDLE OVER EST no "(11) $label @$bt: shown/capped/handle/over/est = ${*:-} — want >= $want rows, the handle, est <= $bt, no over_ceiling= (paid where it already fit?)" fi } -c11check "CLI --for" 2160 17 $( c11row "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=2160 --no-cache 2>/dev/null | cut_arm )" ) +c11check "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 ) diff --git a/test/forrootlegendcheck.sh b/test/forrootlegendcheck.sh index 88ecb160e..b4b6fe245 100755 --- a/test/forrootlegendcheck.sh +++ b/test/forrootlegendcheck.sh @@ -76,24 +76,30 @@ done # rows the header then cannot pay for: on this very fixture the a4a58141 default (before those definitions) was over its # budget at 870, 930 and 1000 tokens and fit 850 by three tokens. That overshoot is labelled over_ceiling="1" and counted in # the CHANGELOG; the root= clause's own cost is what this arm is about, and the full dialect measures it. -OUT2="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=850 --no-cache --legend=full 2>/dev/null )" +# RE-ANCHORED 2026-10-10 (train 26c): 850 -> 1000. lego-charge charges / at their served size and #362's +# rides the bundle, so at 850 this fixture is the 4-row floor with its next= unpaid, labelled over_ceiling="1" +# at est_tokens=967 in the full legend (red on two macOS shards of run 38041394072 and on a 10-core host alike). Swept +# 900..1200 step 25 on the merged binary (full legend / default): 925 serves 5 rows at 892 / 880, 950..975 six rows at 939 / +# 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 850 ]; then - ok "arm2: --token-budget=850 fits the ceiling in the full legend (est_tokens=$EST2)" +if [ -n "$EST2" ] && [ "$EST2" -le 1000 ]; then + ok "arm2: --token-budget=1000 fits the ceiling in the full legend (est_tokens=$EST2)" else - no "arm2: --token-budget=850 est_tokens=${EST2:-unreadable} exceeds the ceiling — the legend clause is too expensive" + no "arm2: --token-budget=1000 est_tokens=${EST2:-unreadable} exceeds the ceiling — the legend clause is too expensive" fi -OUT2D="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=850 --no-cache 2>/dev/null )" +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 850 ] || printf '%s' "$OUT2D" | head -c 600 | grep -q ' over_ceiling="1"'; }; then - ok "arm2: the default answer at --token-budget=850 fits or says it does not (est_tokens=$EST2D)" +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)" else - no "arm2: the default answer at --token-budget=850 is over (est_tokens=${EST2D:-unreadable}) with no over_ceiling label" + no "arm2: the default answer at --token-budget=1000 is over (est_tokens=${EST2D:-unreadable}) with no over_ceiling label" fi if printf '%s' "$OUT2" | grep -qF "$CLAUSE_SNIPPET"; then - ok "arm2: the clause SURVIVES at --token-budget=850 (not dropped by the ceiling ladder)" + ok "arm2: the clause SURVIVES at --token-budget=1000 (not dropped by the ceiling ladder)" else - no "arm2: the clause is missing at --token-budget=850 — it was silently dropped instead of fitting" + no "arm2: the clause is missing at --token-budget=1000 — it was silently dropped instead of fitting" fi if printf '%s' "$OUT2" | xmllint --noout - 2>/dev/null; then ok "arm2: tight-budget output is well-formed (G4)"; else no "arm2: tight-budget output fails xmllint"; fi diff --git a/test/forsectioncollapsecheck.sh b/test/forsectioncollapsecheck.sh index 53de643b0..27eb844cd 100644 --- a/test/forsectioncollapsecheck.sh +++ b/test/forsectioncollapsecheck.sh @@ -25,7 +25,8 @@ # Fixtures: test/legofix (lego collapses: three small interfaces still clear the threshold) and # test/sectionpricefix (both lego and compose collapse: a purpose-built interface + HAS-A owners sized # comfortably above threshold) prove the ABOVE-threshold side; test/hasafix (compose stays whole: two -# short rows, priced BELOW threshold) proves the round-2 delta itself, arm (10). +# short rows, priced BELOW threshold) proves the round-2 delta itself, arm (10). test/legochargefix (a lego +# block bigger than the budget) proves the stub is what the budget is charged for, arm (13). # # Usage: bash test/forsectioncollapsecheck.sh [path-to-ripwire-binary] # RIPWIRE_BIN=build/ripwire bash test/forsectioncollapsecheck.sh @@ -393,5 +394,59 @@ else ok "(12) MCP legend disclosure arm skipped (python3 absent)" fi +# ── (13) lego-charge (K18 root cause): the budget charges a collapsing section at its STUB, not its full render. +# test/legochargefix is textual-shaped: its query names 14 interfaces with 4 implementors each, so the full render +# (~10 KB) is bigger than the default budget, while the default answer serves it as a ~150 B stub. Charging the +# full render left 1 B for : 4 rows in a ~3 KB answer under a 7.5 KB ceiling (textual's round-1 rows: 4 of 40). +# Rules (rw::forSectionsSigChargePlan, serialize.h): no explicit ceiling → the default answer's charge, also under +# --sections= (so the stub's next= restores both sections with the SAME , byte-identically); an explicit ceiling +# (--token-budget, MCP budget_tokens) → a section --sections= opts into is charged in full (hard bound). +LCQ="how do render stage classes arrange and reflow widgets" +lc_shown(){ 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=$? +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 + no "(13) legochargefix: a run failed or printed no /est_tokens= (rc=$rc_def/$rc_rest/$rc_tb/$rc_tbrest shown=$LC_DEF/$LC_REST/$LC_TB/$LC_TBREST est=$LC_TBEST)" +else + # (13a) RED on the base: the stub is served, so the budget pays the stub — the answer carries a budget's worth of rows + grep -Eq '' "$TMP/lc_def.xml" && [ "$LC_DEF" -ge 24 ] \ + && ok "(13a) default: is served as a stub and shows $LC_DEF rows (the stub's price was charged, not the full render's)" \ + || no "(13a) default: (want >= 24 with a stub) — the budget paid for a lego render it never served" + # (13b) the stub's next= restores both sections byte-identically: same block, the full on top + [ "$( lc_sigs "$TMP/lc_def.xml" )" = "$( lc_sigs "$TMP/lc_rest.xml" )" ] && grep -q ' as the stubbed answer ($LC_REST rows) plus the full " \ + || no "(13b) --sections=lego,compose: differs from the stubbed answer's ($LC_DEF) or no full — next= does not restore the cut it disclosed" + LC_STUBTOTAL="$( grep -o ' ($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" +fi +if command -v python3 >/dev/null 2>&1; then + 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)" +else + printf ' SKIP %s\n' "(13e) python3 absent: the MCP twin of the lego charge is not checked" +fi + [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" exit "$fail" diff --git a/test/forwidencheck.sh b/test/forwidencheck.sh index 4bee48adf..eb6e1d5b9 100755 --- a/test/forwidencheck.sh +++ b/test/forwidencheck.sh @@ -276,7 +276,7 @@ run --for="$THIN" --limit=0 >/dev/null; rc=$? if [ "$rc" != 0 ] && grep -q -- '--limit' "$TMP/err"; then ok "(6) --limit=0 is refused (rc=$rc)"; else no "(6) --limit=0 not refused: rc=$rc $( head -c 160 "$TMP/err" )"; fi run --for="$THIN" --limit=abc >/dev/null; rc=$? if [ "$rc" != 0 ] && grep -q -- '--limit' "$TMP/err"; then ok "(6) --limit=abc is refused (rc=$rc)"; else no "(6) --limit=abc not refused: rc=$rc $( head -c 160 "$TMP/err" )"; fi -for flag in --json --format=candidates --detail=1 --signatures-only --token-budget=2000; do +for flag in --json --format=candidates --detail=1 --signatures-only; do run --for="$THIN" --limit=5 $flag >"$TMP/shape.out"; rc=$? if [ "$rc" != 0 ] && [ ! -s "$TMP/shape.out" ]; then ok "(6) the page refuses $flag rather than ignoring it (rc=$rc)" @@ -284,6 +284,17 @@ for flag in --json --format=candidates --detail=1 --signatures-only --token-budg no "(6) the page accepted $flag: rc=$rc, $( wc -c <"$TMP/shape.out" | tr -d ' ' ) bytes on stdout, stderr: $( head -c 160 "$TMP/err" )" fi done +# PAGING-POC (issue #294): --token-budget beside a window no longer refuses — the combination now has a +# DEFINED meaning, the budgeted-bundle CANDIDATE PAGE (a different document, verbs_for.h's shared +# emitForCandidatePage). The old refusal guarded against budget-silently-ignored beside this file page; +# the new arm pins the STRONGER contract: budget+window answers with the -rooted candidate page +# carrying the pageview quintet, so THIS file page stays budgetless by construction, never by refusal. +run --for="$THIN" --limit=5 --token-budget=2000 >"$TMP/shape.out"; rc=$? +if [ "$rc" = 0 ] && grep -q '^/dev/null; } +ref_run(){ spec_env "$BIN" "$SPEC" --top-k=400 --no-cache 2>/dev/null; } +# same_as_ref LABEL SYMBOL want(present|absent): one cached run against one --no-cache run, then the symbol's state +same_as_ref(){ + spec_run >"$TMP/spec.out"; ref_run >"$TMP/ref.out" + if ! cmp -s "$TMP/spec.out" "$TMP/ref.out"; then no "14$1: the cached (speculated) run differs from --no-cache"; return; fi + if grep -q "$2" "$TMP/spec.out"; then found=present; else found=absent; fi + if [ "$found" = "$3" ]; then ok "14$1: cached run == --no-cache, and $2 is $3" + else no "14$1: $2 should be $3, the cached run has it $found"; fi +} +spec_run >/dev/null +if [ -n "$( find "$SC" -name 'ripwire-ignoreprobe-*.bin' 2>/dev/null | head -1 )" ]; then ok "14a: a cached run on a git root keeps the ignore answer for the next call's speculation" +else no "14a: no ignore-probe sidecar under the cache dir — the probe is not speculated"; fi +same_as_ref b rwGateGeneratedSymbol absent # warm: the speculation matches git +printf 'gen_out/\ntracked_anyway.cpp\n' >"$SPEC/.gitignore" # c: an EDITED .gitignore un-ignores generated.cpp +same_as_ref c rwGateGeneratedSymbol present +printf 'int rwGateNestedSymbol( int a ) { return a + 7; }\n' >"$SPEC/keep/extra.cpp"; spec_run >/dev/null +printf 'extra.cpp\n' >"$SPEC/keep/.gitignore" # d: a NEW nested .gitignore +same_as_ref d rwGateNestedSymbol absent +printf 'int rwGateInfoExcludeSymbol( int a ) { return a + 8; }\n' >"$SPEC/info_ex.cpp"; spec_run >/dev/null +mkdir -p "$SPEC/.git/info"; printf 'info_ex.cpp\n' >>"$SPEC/.git/info/exclude" # e: .git/info/exclude +same_as_ref e rwGateInfoExcludeSymbol absent +printf 'int rwGateGlobalSymbol( int a ) { return a + 9; }\n' >"$SPEC/glob_ex.cpp"; spec_run >/dev/null +printf 'glob_ex.cpp\n' >"$TMP/xdg/git/ignore" # f1: the DEFAULT global excludes file +same_as_ref f1 rwGateGlobalSymbol absent +printf 'int rwGateExcludesFileSymbol( int a ) { return a + 10; }\n' >"$SPEC/cfg_ex.cpp"; : >"$TMP/excludes" +git -C "$SPEC" config core.excludesFile "$TMP/excludes"; spec_run >/dev/null +printf 'cfg_ex.cpp\n' >"$TMP/excludes" # f2: core.excludesFile's file, edited +same_as_ref f2 rwGateExcludesFileSymbol absent +printf 'scratch_*.cpp\n' >>"$SPEC/.gitignore"; spec_run >/dev/null # g: no rule FILE changes, only the tree: +printf 'int rwGateScratchSymbol( int a ) { return a + 11; }\n' >"$SPEC/scratch_new.cpp" # a new file an old rule ignores +same_as_ref g rwGateScratchSymbol absent +printf 'gen_out/\n' >"$SPEC/.gitignore" # i: a LOST speculation whose walk the memory +spec_env env RIPWIRE_TEST_MEMGUARD=crawl:6 TMPDIR="$SC/" "$BIN" "$SPEC" --top-k=400 >"$TMP/spec.i" 2>"$TMP/spec.i.err"; rcSpecI=$? # guard stops: +spec_env env RIPWIRE_TEST_MEMGUARD=crawl:6 "$BIN" "$SPEC" --top-k=400 --no-cache >"$TMP/ref.i" 2>/dev/null; rcRefI=$? # the redo is re-armed +if ! grep -q 'stopped the crawl' "$TMP/spec.i.err"; then no "14i: the crawl:6 seam did not stop the walk — the arm tests nothing" +elif [ "$rcSpecI" = "$rcRefI" ] && cmp -s "$TMP/spec.i" "$TMP/ref.i"; then ok "14i: a lost speculation under a crawl memory stop answers as --no-cache does (rc $rcSpecI)" +else no "14i: a lost speculation under a crawl memory stop differs from --no-cache (rc $rcSpecI vs $rcRefI)"; fi +spec_run >"$TMP/spec.a"; spec_run >"$TMP/spec.b" +if cmp -s "$TMP/spec.a" "$TMP/spec.b"; then ok "14h: two cached runs in a row are byte-identical"; else no "14h: two cached runs in a row differ"; fi + # ── 13. the flag is in --help (the deckcheck allowlist row for --no-ignore retires with it). "$BIN" --help=all 2>&1 | grep -q -- '--no-ignore' && ok "--no-ignore is documented in --help" \ || no "--no-ignore is missing from --help" diff --git a/test/graphlegendbudgetcheck.sh b/test/graphlegendbudgetcheck.sh index 00b147303..88a62cf1c 100755 --- a/test/graphlegendbudgetcheck.sh +++ b/test/graphlegendbudgetcheck.sh @@ -118,8 +118,19 @@ budgetFor(){ # when reaches>0. Measured on this probe: 3926 B on main (3fcd515f), 4288 B on this lane (+362 B, exactly the # clause); 4336 keeps impact's 48 B headroom. The gate's RED-on-1dc7b01 property rests on the uses pin (4114 < # 4303), which does not move. - callers) echo 3429 ;; - impact) echo 4336 ;; + # RE-PINNED callers +340 (2026-10-09, lane callers-sites-first-call, CALLSITE-AT): every caller row now carries + # sites_at=, its call sites as file:line, and the full legend defines it (graphlegend.h kCallSitesAtLegend, present + # only when a row carries it). Measured on this probe: 3403 B on main (0852bc0f), 3743 B on this lane (+340 B, + # exactly the clause); 3769 keeps callers' 26 B headroom. The OLD 3429 stays as a twin below: the legend with that + # one sentence cut out must still fit it, so the rest of the essay cannot re-inflate under the new headroom. + # RE-PINNED impact +197 (2026-10-10, lane lean-trio, idea #6): a CUT --impact symbol window now opens with the + # per-file rollup of the WHOLE reach set ( + rows), and the + # full legend gains graphlegend.h impactRollupLegend's FILES sentence, present exactly when that element rides + # (this probe's window is cut). Shortened from a 330 B draft to 197 B before the pin moved. Measured on this probe: + # 4288 B on main (0852bc0f), 4485 B on this lane (+197 B, exactly the sentence); 4533 keeps impact's 48 B headroom. + # The gate's RED-on-1dc7b01 property rests on the uses pin (4114 < 4303), which does not move. + callers) echo 3769 ;; + impact) echo 4533 ;; uses) echo 4114 ;; esac } @@ -142,6 +153,31 @@ for v in $VERBS; do fi done +# (a-twin) the pre-CALLSITE-AT callers pin: the --callers full legend minus the sites_at= sentence still fits 3429 B. The +# cut is taken only when a row carries sites_at= AND the sentence occurs once, start to end; any other premise FAILs. +twin="$( python3 - "$TMP/callers.xml" <<'PY' +import re, sys +doc = open( sys.argv[1], 'rb' ).read().decode( 'utf-8', 'replace' ) +m = re.match( r'\A(?:\s*)+', doc, re.S ) +leg = m.group( 0 ) if m else '' +if not leg or 'sites_at="' not in doc[ len( leg ): ]: + print( 'NOPREMISE no row carries sites_at=' ); sys.exit( 0 ) +a, z = 'sites_at= on a caller row', '(an aliased import). ' +i = leg.find( a ); j = leg.find( z, i + 1 ) if i >= 0 else -1 +if i < 0 or j < 0 or leg.count( a ) != 1: + print( 'NOPREMISE sentence-not-found-once' ); sys.exit( 0 ) +print( len( ( leg[ :i ] + leg[ j + len( z ): ] ).encode() ) ) +PY +)" +case "$twin" in + NOPREMISE*) no "(a-twin) --callers legend twin cannot measure: ${twin#NOPREMISE }" ;; + *) if [ -n "$twin" ] && [ "$twin" -eq "$twin" ] 2>/dev/null && [ "$twin" -le 3429 ]; then + ok "(a-twin) --callers legend minus the sites_at= sentence is $twin B <= 3429 B (the pre-CALLSITE-AT pin)" + else + no "(a-twin) --callers legend minus the sites_at= sentence is ${twin:-?} B > 3429 B — the rest re-inflated" + fi ;; +esac + # ── (b) the honesty vocabulary a reader must meet on all three, unchanged by the trim (test/floormarkcheck.sh # already gates the exact cross-verb anchors; this arm is the lane's own quick check that the shared # constants still carry the attribute-defining words a reader relies on). ───────────────────────── diff --git a/test/greptiercheck.sh b/test/greptiercheck.sh index e864b5cc0..389923d4a 100644 --- a/test/greptiercheck.sh +++ b/test/greptiercheck.sh @@ -82,6 +82,19 @@ int TIERTOKEN_frob( int x ) return x + 1; } /* TIERTOKEN_frob again, block comment */ +/* lane lean-trio (idea #5): seven more CODE uses, so this token's code tier is FULL (>= 8 hits) and arm (1) keeps its + premise — an identifier with a full code tier holds its comments AND strings back. A thin code tier lifts the string + tier now; arm (12h) is that path. */ +int tier_uses( void ) +{ + return TIERTOKEN_frob( 1 ) + + TIERTOKEN_frob( 2 ) + + TIERTOKEN_frob( 3 ) + + TIERTOKEN_frob( 4 ) + + TIERTOKEN_frob( 5 ) + + TIERTOKEN_frob( 6 ) + + TIERTOKEN_frob( 7 ); +} int TIERTOKEN_onlycomment_caller( int y ) { // TIERTOKEN_onlycomment appears in comments and NOWHERE else @@ -100,10 +113,10 @@ D_OUT="$( "$BIN" "$SB" --no-cache --grep=TIERTOKEN_frob 2>/dev/null )" d_hits="$( attr hits "$D_OUT" )" d_comment="$( attr suppressed_comment "$D_OUT" )" d_string="$( attr suppressed_string "$D_OUT" )" -if [ "$d_hits" = "2" ] && [ "$d_comment" = "2" ] && [ "$d_string" = "1" ]; then - ok "(1) code hit + the doc-file mention kept, 2 comment + 1 string suppressed and disclosed" +if [ "$d_hits" = "9" ] && [ "$d_comment" = "2" ] && [ "$d_string" = "1" ]; then + ok "(1) the 8 code hits + the doc-file mention kept, 2 comment + 1 string suppressed and disclosed" else - no "(1) expected hits=2 suppressed_comment=2 suppressed_string=1, got hits=$d_hits comment=$d_comment string=$d_string" + no "(1) expected hits=9 suppressed_comment=2 suppressed_string=1, got hits=$d_hits comment=$d_comment string=$d_string" printf '%s\n' "$D_OUT" | grep -o ']*>' fi # the surviving row must be the DEFINITION line, not one of the prose mentions @@ -116,7 +129,7 @@ echo "=== (2) --grep-in=any: every tier, and NO tier attributes at all ===" # ═══════════════════════════════════════════════════════════════════════════ A_OUT="$( "$BIN" "$SB" --no-cache --grep=TIERTOKEN_frob --grep-in=any 2>/dev/null )" a_hits="$( attr hits "$A_OUT" )" -if [ "$a_hits" = "5" ]; then ok "(2) --grep-in=any keeps all 5 hits"; else no "(2) --grep-in=any expected hits=5, got $a_hits"; fi +if [ "$a_hits" = "12" ]; then ok "(2) --grep-in=any keeps all 12 hits (8 code + 2 comment + 1 string + 1 doc)"; else no "(2) --grep-in=any expected hits=12, got $a_hits"; fi if printf '%s' "$A_OUT" | grep -qE 'suppressed_comment=|suppressed_string=|tier_budget=|tier_unclassified=|tier="'; then no "(2b) --grep-in=any leaked a tier attribute onto an untiered answer" else @@ -390,6 +403,25 @@ fi [ "$( jkey total "$V_OK" )" = "$d_hits" ] \ && ok "(9d-b) an explicit in=\"code\" still answers (the refusal is on unknown values, not on presence)" \ || no "(9d-b) in=\"code\" was refused or changed the answer — the value check is too broad" +# ── (9e) a NON-STRING in= refuses through the shared shape gate and echoes the value SENT (CodeRabbit 5468003465's +# sibling: `in` used to be read inside the grep arm, after that gate, so in:5 decoded to "" and the verb answered the +# default tier as if nothing had been sent). Near-miss twins: (9d) a string outside the set still refuses by name. +for arm in '5|5' '["any"]|["any"]' '{"v":1}|{"v":1}' 'true|true'; do + val="${arm%%|*}"; echo_="${arm#*|}" + V_NS="$( mcpcall '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"grep","arguments":{"path":"'"$SB"'","pattern":"TIERTOKEN_frob","in":'"$val"'}}}' )" + # the message is read out of the JSON (a quote inside the echoed value is \" on the wire), as crossrefcheck (L10b) does + V_MSG="$( printf '%s' "$V_NS" | python3 -c 'import sys,json +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line) + if "error" in d: print("ERROR", d["error"].get("code"), d["error"].get("message",""), end="") + elif d.get("result",{}).get("content"): print("ANSWER", end="")' 2>/dev/null )" + case "$V_MSG" in + "ERROR -32602 invalid value for field: in"*"got '$echo_'"*) ok "(9e) the live MCP grep verb refuses a non-string in=$val through the shape gate and echoes '$echo_'" ;; + *) no "(9e) the live MCP grep verb did not refuse in=$val echoing '$echo_' — got: ${V_MSG:-nothing}" ;; + esac +done # ═══════════════════════════════════════════════════════════════════════════ echo "=== (11) M17 — a class label decided under a budget says so ===" @@ -536,9 +568,17 @@ printf '%s' "$LT_OUT" | grep -o '' | head -1 | grep -q 'code+string' \ && ok "(12d) the legend defines the code+string label in the answer that emits it" \ || no "(12d) tier=code+string was emitted and the legend never says what it means" # (12e) NOT a blanket lift: the same literal as an IDENTIFIER in a C source file keeps the code tier alone. +# lane lean-trio: seven more code uses keep this identifier's code tier FULL (>= 8 hits) — the premise this arm tests; a +# THIN code tier now lifts the string tier (arm 12h). cat >"$LT/src/reader.c" <<'EOF' int LITTOKEN_reader( void ) { return 0; } const char *reader_name = "LITTOKEN_reader"; +int reader_uses( void ) +{ + return LITTOKEN_reader() + LITTOKEN_reader() + LITTOKEN_reader() + + LITTOKEN_reader() + LITTOKEN_reader() + LITTOKEN_reader() + + LITTOKEN_reader(); +} EOF cat >"$LT/tests/reader.sh" <<'EOF' LITTOKEN_reader @@ -565,6 +605,270 @@ else printf '%s\n' "$LN_OUT" | grep -o ']*>' fi +# (12h) lane lean-trio (idea #5): a THIN code tier (fewer than 8 code hits) serves the source string WITH code — an +# identifier asked for once or twice is as likely to be the text a table holds. string_hits= keeps the per-tier count. +# RED on main 0852bc0f: no tier=, suppressed_string="1". +LH="$TMP/thinsandbox" +mkdir -p "$LH/src" +cat >"$LH/src/thin.c" <<'EOF' +int THINTOKEN_name( void ) { return 0; } +const char *thin_label = "THINTOKEN_name"; +EOF +LH_OUT="$( "$BIN" "$LH" --no-cache --grep=THINTOKEN_name 2>/dev/null )" +if [ "$( attr tier "$LH_OUT" )" = "code+string" ] && [ "$( attr string_hits "$LH_OUT" )" = "1" ] && [ -z "$( attr suppressed_string "$LH_OUT" )" ] \ + && printf '%s' "$LH_OUT" | grep -q '"$LS/src/cmd-shape-split.c" <<'EOF' +struct entry { const char *name; }; +const struct entry cmd_split_entry = { + .name = "shape-split", +}; +EOF +# a-keys.c sorts BEFORE it by path: the rank, not the path order, must put the file named like the literal first +cat >"$LS/src/a-keys.c" <<'EOF' +const char *bindings[] = { "bind x shape-split", 0 }; +EOF +cat >"$LS/src/cmd.c" <<'EOF' +extern const struct entry cmd_split_entry; +const struct entry *cmd_table[] = { + &cmd_split_entry, + 0 +}; +EOF +for i in 1 2 3 4 5 6 7 8 9; do printf 'tmux shape-split -t %s\n' "$i" >"$LS/regress/a$i.sh"; done +LS_OUT="$( "$BIN" "$LS" --no-cache --grep=shape-split 2>/dev/null )" +ls_first="$( printf '%s' "$LS_OUT" | grep -o ']*>' +fi +printf '%s' "$LS_OUT" | grep -q 'in="cmd_split_entry" ref="src/cmd.c:3"' \ + && ok "(12j) the callerless table entry carries ref= at its registration (src/cmd.c:3), not the extern line" \ + || no "(12j) no ref=src/cmd.c:3 on the cmd_split_entry hit: $( printf '%s' "$LS_OUT" | grep -o ']*>' | head -2 | tr '\n' ' ' )" +printf '%s' "$LS_OUT" | grep -o '' | head -1 | grep -q 'ref=' \ + && ok "(12k) the legend defines ref= and string_hits= where they are emitted" \ + || no "(12k) ref= emitted with no reading in the legend" +if command -v xmllint >/dev/null 2>&1; then + if printf '%s' "$LS_OUT" | xmllint --noout - 2>/dev/null; then + ok "(12l) the ranked answer is well-formed XML" + else + no "(12l) the ranked answer is not well-formed XML" + fi +fi + +# (12m) fix round 1 (rv-lean-trio item 2): the literal's SHAPE is a text heuristic, so it never outvotes a FULL source code +# tier — the lift fires only while source code holds fewer than 8 of its code hits. A dotted member pattern (Python +# docstrings are string tier, so nearly every one has a source string), a TS member call and a SCREAMING constant, each with +# 12 code uses in source and one string mention, keep main's code-tier answer: no tier=, the string held back, all 12 code +# rows. RED on 33873861: tier="code+string" and a 10-row window (the textual self.app / hono c.json / DEFAULT_CSS repro). +LF="$TMP/fullsandbox" +mkdir -p "$LF/src" +{ + printf 'class Screen:\n """Talks to self.app and DEFAULT_CSS for its styles."""\n' + for i in 1 2 3 4 5 6 7 8 9 10 11 12; do printf ' def m%s(self):\n return self.app.tick(%s)\n' "$i" "$i"; done + printf 'DEFAULT_CSS = "x"\n' + for i in 1 2 3 4 5 6 7 8 9 10 11; do printf 'def css%s():\n return DEFAULT_CSS\n' "$i"; done +} >"$LF/src/screen.py" +{ + printf 'const doc = "send it with c.json";\n' + for i in 1 2 3 4 5 6 7 8 9 10 11 12; do printf 'export function h%s(c: any) { return c.json({ n: %s }); }\n' "$i" "$i"; done +} >"$LF/src/routes.ts" +for lit in self.app DEFAULT_CSS c.json; do + LF_OUT="$( "$BIN" "$LF" --no-cache --grep="$lit" 2>/dev/null )" + if [ -z "$( attr tier "$LF_OUT" )" ] && [ "$( attr hits "$LF_OUT" )" = "12" ] && [ "$( attr suppressed_string "$LF_OUT" )" = "1" ] \ + && [ -z "$( attr string_hits "$LF_OUT" )" ] && [ "$( attr shown "$LF_OUT" )" = "12" ]; then + ok "(12m) --grep=$lit: 12 code hits in source keep the code tier (no lift, all 12 rows, the string held back)" + else + no "(12m) --grep=$lit lifted beside a full source code tier: tier=$( attr tier "$LF_OUT" ) hits=$( attr hits "$LF_OUT" ) shown=$( attr shown "$LF_OUT" ) suppressed_string=$( attr suppressed_string "$LF_OUT" )" + fi +done +# (12m) the code vote counts TEST files too: a call there is parsed code, a use a "list every use" question needs (hono +# c.json: ~490 code hits, nearly all in *.test.ts). Two code uses and a string in source plus 12 calls in a test file keep +# the code answer; the same 12 lines in a shell script are a command typed, not code, and the literal lifts. +LX="$TMP/testsandbox" +mkdir -p "$LX/src" "$LX/tests" +printf 'const note = "reply with r.send"; +export function a(r: any) { return r.send(1); } +export function b(r: any) { return r.send(2); } +' >"$LX/src/app.ts" +for i in 1 2 3 4 5 6 7 8 9 10 11 12; do printf 'test("t%s", () => { r.send(%s); }); +' "$i" "$i"; done >"$LX/tests/app.test.ts" +LX_OUT="$( "$BIN" "$LX" --no-cache --grep=r.send 2>/dev/null )" +[ -z "$( attr tier "$LX_OUT" )" ] && [ "$( attr shown "$LX_OUT" )" = "14" ] && [ "$( attr suppressed_string "$LX_OUT" )" = "1" ] \ + && ok "(12m) 2 source + 12 test-file code uses: no lift, all 14 code rows" \ + || no "(12m) test-file code uses did not hold the lift back: tier=$( attr tier "$LX_OUT" ) shown=$( attr shown "$LX_OUT" )" +rm "$LX/tests/app.test.ts"; mkdir -p "$LX/scripts" +for i in 1 2 3 4 5 6 7 8 9 10 11 12; do printf 'tool r.send %s\n' "$i" >"$LX/scripts/s$i.sh"; done +LX_OUT="$( "$BIN" "$LX" --no-cache --grep=r.send 2>/dev/null )" +[ "$( attr tier "$LX_OUT" )" = "code+string" ] \ + && ok "(12m) 2 source code uses + 12 script lines: the literal lifts (script lines do not vote)" \ + || no "(12m) script lines voted against the lift: tier=$( attr tier "$LX_OUT" )" +# (12n) the boundary, both ways: the count is CODE hits in SOURCE code — lines in shell scripts that type the literal are +# bare-word code and do not count (tmux split-window: 2 code hits in .c, 85 in .sh, and it lifts; K1). 7 in source plus 9 +# script lines lifts; 8 in source does not. +for n in 7 8; do + LB="$TMP/boundary$n" + mkdir -p "$LB/src" "$LB/regress" + { + printf 'class Pane:\n """Reads self.pane on demand."""\n' + i=0; while [ "$i" -lt "$n" ]; do i=$(( i + 1 )); printf ' def p%s(self):\n return self.pane\n' "$i"; done + } >"$LB/src/pane.py" + for i in 1 2 3 4 5 6 7 8 9; do printf 'tool self.pane %s\n' "$i" >"$LB/regress/r$i.sh"; done + LB_OUT="$( "$BIN" "$LB" --no-cache --grep=self.pane 2>/dev/null )" + eval "LB_TIER_$n=\"\$( attr tier \"\$LB_OUT\" )\"" +done +[ "$LB_TIER_7" = "code+string" ] && ok "(12n) 7 source code hits (+9 script lines): the dotted literal still lifts" \ + || no "(12n) 7 source code hits should lift: tier=$LB_TIER_7" +[ -z "$LB_TIER_8" ] && ok "(12n) 8 source code hits: no lift (the floor is kGrepLiftCodeFloor)" \ + || no "(12n) 8 source code hits must not lift: tier=$LB_TIER_8" +# (12o) fix round 1 (rv-lean-trio item 3): ref= names a CODE site only. A comment and a string that name the callerless +# entry inside other definitions, in files that sort BEFORE the registration, are prose, not registrations — in C and in +# Python. RED on 33873861: ref= pointed at the comment (src/alpha.c:3 / src/aaa.py:2). +LR="$TMP/refsandbox" +mkdir -p "$LR/src" +cat >"$LR/src/do-thing.c" <<'EOF' +struct entry { const char *name; int (*exec)(int); }; +static int do_thing_exec(int x) { return x; } +const struct entry do_thing_entry = { .name = "do-thing", .exec = do_thing_exec }; +EOF +cat >"$LR/src/alpha.c" <<'EOF' +struct entry; +void helper(void) { + /* unlike do_thing_entry, this one is not registered */ + int k = 0; +} +EOF +cat >"$LR/src/alpha2.c" <<'EOF' +const char *describe(void) { + return "do_thing_entry"; +} +EOF +cat >"$LR/src/zeta.c" <<'EOF' +struct entry; +extern const struct entry do_thing_entry; +const struct entry *table[] = { + &do_thing_entry, + 0 +}; +EOF +printf '#!/bin/sh\ntool do-thing a\ntool do-thing b\n' >"$LR/run.sh" # script lines that TYPE it: the code tier beside the string (fxg) +cat >"$LR/src/jobrun.py" <<'EOF' +def jobrun_handler(req): + return "jobrun" +EOF +cat >"$LR/src/aaa.py" <<'EOF' +def unrelated(): + # jobrun_handler is documented elsewhere + return 1 +EOF +cat >"$LR/src/abc.py" <<'EOF' +def describe_job(): + return "jobrun_handler" +EOF +cat >"$LR/src/routes.py" <<'EOF' +from jobrun import jobrun_handler + +def register(table): + table["job"] = jobrun_handler +EOF +LR_C="$( "$BIN" "$LR" --no-cache --grep=do-thing 2>/dev/null )" +printf '%s' "$LR_C" | grep -q 'in="do_thing_entry" ref="src/zeta.c:4"' \ + && ok "(12o) C: ref= is the registration (src/zeta.c:4), never the comment (alpha.c:3) or the string (alpha2.c:2)" \ + || no "(12o) C: ref= is not src/zeta.c:4: $( printf '%s' "$LR_C" | grep -o ']*in="do_thing_entry"[^>]*>' )" +LR_P="$( "$BIN" "$LR" --no-cache --grep=jobrun 2>/dev/null )" +printf '%s' "$LR_P" | grep -q 'in="jobrun_handler" ref="src/routes.py:4"' \ + && ok "(12o) Python: ref= is the registration (src/routes.py:4), never the comment (aaa.py:2) or the string (abc.py:2)" \ + || no "(12o) Python: ref= is not src/routes.py:4: $( printf '%s' "$LR_P" | grep -o ']*in="jobrun_handler"[^>]*>' )" + +# (12o) fix round 2 (rv-lean-trio2 nit c): the occurrence PAIRING — the k-th raw hit on a line is the k-th whole-word +# occurrence on it. Same-line shapes, both ways: a comment then the code reference on ONE line is a registration +# (ref=src/zeta.c:4); a near-namesake in code, then a comment and a string on one line, is not (no ref=). Dropping the +# pairing ("some hit on the line is code, some occurrence is whole-word") claims src/zeta.c:4 on the second; reading only +# the line's first occurrence misses the first. RED on 33873861: the second claimed src/a.c:3 (a string). +for shape in comment-then-code namesake-comment-string; do + LQ="$TMP/refline-$shape" + mkdir -p "$LQ/src" + cp "$LR/src/do-thing.c" "$LQ/src/do-thing.c" + printf '#!/bin/sh\ntool do-thing a\ntool do-thing b\n' >"$LQ/run.sh" + printf 'struct entry;\nvoid say(void) {\n const char *s = "do_thing_entry";\n (void)s;\n}\n' >"$LQ/src/a.c" + if [ "$shape" = comment-then-code ]; then reg=' /* do_thing_entry */ &do_thing_entry,'; else reg=' &do_thing_entry_old, /* do_thing_entry */ "do_thing_entry",'; fi + printf 'struct entry;\nextern const struct entry do_thing_entry;\nconst struct entry *table[] = {\n%s\n 0 };\n' "$reg" >"$LQ/src/zeta.c" + LQ_OUT="$( "$BIN" "$LQ" --no-cache --grep=do-thing 2>/dev/null; printf "\nrc=%s\n" "$?" )" + LQ_HIT="$( printf '%s' "$LQ_OUT" | grep -o ']*in="do_thing_entry"[^>]*>' )" + if ! printf '%s' "$LQ_OUT" | grep -q '^rc=0$' || [ "$( attr tier "$LQ_OUT" )" != "code+string" ] || [ -z "$LQ_HIT" ]; then + no "(12o) $shape: premise — the ranked answer with the do_thing_entry hit: tier=$( attr tier "$LQ_OUT" ) hit=$LQ_HIT" + elif [ "$shape" = comment-then-code ]; then + printf '%s' "$LQ_HIT" | grep -q 'ref="src/zeta.c:4"' \ + && ok "(12o) one line, a comment then the code reference: ref=src/zeta.c:4 (the second occurrence pairs with its code hit)" \ + || no "(12o) one line, a comment then the code reference: ref= is not src/zeta.c:4: $LQ_HIT" + else + printf '%s' "$LQ_HIT" | grep -q 'ref=' \ + && no "(12o) one line, a code near-namesake then a comment and a string: a ref= was claimed: $LQ_HIT" \ + || ok "(12o) one line, a code near-namesake then a comment and a string: no ref= (neither occurrence is code)" + fi +done + +# (12p) fix round 2 (rv-lean-trio2 item 1b, checklist 13b): a STOPPED classification cannot prove a code tier thin. 140 +# files hold the dotted literal in a docstring, 3 early files call it, and 30 more callers sort past the 128-file tier +# budget (unclassified). The ONE program-code count includes unclassified hits, so the shape lift does not fire: main's +# code-tier answer, every row (all 30 late callers), no window. RED on 9bd8528a: tier=code+string, shown=10 of 173, 0 of +# the 30 late rows. +LP="$TMP/budgetsandbox" +mkdir -p "$LP/pkg" +printf 'class O:\n def frob(self, x=0):\n return x\n\nobj = O()\n' >"$LP/pkg/core.py" +for i in 1 2 3; do printf 'from pkg.core import obj\n\ndef early_%s():\n return obj.frob(%s)\n' "$i" "$i" >"$LP/pkg/a_000$i.py"; done +i=0; while [ "$i" -lt 140 ]; do i=$(( i + 1 )); n="$( printf '%03d' "$i" )" + printf 'def doc_%s():\n """Call obj.frob to frob."""\n return 1\n' "$n" >"$LP/pkg/a_$n.py"; done +i=0; while [ "$i" -lt 30 ]; do i=$(( i + 1 )); n="$( printf '%02d' "$i" )" + printf 'from pkg.core import obj\n\ndef use_%s():\n return obj.frob(%s)\n' "$n" "$i" >"$LP/pkg/z_$n.py"; done +LP_OUT="$( "$BIN" "$LP" --no-cache --grep=obj.frob 2>/dev/null; printf "\nrc=%s\n" "$?" )" +lp_late="$( printf '%s' "$LP_OUT" | grep -o '"$LQ/src/app.py" + i=0; while [ "$i" -lt "$n" ]; do i=$(( i + 1 )); printf 'def test_q%s(app):\n assert app.query("#w%s")\n' "$i" "$i" >"$LQ/tests/test_q$i.py"; done + if [ "$n" = 7 ]; then mkdir -p "$LQ/scripts"; i=0; while [ "$i" -lt 7 ]; do i=$(( i + 1 )); printf 'tool app.query %s\n' "$i" >"$LQ/scripts/s$i.sh"; done; fi + LQ_OUT="$( "$BIN" "$LQ" --no-cache --grep=app.query 2>/dev/null; printf "\nrc=%s\n" "$?" )" + if ! printf '%s' "$LQ_OUT" | grep -q '^rc=0$' || [ "$( attr tier "$LQ_OUT" )" != "code+string" ]; then + no "(12q) $n test calls: premise — main's literal lift should serve code+string: tier=$( attr tier "$LQ_OUT" )" + elif [ "$n" = 12 ]; then + [ -z "$( attr string_hits "$LQ_OUT" )" ] && [ "$( attr hits "$LQ_OUT" )" = 13 ] && [ "$( attr shown "$LQ_OUT" )" = 13 ] \ + && ok "(12q) 12 test-file code uses under the literal lift: main's answer, all 13 rows, no window, no string_hits=" \ + || no "(12q) 12 test-file code uses: the window cut code rows: shown=$( attr shown "$LQ_OUT" ) hits=$( attr hits "$LQ_OUT" ) string_hits=$( attr string_hits "$LQ_OUT" )" + else + [ "$( attr string_hits "$LQ_OUT" )" = 1 ] && [ "$( attr hits "$LQ_OUT" )" = 15 ] && [ "$( attr shown "$LQ_OUT" )" = 10 ] \ + && ok "(12q) 7 test calls + 7 script lines (a thin count): the literal lift is ranked and windowed (10 of 15, string_hits=1)" \ + || no "(12q) a thin count under the literal lift should rank and window: shown=$( attr shown "$LQ_OUT" ) hits=$( attr hits "$LQ_OUT" ) string_hits=$( attr string_hits "$LQ_OUT" )" + fi +done + # (12f) the MCP grep twin lifts the same rows (one collection, one decision). LT_MCP="$( printf '%s\n%s\n%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \ diff --git a/test/hazardpatterncheck.sh b/test/hazardpatterncheck.sh index 7714a19ba..a714765e3 100755 --- a/test/hazardpatterncheck.sh +++ b/test/hazardpatterncheck.sh @@ -487,6 +487,7 @@ REGA cat > "$TMP/reg_b1.tsv" <<'REGB1' infra/emit.h emitTo silent 1 not an answer path: std::print's failed write is made silent to keep fputs's contract, which the header documents ingest_astquery.h astQueryGrouped silent 1 FINDING: a file whose query walk throws drops out of --match/--lint hits with no disclosure in any build; file held by the regexguard and crash-fixes lanes +ingest_astquery.h definitionLinesWorker alert-only 1 records in every build: the handler is the sink form DISCLOSE( ( BlobDefsSlot{ slot } ), WorkerThrew, … ), whose disclose() clears that blob's lines and marks it Failed, so the blob confirms no definition (its rows read kind="text"); the parenthesized sink is what the RECORDS pattern does not see ingest_astquery.h spanTiersOfFiles alert-only 1 FINDING (disclosure lane): a tier worker that throws leaves its files unclassified for --grep-in, silent in Release ingest_docpass.h runDocPostPass alert-only 1 FINDING (disclosure lane): a doc post-pass throw skips that document, silent in Release mcpindex.h maybePrefetchHeadSnapshot silent 1 not an answer path: optional background prefetch of the HEAD snapshot; a request recomputes whatever it did not fill (§2b rule 3) @@ -497,6 +498,8 @@ infra/dynamic_map.hpp at 1 vendored container API; no ripwire call site reaches regexguard.h throwIfMatchFaultInjected 1 the RIPWIRE_FAULT_REGEX_MATCH fault switch, off unless the variable is set; its three call sites are the first statement inside GuardedRegex's search, search and forEachMatch try blocks, which catch std::regex_error by type and return Exhausted REGB2 cat > "$TMP/reg_c.tsv" <<'REGC' +crossref.h listTreeRows .at 2 readTreeLevels returns its table only when every level was served whole by catFileBatch and every child tree id was queued unless already read, so the root tree and each subtree id it walks are keys +crossref.h listTreesOfRefs .at 1 rootTreesOf returns its map only when catFileBatch served every distinct tip (an answer for another object fails the read) and each served tip is assigned its tree, so every ref's tip is a key infra/dynamic_map.hpp at .at 1 the non-const at forwards to the const at of the same container; no ripwire caller ingest_astquery.h nearestNodeKindHint .at 1 the key was just read out of this same map by nearestNameByEditDistance, so it is present REGC @@ -522,6 +525,7 @@ ingest_astquery.h collectGatedLocalNames ts_parser_new 1 deleted before each of ingest_astquery.h collectGatedLocalNames ts_parser_parse_string 1 deleted before the final return ingest_astquery.h compileGrammarQueries ts_query_new 2 moved into GrammarQueries (or deleted when the combined query does not add up); astQueryGrouped deletes every one by hand after the pass ingest_astquery.h computeGrammarDisclosure ts_query_new 1 a compile probe, deleted inside the same if +ingest_astquery.h definitionLinesWorker ts_query_cursor_new 1 deleted on both exits of the noexcept worker: the no-parser return (when non-null) and after the claim loop, whose one throwing call sits inside try/catch ingest_astquery.h spanTiersOfFiles ts_parser_parse_string 1 the tree is deleted right after the walk ingest_crawl.h compileQueryStandalone ts_query_new 1 returned into the process-lifetime CompiledQueryCache, whose destructor deletes every query ingest_parsepool.h runParseWorker ts_query_cursor_new 1 deleted at the end of the worker; the returns in between belong to lambdas diff --git a/test/impactimportcheck.sh b/test/impactimportcheck.sh index ea9e08830..239e22e92 100755 --- a/test/impactimportcheck.sh +++ b/test/impactimportcheck.sh @@ -261,11 +261,17 @@ fi # lead (each file's own importer count), and --limit sizes the tier like the symbol rows. The sandbox makes path # order and relevance disagree: 45 leaf importers pkg/a_NN.py (nobody imports them) sort before 3 pkg/z_N.py, # each imported by 4 other files. RED on 60b65f02 (the three arms below); GREEN on cut-fix C. -RS="$( mktemp -d )" -mkdir -p "$RS/pkg" -python3 - "$RS" <<'PY' +# lane lean-trio (idea #6): 60 unrelated importers (pkg/x_NN.py, importing pkg/other.py) keep hub.py BELOW the umbrella +# line (imported by fewer than half the files that import anything), so these arms keep their premise; arm #9e builds the +# same sandbox WITHOUT them, where hub.py is an umbrella and the tier becomes a count. +mkhub(){ # $1 dir $2 extra unrelated importers +mkdir -p "$1/pkg" +python3 - "$1" "$2" <<'PY' import os, sys d = os.path.join( sys.argv[1], "pkg" ) +open( os.path.join( d, "other.py" ), "w" ).write( "def otherFn( x ):\n return x\n" ) +for k in range( int( sys.argv[2] ) ): + open( os.path.join( d, "x_%02d.py" % k ), "w" ).write( "from pkg.other import otherFn\n\ndef use_x_%02d( ):\n return otherFn( %d )\n" % ( k, k ) ) open( os.path.join( d, "__init__.py" ), "w" ).write( "" ) open( os.path.join( d, "hub.py" ), "w" ).write( "def importHubFn( x ):\n return x\n" ) for i in range( 45 ): @@ -275,6 +281,9 @@ for i in range( 3 ): for j in range( 4 ): open( os.path.join( d, "d_%d.py" % j ), "w" ).write( "".join( "from pkg.z_%d import use_z_%d\n" % ( i, i ) for i in range( 3 ) ) ) PY +} +RS="$( mktemp -d )" +mkhub "$RS" 60 ri(){ perl -e 'alarm 30; exec @ARGV' "$BIN" "$RS" --impact=importHubFn --no-cache "$@" 2>/dev/null; } R_DEF="$( ri )" R_ALL="$( ri --limit=100 )" @@ -292,6 +301,184 @@ body "$R_DEF" | grep -oE '/dev/null )" +U_ALL="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$RU" --impact=importHubFn --no-cache --limit=100 2>/dev/null )" +{ [ "$( attr importers "$U_DEF" )" = 48 ] && [ "$( attr shown_importers "$U_DEF" )" = 0 ] && [ "$( attr importers_umbrella "$U_DEF" )" = 1 ] \ + && [ "$( attr importers_next "$U_DEF" )" = "--impact=importHubFn --limit=48" ] && [ "$( body "$U_DEF" | grep -c ' +# rolls the WHOLE reach set up by file before the symbol rows: files= counts every reaching file, the default page lists +# 20 rf rows with files_next= when cut, and pasting files_next= names every file (as rf rows while the symbol window is +# still cut, as the symbol rows' own p= once it is not). An UNCUT symbol window carries no rollup: its rows already name +# every file. Noun-prefixed counts only, so the root's bare shown=/capped= stay the only ones (emittertruthcheck reads +# the last shown= on the line). RED on main 0852bc0f: no element. +F_ROOT="$( body "$R_DEF" | grep -o ']*>' | head -1 )" +F_N="$( attr files "$F_ROOT" )"; F_SH="$( attr shown_files "$F_ROOT" )" +F_DEF="$( body "$R_DEF" | grep -o ' and files_next= names the listing call" +else + no "rollup: expected 20 shown_files=20 files_capped=1 files_next=> beside a cut window; got '$F_ROOT' with $F_DEF rf rows" +fi +R_FN="$( ri --limit="${F_N:-0}" )" +if [ "$( attr capped "$R_FN" )" = 1 ]; then + F_PASTE="$( body "$R_FN" | grep -o ']*p="[^":]*' | sed 's/.*p="//' | sort -u | wc -l | tr -d ' ' )" +fi +[ -n "$F_N" ] && [ "$F_PASTE" = "$F_N" ] \ + && ok "rollup: pasting files_next= names all $F_N files" \ + || no "rollup: pasting files_next= named $F_PASTE files (want ${F_N:-?})" +body "$R_DEF" | grep -o ']*>' | head -1 | grep -qE 'syms="[0-9]+" d="1"' \ + && ok "rollup: the first rf row is a depth-1 file (ordered by d= first)" \ + || no "rollup: the first rf row is not depth 1: $( body "$R_DEF" | grep -o ']*>' | head -1 )" +[ "$( body "$R_DEF" | sed 's/ rows, so a file whose every reached +# symbol was hedged read as a dependant of the asked symbol. Now, on a multi-definition selector only, an rf row whose +# reached symbols are ALL hedged carries via="name", a mixed file by_name=K, an all-proven file neither. Near misses: the +# same corpus with ONE definition (the free function's file removed) and a qualified selector fold exactly as before (no +# hedge attribute). Python, TypeScript and Go each. RED on e07f8f33 (train 27): no rf row carries via=/by_name=. +# Shape per language: 45 files o_NN calling o.arrange() on an untyped/interface receiver (name-only edges, so the window is +# cut at 40), mixed.* with one proven call and one name-only call, a_proven.* (py/go: a proven call to the free function). +mkmulti(){ # $1 dir $2 lang (py|ts|go) $3 1 = also write the free function's file (two definitions) +python3 - "$1" "$2" "$3" <<'PY' +import os, sys +d, lang, two = sys.argv[1], sys.argv[2], sys.argv[3] == "1" +w = lambda n, t: open( os.path.join( d, n ), "w" ).write( t ) +if lang == "py": + os.makedirs( os.path.join( d, "pkg" ) ); d = os.path.join( d, "pkg" ); w( "__init__.py", "" ) + w( "widget.py", "class Widget:\n def arrange(self, n):\n return n\n" ) + if two: w( "arr.py", "def arrange(n):\n return n\n" ) + for k in range( 45 ): w( "o_%02d.py" % k, "def use_o_%02d(o):\n return o.arrange(%d)\n" % ( k, k ) ) + if two: + w( "mixed.py", "from pkg.arr import arrange\n\ndef proven_m():\n return arrange(2)\n\ndef named_m(o):\n return o.arrange(3)\n" ) + w( "a_proven.py", "from pkg.arr import arrange\n\ndef proven_a():\n return arrange(4)\n" ) +elif lang == "ts": + os.makedirs( os.path.join( d, "src" ) ); d = os.path.join( d, "src" ) + w( "widget.ts", "export class Widget {\n arrange(n: number): number { return n; }\n}\n" ) + if two: w( "arr.ts", "export function arrange(n: number): number { return n; }\n" ) + for k in range( 45 ): w( "o_%02d.ts" % k, "export function useO%02d(o: any): number { return o.arrange(%d); }\n" % ( k, k ) ) + w( "mixed.ts", "import { Widget } from \"./widget\";\nexport function provenM(): number { return new Widget().arrange(2); }\n" + "export function namedM(o: any): number { return o.arrange(3); }\n" ) +else: + w( "go.mod", "module example.com/m\n\ngo 1.21\n" ) + # the interface's method is a definition too, so the single-definition corpus has no interface: its receivers are *Widget + w( "widget.go", "package m\n\ntype Widget struct{}\n\nfunc (w *Widget) Arrange(n int) int { return n }\n" ) + if two: + w( "iface.go", "package m\n\ntype Arranger interface{ Arrange(int) int }\n" ) + w( "arr.go", "package m\n\nfunc Arrange(n int) int { return n }\n" ) + for k in range( 45 ): w( "o_%02d.go" % k, "package m\n\nfunc UseO%02d(o %s) int { return o.Arrange(%d) }\n" % ( k, "Arranger" if two else "*Widget", k ) ) + if two: + w( "mixed.go", "package m\n\nfunc ProvenM() int { return Arrange(2) }\n\nfunc NamedM(o Arranger) int { return o.Arrange(3) }\n" ) + w( "a_proven.go", "package m\n\nfunc ProvenA() int { return Arrange(4) }\n" ) +PY +} +rfrow(){ body "$1" | grep -oE "]*>" | head -1; } +for LG in py ts go; do + # Q: the qualified spelling each language's resolver takes (Owner.name for Python; file:name for TS and Go) + case $LG in py) SY=arrange; Q=Widget.arrange; PD=pkg/;; ts) SY=arrange; Q=widget.ts:arrange; PD=src/;; go) SY=Arrange; Q=widget.go:Arrange; PD=;; esac + MD="$( mktemp -d )"; SD="$( mktemp -d )" + mkmulti "$MD" $LG 1; mkmulti "$SD" $LG 0 + M_OUT="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$MD" --impact=$SY --no-cache --legend=full 2>/dev/null )"; rcM=$? + S_OUT="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$SD" --impact=$SY --no-cache --legend=full 2>/dev/null )"; rcS=$? + Q_OUT="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$MD" --impact=$Q --no-cache --legend=full 2>/dev/null )"; rcQ=$? + M_DEFS="$( attr defs "$M_OUT" )"; S_DEFS="$( attr defs "$S_OUT" )"; Q_DEFS="$( attr defs "$Q_OUT" )" + # presence guards: each document answered, the multi corpus matched >1 definition and cut its window, its o_ rows are + # hedged as SYMBOL rows (the fact the fold must keep), and the single/qualified near misses matched exactly one + if [ "$rcM" = 0 ] && [ -n "$M_DEFS" ] && [ "$M_DEFS" -gt 1 ] && [ "$( attr capped "$M_OUT" )" = 1 ] \ + && body "$M_OUT" | grep -qE ']*p="'"$PD"'o_[0-9]+\.[a-z]+:[0-9]+"[^>]* via="name"' \ + && [ "$rcS" = 0 ] && [ "$S_DEFS" = 1 ] && [ "$( attr capped "$S_OUT" )" = 1 ] && [ "$rcQ" = 0 ] && [ "$Q_DEFS" = 1 ]; then + ok "multidef $LG: guard — defs=$M_DEFS cut window with via=name o_ symbol rows; single-def and $Q near misses defs=1" + else + no "multidef $LG: premise broken — rc=$rcM/$rcS/$rcQ defs=$M_DEFS/$S_DEFS/$Q_DEFS capped=$( attr capped "$M_OUT" )/$( attr capped "$S_OUT" )" + fi + # the single-definition near miss can only catch a hedge applied regardless of defs= if ITS symbol rows are hedged too + # (Python, TS: untyped receivers; Go's *Widget receivers bind proven, so its arm is the plain fold only) + if [ $LG = go ] || body "$S_OUT" | grep -qE ']* via="name"'; then + ok "multidef $LG: single-definition near miss premise (py/ts: its symbol rows carry via=name)" + else + 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"/>' \ + && 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='; } \ + && 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 + P_RF="$( rfrow "$M_OUT" 'a_proven\.[a-z]+' )" + { [ -n "$P_RF" ] && ! printf '%s' "$P_RF" | grep -qE 'via=|by_name='; } \ + && ok "multidef $LG: an all-proven file stays plain: $P_RF" \ + || no "multidef $LG: the all-proven rf row: '$P_RF' (want present, no via=/by_name=)" + fi + 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"\}' \ + && ok "multidef $LG: --json carries the same hedge" \ + || no "multidef $LG: --json rollup rows lack \"via\":\"name\"" + for NM in single qualified; do + [ $NM = single ] && NO="$S_OUT" || NO="$Q_OUT" + NRF="$( body "$NO" | grep -o ']*>' | wc -l | tr -d ' ' )" + { [ "$NRF" -gt 0 ] && ! body "$NO" | grep -oE ']*>' | grep -qE 'via=|by_name=' && ! printf '%s' "$NO" | grep -qF 'rf via="name"'; } \ + && ok "multidef $LG: $NM-definition near miss folds as before ($NRF rf rows, no hedge, no sentence)" \ + || no "multidef $LG: $NM-definition near miss: $NRF rf rows, hedge or sentence present" + done + if command -v xmllint >/dev/null 2>&1; then + if printf '%s' "$M_OUT" | xmllint --noout - 2>/dev/null; then + ok "multidef $LG: well-formed with the hedge sentence (--legend=full)" + else + no "multidef $LG: malformed XML" + fi + fi + [ $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\"' \ + && ok "multidef py: the MCP impact twin keeps the same hedge" \ + || no "multidef py: MCP impact rollup rows lack via=/by_name=" + } + rm -rf "$MD" "$SD" +done + # ── #9c THE CUT NAMES ITS CALL (cut-fix E, 2026-09-24) ────────────────────────────────────────────────── # A cut tier was a DEAD-END cut (answer-completeness §1.3/§5.8): counted, and no call named that serves the rest. # importers_next= on a cut root (the root's next= is --safe-delete's), in the XML and the JSON dialect; pasting it diff --git a/test/importcapcheck.sh b/test/importcapcheck.sh index b443f199a..9eaf6f4f8 100755 --- a/test/importcapcheck.sh +++ b/test/importcapcheck.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# importcapcheck.sh — the gate for the shared import-capture vocabulary (issue #358), C-family slice. +# importcapcheck.sh — the gate for the shared import-capture vocabulary (issue #358), C-family and Go slices. # # WHAT THIS PINS. `#include` / `#import` for c, cpp, objc (and the .h/.hpp/.cu/.cuh/.metal extensions # that ride those grammars) no longer come from a per-language extractor in src/ingest_relations.h. They @@ -40,6 +40,20 @@ # identically, which is the evidence the records did not change # (RIPWIRE_BASE_BIN, the house name; SKIPs when unset, as on CI) # +# +# THE GO SLICE (arms 14-19, below the C-family arms). Go's `import_declaration` — single and grouped, aliased, +# dot, blank, cgo `import "C"` — is captured as `@import.path` in queries/go/tags.scm and read by the ONE +# DepDialect::Go normaliser in src/ingest_importcap.h; the walk in captureIncludes no longer runs for Go. +# 14 the Go capture is COMPILED IN — `(import_declaration) @import.path` is in the binary's embedded tags.scm +# 15 every Go form, exact rows — single/aliased/dot/blank/group/cgo/tab/semicolon/raw-string/UTF-8 cut/error +# recovery, pinned as the byte-for-byte rows the extractor emitted +# 16 no widening — an import_declaration the grammar never makes a child of the file root +# (inside a function body) stays out, exactly as the walk left it +# 17 resolution through imports — a go.mod `replace` still maps an import onto a sibling root's package +# (single, aliased and grouped), and a bogus replace target removes the edge +# 18 the use-site half — `--uses=` reports role="import" for a Go import +# 19 Go cache + determinism — warm == cold == --no-cache on --deps over the Go fixture +# # Usage: test/importcapcheck.sh # RIPWIRE_BIN=asan/ripwire test/importcapcheck.sh # Exits non-zero on any failure. Does NOT edit test/regression.sh or test/golden.xml. @@ -443,6 +457,514 @@ else no "non-deterministic output" fi +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# JS/TS SLICE (DepDialect::Web) — TypeScript, TSX, JavaScript and the .astro frontmatter. +# +# WHAT THIS PINS. `import … from 'x'`, the re-exports (`export * from`, `export { a as b } from`, +# `export type { T } from`), and the CommonJS / dynamic `require('x')` / `import('x')` no longer come from +# captureIncludes' walk (directiveTargetOf's import_statement-source / export_statement / call_expression +# branches). They come from the same `@import.path` name, three patterns per queries/{typescript,tsx, +# javascript}/tags.scm, normalised by the DepDialect::Web arm of src/ingest_importcap.h. The EMITTED edges +# are exactly the ones the walk emitted, for every spelling — and for every spelling the walk did NOT emit: +# +# J1 the Web patterns are COMPILED IN, and the walk's TS/JS branches are GONE from src/ +# J2 exact edge list, in source order, for .ts .js .tsx .mjs .cjs .jsx and the .astro frontmatter — +# ES default/named/namespace/side-effect/type imports, three re-export spellings, CommonJS in every +# container shape the walk entered (declaration, assignment, export, callback, await, if/try/for…) +# J3 THE NEGATIVES — the query matches ANY `x('s')` call at ANY depth; every `neg_*` specifier below is +# a shape the walk never turned into an edge (member callee, `.foo` chained on the call, ternary, +# array, two arguments, a comment before the string, template literal, computed, `require.resolve`, +# a `new` argument, TS `import x = require()`, a `declare module` body, a class method) and none may +# become one. The leading text gate (callee is `require`/`import`) and the reach rule are what hold. +# J4 the LAZY bit — a require()/import() written inside a function body is lazy="1" on --impact's import +# tier; a top-level one is lazy="0"; one file required both ways is lazy="0" (the pair rule) +# J5 the use-site half — --uses reports the import-role ref of a named import +# J6 the DEPTH BOUND on blocks — kept at 255 and 256 containers, cut AND announced at 257, as the walk did +# J7 the DELIBERATE disclosure differences (the two of the C-family arm 12c, same reason, plus a non-import under a too-deep nest) +# J8 cache round-trip, a pre-change binary's cache accepted (no kParserVer bump), determinism +# +# The expected lists are the SPECIFICATION and were read off the pre-change binary, never reasoned out: this +# slice is a refactor, so main's set is the contract. The `neg_*` / `lazy_*` / `cjs_*` names say what each +# edge (or non-edge) is, so a diff reads without opening the fixture. + +web_fixture_ts() { local W="$1" + cat > "$W/main.ts" <<'TS' +import def from "./es_default"; +import { named1, named2 as alias2 } from "./es_named"; +import * as ns from "./es_namespace"; +import "./es_sideeffect"; +import type { T } from "./es_type_named"; +import type DefT from "./es_type_default"; +import { type U, V } from "./es_inline_type"; +import dflt, { n1 } from "./es_default_and_named"; +import json from "./es_import_attributes" with { type: "json" }; +export * from "./re_star"; +export { default } from "./re_default"; +export { default as D } from './re_default_as_single_quote'; +export * as hns from "./re_star_as"; +export { x as y } from "./re_named_as"; +export type { Z } from "./re_type"; +import eq = require("./neg_import_equals"); +import type eq2 = require("./neg_import_equals_type"); +export = ns; +const r1 = require("./cjs_const"); +const { q1, q2 } = require("./cjs_destructured"); +var r2 = require('./cjs_var_single'); +require("./cjs_bare"); +module.exports = require("./cjs_module_exports"); +const dyn = await import("./dyn_await"); +const opt = require?.("./cjs_optional_call"); +const memb = require("./neg_member").foo; +const tern = cond ? require("./neg_ternary") : null; +const arr = [require("./neg_array")]; +const two = require("./neg_two_args", "./neg_second"); +const cm = require(/*c*/ "./neg_comment"); +const tpl = require(`./neg_template`); +const nonlit = require(neg_variable); +const other = foo.require("./neg_foo_require"); +const cat = require("./neg_concat" + sfx); +const res = require.resolve("./neg_resolve"); +const nw = new Foo(require("./neg_new_arg")); +const nest = foo(bar(require("./fn_arg_nested"))); +const th = import("./neg_dynamic_then").then(( m ) => m); +const bang = !require("./neg_unary"); +function f() { return require("./lazy_function"); } +const g2 = () => import("./lazy_arrow"); +class K { m() { return require("./neg_class_method"); } } +const o = { get G() { return require("./lazy_getter"); } }; +if (x) { require("./if_then"); } else { require("./if_else"); } +try { require("./try_body"); } catch (e) { require("./catch_body"); } +for (;;) { require("./for_body"); } +switch (x) { case 1: require("./switch_case"); } +export default require("./export_default_require"); +export const ex = require("./export_const_require"); +export function ef() { return import("./export_fn_dynamic"); } +foo(function () { require("./callback_fn"); }); +foo(() => require("./callback_arrow"), require("./callback_second")); +const pe = (require("./paren_require")); +const aw = await require("./await_require"); +label: require("./labeled"); +TS +} +web_fixture_js() { local W="$1" + cat > "$W/main.js" <<'JS' +import def from "./es_default"; +import { named1, named2 as alias2 } from "./es_named"; +import * as ns from "./es_namespace"; +import "./es_sideeffect"; +export * from "./re_star"; +export * as hns from "./re_star_as"; +export { x as y } from "./re_named_as"; +const r1 = require("./cjs_const"); +const { q1, q2 } = require("./cjs_destructured"); +require("./cjs_bare"); +module.exports = require("./cjs_module_exports"); +const dyn = await import("./dyn_await"); +const memb = require("./neg_member").foo; +const arr = [require("./neg_array")]; +const two = require("./neg_two_args", "./neg_second"); +const cm = require(/*c*/ "./neg_comment"); +const nonlit = require(neg_variable); +function f() { return require("./lazy_function"); } +const g2 = () => import("./lazy_arrow"); +class K { m() { return require("./neg_class_method"); } } +exports.run = function () { return require("./lazy_exports_fn"); }; +JS +} +web_fixture_small() { local W="$1" # .tsx .mjs .cjs .jsx and the .astro frontmatter + cat > "$W/neg.ts" <<'NEG' +declare module "amb" { import q from "./neg_ambient"; } +namespace NS { import w = require("./neg_ns_equals"); const z2 = require("./neg_ns_const"); } +NEG + cat > "$W/comp.tsx" <<'TSX' +import React from "react"; +import { Button } from "./tsx_button"; +export { Card } from "./tsx_card"; +export const View = () => ; +const side = require("./tsx_cjs"); +TSX + printf 'import m from "./mjs_import";\nexport * from "./mjs_reexport";\nconst c = require("./mjs_cjs");\n' > "$W/mod.mjs" + printf 'const a = require("./cjs_file");\nmodule.exports = { get b() { return require("./cjs_lazy"); } };\n' > "$W/mod.cjs" + printf 'import J from "./jsx_import";\nconst V = () => ;\n' > "$W/view.jsx" + printf '%s\n' '---' 'import A from "./astro_import";' 'const m = require("./astro_require");' 'export * from "./astro_reexport";' '---' '
{A}
' > "$W/page.astro" +} +web_fixture_stubs() { local W="$1" + # every specifier above names a real file, so each edge RESOLVES and the lazy bit and --uses are observable + local spec + for spec in $(cat "$W"/main.ts "$W"/neg.ts "$W"/main.js "$W"/comp.tsx "$W"/mod.mjs "$W"/mod.cjs "$W"/view.jsx "$W"/page.astro \ + | grep -o '"\./[A-Za-z_0-9]*"\|'"'"'\./[A-Za-z_0-9]*'"'" | tr -d "\"'" | sort -u); do + printf 'export const %s = 1;\n' "${spec#./}" > "$W/${spec#./}.js" + done +} +web_fixture() { # $1 = an empty directory + mkdir -p "$1" + web_fixture_ts "$1"; web_fixture_js "$1"; web_fixture_small "$1"; web_fixture_stubs "$1" +} + +# The expected Include targets per file, in source order, as one space-separated line each — read off the +# PRE-CHANGE binary on exactly this fixture. 40 / 15 / 4 / 3 / 2 / 1 / 3 edges. +W_MAIN_TS='./es_default ./es_named ./es_namespace ./es_sideeffect ./es_type_named ./es_type_default ./es_inline_type ./es_default_and_named ./es_import_attributes ./re_star ./re_default ./re_default_as_single_quote ./re_star_as ./re_named_as ./re_type ./cjs_const ./cjs_destructured ./cjs_var_single ./cjs_bare ./cjs_module_exports ./dyn_await ./cjs_optional_call ./fn_arg_nested ./lazy_function ./lazy_arrow ./lazy_getter ./if_then ./if_else ./try_body ./catch_body ./for_body ./export_default_require ./export_const_require ./export_fn_dynamic ./callback_fn ./callback_arrow ./callback_second ./paren_require ./await_require ./labeled' +W_MAIN_JS='./es_default ./es_named ./es_namespace ./es_sideeffect ./re_star ./re_star_as ./re_named_as ./cjs_const ./cjs_destructured ./cjs_bare ./cjs_module_exports ./dyn_await ./lazy_function ./lazy_arrow ./lazy_exports_fn' +W_COMP_TSX='react ./tsx_button ./tsx_card ./tsx_cjs' +W_MOD_MJS='./mjs_import ./mjs_reexport ./mjs_cjs' +W_MOD_CJS='./cjs_file ./cjs_lazy' +W_VIEW_JSX='./jsx_import' +W_PAGE_ASTRO='./astro_import ./astro_require ./astro_reexport' + +WEB="$TMP/web"; web_fixture "$WEB" +"$BIN" "$WEB" --deps --no-cache > "$TMP/web.deps.xml" 2> "$TMP/web.deps.err" +web_incs(){ sed 's/= 6 occurrences of the JS/TS @import.path patterns in the binary, got '${WEB_PATTERNS:-none}'" +fi +# train 27: the walk's branch is matched with its language condition — `kindIs( t, "export_statement" )` alone also matches +# ingest_relations.h baseDeclOf (lego-transitive: `export class X` is the declaration a base clause binds to), which is not the +# walk. The full spelling is the line the walk held (main 0852bc0f: present; every tree since #358's JS/TS slice: absent). +for gone in 'kindIs( t, "export_statement" ) && ( lang == Lang::TypeScript || lang == Lang::JavaScript )' 'target = jsModuleLoadTarget( n, src );'; do + if grep -rqF "$gone" "$ROOT/src"; then + no "J1: the walk's TS/JS branch \`$gone\` is still in src/ — it should have been removed with this language" + else + ok "J1: the walk's TS/JS branch is gone from src/ ($gone)" + fi +done + +# ── J2 + J3: exact edge list per file, which also pins every negative ────────────────────────────────── +WEB_BAD=0 +for pair in "main.ts:$W_MAIN_TS" "main.js:$W_MAIN_JS" "comp.tsx:$W_COMP_TSX" "mod.mjs:$W_MOD_MJS" \ + "mod.cjs:$W_MOD_CJS" "view.jsx:$W_VIEW_JSX" "page.astro:$W_PAGE_ASTRO"; do + f=${pair%%:*}; want=${pair#*:} + got=$(web_incs "$f") + if [ -z "$got" ] || [ "$got" != "$want" ]; then + WEB_BAD=1 + no "J2: $f Include targets differ from the pre-change set" + printf ' expected: %s\n got: %s\n' "$want" "${got:-}" + fi +done +[ "$WEB_BAD" -eq 0 ] && ok "J2: exact Include set and order for .ts .js .tsx .mjs .cjs .jsx and the .astro frontmatter (68 edges)" +NEG=$(grep -o ' "$LZ/entry.js" +for n in eager lazy_only both; do printf 'function %s_fn() { return 1; }\nmodule.exports = %s_fn;\n' "$n" "$n" > "$LZ/$n.js"; done +LZ_BAD=0 +for pair in eager_fn:0 lazy_only_fn:1 both_fn:0; do + s=${pair%%:*}; want=${pair#*:} + got=$("$BIN" "$LZ" --impact="$s" --no-cache 2>/dev/null | grep -o '/dev/null | grep -o ' "$WB/$1/deep.js" + local i=0 + { while [ "$i" -lt "$1" ]; do printf '{'; i=$((i + 1)); done + printf ' require("./deep"); ' + i=0; while [ "$i" -lt "$1" ]; do printf '}'; i=$((i + 1)); done; printf '\n'; } > "$WB/$1/a.js" +} +WB_BAD=0 +for n in 254 255 256; do + web_bound "$n" + e=$("$BIN" "$WB/$n" --deps --no-cache 2>/dev/null | grep -o '/dev/null | grep -c 'why="extract-partial"' | tr -d '[:space:]' || true) + if [ "$n" -le 255 ]; then want_e=1; want_f=0; else want_e=0; want_f=1; fi + if [ "$e" != "$want_e" ] || [ "$f" != "$want_f" ]; then + WB_BAD=1 + no "J6: $n nested blocks: edge=$e extract-partial=$f (want $want_e/$want_f)" + fi +done +[ "$WB_BAD" -eq 0 ] && ok "J6: depth bound — kept under 255 and 256 containers, cut AND announced under 257 (edges identical to the walk's)" + +# ── J7: the DELIBERATE disclosure differences, in both directions (the C-family arm 12c, for JS) ──── +# (i) QUIETER: `require('./deep')` is ITSELF a container, so the walk refused to descend into it from depth +# 256 and announced, although the edge was kept and nothing was dropped. n=255 above is that shape: edge +# kept, and this binary says nothing because nothing was lost (the walk said extract-partial). +# (ii) LOUDER: 300 blocks inside a class METHOD. The walk never entered the class (class_declaration is not +# an import container) so it said nothing; the ancestor loop reaches the bound first and announces. The +# safe direction — a floor that says so — and the same trade the C-family slice accepted. +WL="$TMP/wloud"; mkdir -p "$WL"; printf 'export const deep = 1;\n' > "$WL/deep.js" +{ printf 'class K { m() { '; i=0; while [ "$i" -lt 300 ]; do printf '{'; i=$((i + 1)); done + printf ' require("./deep"); '; i=0; while [ "$i" -lt 300 ]; do printf '}'; i=$((i + 1)); done; printf ' } }\n'; } > "$WL/a.js" +LE=$("$BIN" "$WL" --deps --no-cache 2>/dev/null | grep -o '/dev/null | grep -c 'why="extract-partial"' | tr -d '[:space:]' || true) +QF=$("$BIN" "$WB/255" --skipped --no-cache 2>/dev/null | grep -c 'why="extract-partial"' | tr -d '[:space:]' || true) +if [ "$LE" = "0" ] && [ "$LF" = "1" ] && [ "$QF" = "0" ]; then + ok "J7: disclosure differences hold — silent when the edge was kept (255 blocks), loud when the bound cut an unreachable import (class method)" +else + no "J7: class-method-under-300: edges=$LE partial=$LF (want 0/1); 255-blocks partial=$QF (want 0)" +fi + +# (iii) QUIETER, and no import in the file at all: 300 nested blocks around `foo( "x" )` (a call the Web pattern +# captures and the text gate drops), and 300 nested #ifdef around `#pragma once` (captured by the C/C++ +# preproc_call pattern, dropped by the #import gate). The walk announced on any too-deep CONTAINER, and the +# first version of this arm announced because the bound test ran before the text gate; both said "an import +# was cut" when nothing import-shaped existed. The text gate now runs first, so neither does. +WQ="$TMP/wquiet"; mkdir -p "$WQ/js" "$WQ/c" +{ i=0; while [ "$i" -lt 300 ]; do printf '{'; i=$((i + 1)); done + printf ' foo("x"); '; i=0; while [ "$i" -lt 300 ]; do printf '}'; i=$((i + 1)); done; printf '\n'; } > "$WQ/js/a.js" +{ i=0; while [ "$i" -lt 300 ]; do printf '#ifdef G%s\n' "$i"; i=$((i + 1)); done + printf '#pragma once\n'; i=0; while [ "$i" -lt 300 ]; do printf '#endif\n'; i=$((i + 1)); done; } > "$WQ/c/a.c" +QJ=$("$BIN" "$WQ/js" --skipped --no-cache 2>/dev/null | grep -c 'why="extract-partial"' | tr -d '[:space:]' || true) +QC=$("$BIN" "$WQ/c" --skipped --no-cache 2>/dev/null | grep -c 'why="extract-partial"' | tr -d '[:space:]' || true) +if [ "$QJ" = "0" ] && [ "$QC" = "0" ]; then + ok "J7: no announcement when nothing import-shaped was cut (foo( \"x\" ) under 300 blocks, #pragma once under 300 #ifdef)" +else + no "J7: non-import under a too-deep nest announced an import cut: js=$QJ c=$QC (want 0/0)" +fi + +# ── J8: cache round-trip and a pre-change cache ───────────────────────────────────────────────────── +WC="$TMP/web.cache.bin" +"$BIN" "$WEB" --cache="$WC" --deps > "$TMP/web.cold.xml" 2>/dev/null +"$BIN" "$WEB" --cache="$WC" --deps > "$TMP/web.warm.xml" 2>/dev/null +if cmp -s "$TMP/web.cold.xml" "$TMP/web.warm.xml" && cmp -s "$TMP/web.cold.xml" "$TMP/web.deps.xml"; then + ok "J8: warm == cold == --no-cache on --deps for the JS/TS fixture (isLazy round-trips)" +else + no "J8: JS/TS --deps differs between cold, warm and --no-cache" +fi +if [ -n "$BASE_BIN" ] && [ -x "$BASE_BIN" ]; then + WP="$TMP/web.pre.bin"; rm -f "$WP" + "$BASE_BIN" "$WEB" --cache="$WP" --deps >/dev/null 2>&1 + cp "$WP" "$TMP/web.pre.bin.copy" + "$BIN" "$WEB" --cache="$WP" --deps > "$TMP/web.prewarm.xml" 2>/dev/null + if cmp -s "$TMP/web.prewarm.xml" "$TMP/web.deps.xml" && cmp -s "$WP" "$TMP/web.pre.bin.copy"; then + ok "J8: a pre-change binary's JS/TS cache is accepted unchanged and reads identically — no kParserVer bump is owed" + else + no "J8: a pre-change JS/TS cache reads differently or was rewritten — extraction output changed, kParserVer must be bumped" + fi +else + skip "J8: pre-change-cache arm for JS/TS (set RIPWIRE_BASE_BIN= to run it)" +fi +"$BIN" "$WEB" --no-cache --deps > "$TMP/web.d2.xml" 2>/dev/null +if cmp -s "$TMP/web.deps.xml" "$TMP/web.d2.xml"; then + ok "J8: deterministic (two --no-cache JS/TS runs identical)" +else + no "J8: non-deterministic JS/TS output" +fi + +# THE GO SLICE (arms 14-19). Go's import_declaration rides `(import_declaration) @import.path` and the +# DepDialect::Go normaliser; the Include target is the declaration CLAUSE, exactly as the removed walk branch +# built it, so every row below is the byte-for-byte row the extractor emitted (read off the pre-change binary, +# not predicted). A GROUPED import is ONE row whose text is the whole group cut at 96 bytes — resolve.h's +# resolveGoImport documents that — and splitting it per spec would ADD rows, which is exactly what arm 15 +# would show as a diff. +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +GO="$TMP/go"; mkdir -p "$GO/cmd" "$GO/pkg/util" "$GO/replaced" +printf 'module example.com/app\n\ngo 1.21\n\nrequire example.com/ext v0.0.0\n\nreplace example.com/ext => ./replaced\n' > "$GO/go.mod" +printf 'package ext\n\nfunc Ext() int { return 1 }\n' > "$GO/replaced/ext.go" +printf 'package util\n\nfunc Helper() int { return 2 }\n' > "$GO/pkg/util/util.go" +cat > "$GO/main.go" <<'GOSRC' +package main + +import "fmt" +import f2 "os" +import . "strings" +import _ "embed" +import ( + "example.com/app/pkg/util" + alias "example.com/ext" + . "math" + _ "net/http/pprof" + "strconv" // trailing comment +) + +import "C" + +func main() { + fmt.Println(util.Helper(), alias.Ext(), f2.Args, strconv.Itoa(1), ToUpper("x"), Abs(1)) +} +GOSRC +# a TAB after the keyword keeps the word `import` (the keyword is dropped at a SPACE); two spaces leave one +# leading space; a trailing `;` is trimmed; a raw-string path keeps its backticks. +printf 'package main\n\nimport\t"os"\nimport "io";\nimport `raw/path`\n\nfunc t() { _ = os.Args }\n' > "$GO/cmd/tab.go" +cat > "$GO/cmd/long.go" <<'GOSRC' +package main + +import ( + "github.com/very/long/module/path/that/goes/on/and/on/and/on/forever/and/ever/amen/pkg1" + "github.com/very/long/module/path/that/goes/on/and/on/and/on/forever/and/ever/amen/pkg2" +) + +func l() {} +GOSRC +# error recovery: a bare `import` swallowing the next lines is still ONE import_declaration, and a broken +# function header does not stop the next import being recovered. +printf 'package main\n\nimport\nfunc b( {\nimport "after/error"\n' > "$GO/cmd/broken.go" +printf 'package main\n\nfunc f( {\nimport "x"\n}\n' > "$GO/cmd/err.go" +# the 96-byte cut lands mid multibyte sequence on purpose: the pre-change bytes are kept, not "fixed". +printf 'package main\nimport "é世界/pkg"; import "z"\nimport ("é世界/pkg/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\n)\n' > "$GO/cmd/utf.go" +# a definition named like the last path element of utf.go's first import: --uses keys on an INDEXED name, so +# arm 18 needs one (`fmt`, `os` have no definition in the tree and answer found=0). +printf 'package main\n\nfunc pkg() {}\n' > "$GO/cmd/pkgdef.go" +# arm 16's negative: an import inside a function body is not a file-scope declaration and was never an edge. +printf 'package main\n\nfunc a() {\n\timport "inbody"\n}\n' > "$GO/cmd/nested.go" + +"$BIN" "$GO" --deps --no-cache > "$TMP/go.xml" 2>/dev/null +grep -o ' "$TMP/go.rows" +cat > "$TMP/go.want" <<'GOWANT' + "$WS/svc/pkg/handle.go" +printf 'module example.com/svc\n\ngo 1.21\n' > "$WS/svc/go.mod" +printf 'module example.com/cli\n\ngo 1.21\n\nreplace example.com/svc => ../svc\n' > "$WS/cli/go.mod" +printf 'package main\n\nimport svc "example.com/svc/pkg"\n\nfunc runGoMain() int { return svc.GoHandle() }\n' > "$WS/cli/main.go" +printf 'module example.com/cli2\n\ngo 1.21\n\nreplace example.com/svc => ../svc\n' > "$WS/cli2/go.mod" +printf 'package main\n\nimport (\n\tsvc "example.com/svc/pkg"\n\t"fmt"\n)\n\nfunc runGrouped() int { fmt.Println(); return svc.GoHandle() }\n' > "$WS/cli2/main.go" +printf 'module example.com/clibogus\n\ngo 1.21\n\nreplace example.com/svc => ../svc/NONEXISTENT\n' > "$WS/clibogus/go.mod" +printf 'package main\n\nimport svc "example.com/svc/pkg"\n\nfunc runBogus() int { return svc.GoHandle() }\n' > "$WS/clibogus/main.go" +"$BIN" "$WS/svc" "$WS/cli" "$WS/cli2" --deps --no-cache > "$TMP/gows.xml" 2>/dev/null +if grep -q ']*' "$TMP/gows.xml")" +fi +# the evidence-driven negative: the same import under a replace whose target does not exist resolves to nothing. +"$BIN" "$WS/svc" "$WS/clibogus" --deps --no-cache > "$TMP/gobogus.xml" 2>/dev/null +if grep -q ']*' "$TMP/gobogus.xml")" +fi + +# ── arm 18: the use-site half ───────────────────────────────────────────────────────────────────────── +GUSES="$("$BIN" "$GO" --uses=pkg --no-cache 2>/dev/null)" +if printf '%s' "$GUSES" | grep -q ']*' | head -3)" +fi + +# ── arm 19: Go cache round-trip + determinism ──────────────────────────────────────────────────────── +GCACHE="$TMP/go.cache" +"$BIN" "$GO" --cache="$GCACHE" --deps > "$TMP/gocold.xml" 2>/dev/null +"$BIN" "$GO" --cache="$GCACHE" --deps > "$TMP/gowarm.xml" 2>/dev/null +if cmp -s "$TMP/gocold.xml" "$TMP/gowarm.xml" && cmp -s "$TMP/gocold.xml" "$TMP/go.xml"; then + ok "Go: warm == cold == --no-cache on --deps (the Go Include round-trip is byte-identical)" +else + no "Go: --deps differs between cold, warm and --no-cache" +fi +if [ -n "${BASE_BIN:-}" ] && [ -x "${BASE_BIN:-}" ]; then + "$BASE_BIN" "$GO" --deps --no-cache > "$TMP/gobase.xml" 2>/dev/null + if cmp -s "$TMP/gobase.xml" "$TMP/go.xml"; then + ok "Go: --deps over the fixture is byte-identical to the pre-change binary's" + else + no "Go: --deps differs from the pre-change binary — dependency output changed" + fi + # and the Go half of arm 13: a cache written by the pre-change binary over the Go fixture is ACCEPTED + # (file untouched) and reads identically, so no kParserVer bump is owed for the Go records either. + GPRE="$TMP/gopre.bin"; rm -f "$GPRE" + "$BASE_BIN" "$GO" --cache="$GPRE" --deps >/dev/null 2>&1 + cp "$GPRE" "$TMP/gopre.bin.copy" + "$BIN" "$GO" --cache="$GPRE" --deps > "$TMP/gopre.xml" 2>/dev/null + if cmp -s "$TMP/gopre.xml" "$TMP/go.xml" && cmp -s "$GPRE" "$TMP/gopre.bin.copy"; then + ok "Go: a pre-change binary's cache over the Go fixture is accepted untouched and reads identically — no kParserVer bump owed" + else + no "Go: a pre-change cache reads differently or was rewritten — extraction output changed, kParserVer must be bumped" + fi +else + skip "Go: pre-change comparison (set RIPWIRE_BASE_BIN= to run it)" +fi + # ── well-formed XML ─────────────────────────────────────────────────────────────────────────────────── if command -v xmllint >/dev/null 2>&1; then if xmllint --noout "$TMP/cold.xml" 2>/dev/null; then ok "xml well-formed"; else no "xml malformed"; fi diff --git a/test/kotlincheck.sh b/test/kotlincheck.sh index 4297bbe68..1407abd9f 100755 --- a/test/kotlincheck.sh +++ b/test/kotlincheck.sh @@ -276,11 +276,11 @@ HELPER_CALLERS="$( "$BIN" "$FIX" --callers="JavaBridge.java:helper" --no-cache 2 # `JavaBridge.helper(5)` reaches Java's JavaBridge.helper — proven, no via= — while the BARE ambiguousCall() keeps the # own-language-first rule (it never reaches Java's helper) and binds Kotlin's Extra.helper only HEDGED: a top-level # function's bare call reaches an object's member through an import, which this file does not have. -echo "$UTIL_HELPER_CALLERS" | grep -q 'count="1"' && echo "$UTIL_HELPER_CALLERS" | grep -q '' \ +echo "$UTIL_HELPER_CALLERS" | grep -q 'count="1"' && echo "$UTIL_HELPER_CALLERS" | grep -q '' \ && ! echo "$UTIL_HELPER_CALLERS" | grep -q 'n="useJavaHelper"' \ && ok "--callers=Util.kt:helper: count=1 — the bare ambiguousCall, hedged via=\"name\"; the qualified useJavaHelper no longer lands here" \ || no "--callers=Util.kt:helper: expected only the bare call, hedged: $( echo "$UTIL_HELPER_CALLERS" | grep -oE ']*>|]*/>' | tr '\n' ' ' )" -echo "$HELPER_CALLERS" | grep -q 'count="1"' && echo "$HELPER_CALLERS" | grep -q '' \ +echo "$HELPER_CALLERS" | grep -q 'count="1"' && echo "$HELPER_CALLERS" | grep -q '' \ && ok "--callers=JavaBridge.java:helper: count=1 — the qualified useJavaHelper, proven by its class-name receiver; the bare site never reaches Java (own language first)" \ || no "--callers=JavaBridge.java:helper: expected exactly the qualified useJavaHelper, proven: $( echo "$HELPER_CALLERS" | grep -oE ']*>|]*/>' | tr '\n' ' ' )" @@ -876,7 +876,8 @@ for L in split flat; do # reading the Java interface. Implementors of that definition are the read: collapsed as a declaration, it had none. LEGO="$( "$BIN" "$BL/$L" --lego=Marker --no-cache 2>/dev/null )" if echo "$LEGO" | grep -q "]*>' | tr '\n' ' ' )" else diff --git a/test/legobundlecheck.sh b/test/legobundlecheck.sh index a4aa005bb..3d36bd467 100755 --- a/test/legobundlecheck.sh +++ b/test/legobundlecheck.sh @@ -176,8 +176,10 @@ fi # unambiguous, and --lego now discloses the definition count beside the pick. # RE-PINNED AGAIN (§L10b LOW tail): the legend gained one clause defining methods="0" caveat="not-extracted-for-lang" # (the value --lego=Vehicle prints on a Rust trait, previously nowhere spelled out). +# RE-PINNED (lane lego-transitive, S2): the STANDALONE answer's rows carry their definition line (shapes.h:16 / :26); +# the bundle embedding keeps the bare p= (asserted below and in test/legotransitivecheck.sh). LEGO_LEGEND='' -printf '%s' "${LEGO_LEGEND}"'virtual double area() const = 0virtual void draw() const = 0' >"$TMP/standalone.golden" +printf '%s' "${LEGO_LEGEND}"'virtual double area() const = 0virtual void draw() const = 0' >"$TMP/standalone.golden" cmp -s "$TMP/standalone" "$TMP/standalone.golden" \ && ok "--lego=Shape standalone byte-identical to the reference output (bundle-only change)" \ || no "--lego=Shape standalone CHANGED — the §P3 fix must touch the bundle embedding only: $( cat "$TMP/standalone" )" diff --git a/test/legochargefix/pipeline.cpp b/test/legochargefix/pipeline.cpp new file mode 100644 index 000000000..c8c79f6bc --- /dev/null +++ b/test/legochargefix/pipeline.cpp @@ -0,0 +1,59 @@ +// legochargefix/pipeline.cpp — the ranked rows: free functions the render-stage query matches by name. +#include "stages.h" + +int arrangeRenderStageWidgetsLayout( LayoutRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeLayoutWidgets( regionWidth, regionHeight, "Layout" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsLayout( LayoutRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowLayoutWidgets( regionWidth, regionHeight, "Layout" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsLayout( LayoutRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshLayoutWidgets( regionWidth, regionHeight, "Layout" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsLayout( LayoutRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintLayoutWidgets( regionWidth, regionHeight, "Layout" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsCompositor( CompositorRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeCompositorWidgets( regionWidth, regionHeight, "Compositor" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsCompositor( CompositorRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowCompositorWidgets( regionWidth, regionHeight, "Compositor" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsCompositor( CompositorRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshCompositorWidgets( regionWidth, regionHeight, "Compositor" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsCompositor( CompositorRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintCompositorWidgets( regionWidth, regionHeight, "Compositor" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsStyles( StylesRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeStylesWidgets( regionWidth, regionHeight, "Styles" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsStyles( StylesRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowStylesWidgets( regionWidth, regionHeight, "Styles" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsStyles( StylesRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshStylesWidgets( regionWidth, regionHeight, "Styles" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsStyles( StylesRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintStylesWidgets( regionWidth, regionHeight, "Styles" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsScreen( ScreenRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeScreenWidgets( regionWidth, regionHeight, "Screen" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsScreen( ScreenRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowScreenWidgets( regionWidth, regionHeight, "Screen" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsScreen( ScreenRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshScreenWidgets( regionWidth, regionHeight, "Screen" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsScreen( ScreenRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintScreenWidgets( regionWidth, regionHeight, "Screen" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsMessage( MessageRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeMessageWidgets( regionWidth, regionHeight, "Message" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsMessage( MessageRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowMessageWidgets( regionWidth, regionHeight, "Message" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsMessage( MessageRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshMessageWidgets( regionWidth, regionHeight, "Message" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsMessage( MessageRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintMessageWidgets( regionWidth, regionHeight, "Message" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsBinding( BindingRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeBindingWidgets( regionWidth, regionHeight, "Binding" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsBinding( BindingRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowBindingWidgets( regionWidth, regionHeight, "Binding" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsBinding( BindingRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshBindingWidgets( regionWidth, regionHeight, "Binding" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsBinding( BindingRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintBindingWidgets( regionWidth, regionHeight, "Binding" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsTimer( TimerRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeTimerWidgets( regionWidth, regionHeight, "Timer" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsTimer( TimerRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowTimerWidgets( regionWidth, regionHeight, "Timer" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsTimer( TimerRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshTimerWidgets( regionWidth, regionHeight, "Timer" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsTimer( TimerRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintTimerWidgets( regionWidth, regionHeight, "Timer" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsReactive( ReactiveRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeReactiveWidgets( regionWidth, regionHeight, "Reactive" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsReactive( ReactiveRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowReactiveWidgets( regionWidth, regionHeight, "Reactive" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsReactive( ReactiveRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshReactiveWidgets( regionWidth, regionHeight, "Reactive" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsReactive( ReactiveRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintReactiveWidgets( regionWidth, regionHeight, "Reactive" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsScroll( ScrollRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeScrollWidgets( regionWidth, regionHeight, "Scroll" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsScroll( ScrollRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowScrollWidgets( regionWidth, regionHeight, "Scroll" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsScroll( ScrollRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshScrollWidgets( regionWidth, regionHeight, "Scroll" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsScroll( ScrollRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintScrollWidgets( regionWidth, regionHeight, "Scroll" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsFocus( FocusRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeFocusWidgets( regionWidth, regionHeight, "Focus" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsFocus( FocusRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowFocusWidgets( regionWidth, regionHeight, "Focus" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsFocus( FocusRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshFocusWidgets( regionWidth, regionHeight, "Focus" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsFocus( FocusRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintFocusWidgets( regionWidth, regionHeight, "Focus" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsMount( MountRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeMountWidgets( regionWidth, regionHeight, "Mount" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsMount( MountRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowMountWidgets( regionWidth, regionHeight, "Mount" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsMount( MountRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshMountWidgets( regionWidth, regionHeight, "Mount" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsMount( MountRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintMountWidgets( regionWidth, regionHeight, "Mount" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsTheme( ThemeRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeThemeWidgets( regionWidth, regionHeight, "Theme" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsTheme( ThemeRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowThemeWidgets( regionWidth, regionHeight, "Theme" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsTheme( ThemeRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshThemeWidgets( regionWidth, regionHeight, "Theme" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsTheme( ThemeRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintThemeWidgets( regionWidth, regionHeight, "Theme" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsCursor( CursorRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeCursorWidgets( regionWidth, regionHeight, "Cursor" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsCursor( CursorRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowCursorWidgets( regionWidth, regionHeight, "Cursor" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsCursor( CursorRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshCursorWidgets( regionWidth, regionHeight, "Cursor" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsCursor( CursorRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintCursorWidgets( regionWidth, regionHeight, "Cursor" ); return regionWidth + regionHeight; } +int arrangeRenderStageWidgetsRegion( RegionRenderStage& stage, int regionWidth, int regionHeight ) { stage.arrangeRegionWidgets( regionWidth, regionHeight, "Region" ); return regionWidth + regionHeight; } +int reflowRenderStageWidgetsRegion( RegionRenderStage& stage, int regionWidth, int regionHeight ) { stage.reflowRegionWidgets( regionWidth, regionHeight, "Region" ); return regionWidth + regionHeight; } +int refreshRenderStageWidgetsRegion( RegionRenderStage& stage, int regionWidth, int regionHeight ) { stage.refreshRegionWidgets( regionWidth, regionHeight, "Region" ); return regionWidth + regionHeight; } +int repaintRenderStageWidgetsRegion( RegionRenderStage& stage, int regionWidth, int regionHeight ) { stage.repaintRegionWidgets( regionWidth, regionHeight, "Region" ); return regionWidth + regionHeight; } diff --git a/test/legochargefix/stages.h b/test/legochargefix/stages.h new file mode 100644 index 000000000..fd782a5ad --- /dev/null +++ b/test/legochargefix/stages.h @@ -0,0 +1,787 @@ +// legochargefix/stages.h — lego-charge gate fixture (forsectioncollapsecheck.sh arm (13)): a textual-like corpus whose +// --for answer names many interfaces with implementors, so the FULL render (~8 KB) is bigger than the default +// budget while the default answer serves it as a ~150 B counted stub. Generated once; plain C++ on purpose. + +class LayoutRenderStage +{ +public: + virtual ~LayoutRenderStage() = default; + virtual void arrangeLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class LayoutRenderStageImpl1 : public LayoutRenderStage +{ +public: + void arrangeLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class LayoutRenderStageImpl2 : public LayoutRenderStage +{ +public: + void arrangeLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class LayoutRenderStageImpl3 : public LayoutRenderStage +{ +public: + void arrangeLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class LayoutRenderStageImpl4 : public LayoutRenderStage +{ +public: + void arrangeLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveLayoutWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CompositorRenderStage +{ +public: + virtual ~CompositorRenderStage() = default; + virtual void arrangeCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class CompositorRenderStageImpl1 : public CompositorRenderStage +{ +public: + void arrangeCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CompositorRenderStageImpl2 : public CompositorRenderStage +{ +public: + void arrangeCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CompositorRenderStageImpl3 : public CompositorRenderStage +{ +public: + void arrangeCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CompositorRenderStageImpl4 : public CompositorRenderStage +{ +public: + void arrangeCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCompositorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class StylesRenderStage +{ +public: + virtual ~StylesRenderStage() = default; + virtual void arrangeStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class StylesRenderStageImpl1 : public StylesRenderStage +{ +public: + void arrangeStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class StylesRenderStageImpl2 : public StylesRenderStage +{ +public: + void arrangeStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class StylesRenderStageImpl3 : public StylesRenderStage +{ +public: + void arrangeStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class StylesRenderStageImpl4 : public StylesRenderStage +{ +public: + void arrangeStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveStylesWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScreenRenderStage +{ +public: + virtual ~ScreenRenderStage() = default; + virtual void arrangeScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class ScreenRenderStageImpl1 : public ScreenRenderStage +{ +public: + void arrangeScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScreenRenderStageImpl2 : public ScreenRenderStage +{ +public: + void arrangeScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScreenRenderStageImpl3 : public ScreenRenderStage +{ +public: + void arrangeScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScreenRenderStageImpl4 : public ScreenRenderStage +{ +public: + void arrangeScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScreenWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MessageRenderStage +{ +public: + virtual ~MessageRenderStage() = default; + virtual void arrangeMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class MessageRenderStageImpl1 : public MessageRenderStage +{ +public: + void arrangeMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MessageRenderStageImpl2 : public MessageRenderStage +{ +public: + void arrangeMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MessageRenderStageImpl3 : public MessageRenderStage +{ +public: + void arrangeMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MessageRenderStageImpl4 : public MessageRenderStage +{ +public: + void arrangeMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMessageWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class BindingRenderStage +{ +public: + virtual ~BindingRenderStage() = default; + virtual void arrangeBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class BindingRenderStageImpl1 : public BindingRenderStage +{ +public: + void arrangeBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class BindingRenderStageImpl2 : public BindingRenderStage +{ +public: + void arrangeBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class BindingRenderStageImpl3 : public BindingRenderStage +{ +public: + void arrangeBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class BindingRenderStageImpl4 : public BindingRenderStage +{ +public: + void arrangeBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveBindingWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class TimerRenderStage +{ +public: + virtual ~TimerRenderStage() = default; + virtual void arrangeTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class TimerRenderStageImpl1 : public TimerRenderStage +{ +public: + void arrangeTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class TimerRenderStageImpl2 : public TimerRenderStage +{ +public: + void arrangeTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class TimerRenderStageImpl3 : public TimerRenderStage +{ +public: + void arrangeTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class TimerRenderStageImpl4 : public TimerRenderStage +{ +public: + void arrangeTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveTimerWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ReactiveRenderStage +{ +public: + virtual ~ReactiveRenderStage() = default; + virtual void arrangeReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class ReactiveRenderStageImpl1 : public ReactiveRenderStage +{ +public: + void arrangeReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ReactiveRenderStageImpl2 : public ReactiveRenderStage +{ +public: + void arrangeReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ReactiveRenderStageImpl3 : public ReactiveRenderStage +{ +public: + void arrangeReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ReactiveRenderStageImpl4 : public ReactiveRenderStage +{ +public: + void arrangeReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveReactiveWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScrollRenderStage +{ +public: + virtual ~ScrollRenderStage() = default; + virtual void arrangeScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class ScrollRenderStageImpl1 : public ScrollRenderStage +{ +public: + void arrangeScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScrollRenderStageImpl2 : public ScrollRenderStage +{ +public: + void arrangeScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScrollRenderStageImpl3 : public ScrollRenderStage +{ +public: + void arrangeScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ScrollRenderStageImpl4 : public ScrollRenderStage +{ +public: + void arrangeScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveScrollWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class FocusRenderStage +{ +public: + virtual ~FocusRenderStage() = default; + virtual void arrangeFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class FocusRenderStageImpl1 : public FocusRenderStage +{ +public: + void arrangeFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class FocusRenderStageImpl2 : public FocusRenderStage +{ +public: + void arrangeFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class FocusRenderStageImpl3 : public FocusRenderStage +{ +public: + void arrangeFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class FocusRenderStageImpl4 : public FocusRenderStage +{ +public: + void arrangeFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveFocusWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MountRenderStage +{ +public: + virtual ~MountRenderStage() = default; + virtual void arrangeMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class MountRenderStageImpl1 : public MountRenderStage +{ +public: + void arrangeMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MountRenderStageImpl2 : public MountRenderStage +{ +public: + void arrangeMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MountRenderStageImpl3 : public MountRenderStage +{ +public: + void arrangeMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class MountRenderStageImpl4 : public MountRenderStage +{ +public: + void arrangeMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveMountWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ThemeRenderStage +{ +public: + virtual ~ThemeRenderStage() = default; + virtual void arrangeThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class ThemeRenderStageImpl1 : public ThemeRenderStage +{ +public: + void arrangeThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ThemeRenderStageImpl2 : public ThemeRenderStage +{ +public: + void arrangeThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ThemeRenderStageImpl3 : public ThemeRenderStage +{ +public: + void arrangeThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class ThemeRenderStageImpl4 : public ThemeRenderStage +{ +public: + void arrangeThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveThemeWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CursorRenderStage +{ +public: + virtual ~CursorRenderStage() = default; + virtual void arrangeCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class CursorRenderStageImpl1 : public CursorRenderStage +{ +public: + void arrangeCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CursorRenderStageImpl2 : public CursorRenderStage +{ +public: + void arrangeCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CursorRenderStageImpl3 : public CursorRenderStage +{ +public: + void arrangeCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class CursorRenderStageImpl4 : public CursorRenderStage +{ +public: + void arrangeCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveCursorWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class RegionRenderStage +{ +public: + virtual ~RegionRenderStage() = default; + virtual void arrangeRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void reflowRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void refreshRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void repaintRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void measureRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; + virtual void resolveRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) = 0; +}; + +class RegionRenderStageImpl1 : public RegionRenderStage +{ +public: + void arrangeRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class RegionRenderStageImpl2 : public RegionRenderStage +{ +public: + void arrangeRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class RegionRenderStageImpl3 : public RegionRenderStage +{ +public: + void arrangeRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; + +class RegionRenderStageImpl4 : public RegionRenderStage +{ +public: + void arrangeRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void reflowRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void refreshRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void repaintRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void measureRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} + void resolveRegionWidgets( int regionWidth, int regionHeight, const char* stageLabel ) override {} +}; diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh new file mode 100755 index 000000000..228a2462f --- /dev/null +++ b/test/legotransitivecheck.sh @@ -0,0 +1,393 @@ +#!/usr/bin/env bash +# legotransitivecheck.sh — the --lego verb's IMPLEMENTOR CLOSURE and the edges that feed it. +# +# test/legotransitivecheck.sh [BIN] # BIN defaults to $RIPWIRE_BIN, then build/ripwire +# +# Corpus test/legotransitivefix (in-tree, outside test/fixture): +# py/formatters.py BaseFormatter <- {SimpleFormatter <- {Default, Pylint, FilenameOnly}, Nothing} +# py/diamond.py Top <- {Left, Right} <- Bottom (both) <- Deepest; Alone <- OnlyChild (direct rows only) +# rb/*.rb Rake::Task <- {FileTask <- FileCreationTask, MultiTask}; Rake::DSL mixed in +# js/*.js, ts/* Node.extend('BinOp', …) class factories (+ near misses); Loader defined in code AND as a docs heading +# ns/{py,js,cpp,java,rb} fix 1 NAMESAKES: each language holds a base, a Mid extending it, an UNRELATED same-named Mid +# with a Leaf below it, and a true Leaf below the right Mid (by import, scope, namespace or package); +# Python dotted / subscript / keyword bases, a package re-export, a star import; the debug-logger idiom +# +# (S1) the TARGETED --lego lists implementors below a direct one, each once at its shallowest depth, as +# ; transitive=N on the ; implementors= still counts the direct rows alone; +# --limit/--offset (MCP limit/offset) page the deeper rows, a cut says has_more="1" + a next= that pages on. +# An interface with no deeper row carries none of it (byte identity with the direct-only answer). +# (S2) every row of the TARGETED answer says where the class is: p="file:LINE" (the --uses/--callers spelling); the +# row and the ranked --for section keep their bare p=. +# (M1) a Ruby module mixed in with include/extend (captureRubyMixinBases) is an implements edge: --lego=DSL lists TaskLib. +# (M2) a JS/TS class factory `const Sub = Base.extend('Sub', …)` is a class definition extending its receiver. +# (M3) a bare name resolves to a CODE definition whenever one matched, never to a markdown heading sorted first. +# (NS) fix 1: the closure descends only through extends edges the index PROVES (a same-file declaration in scope, an import, +# an include, the package, a qualifier). A name-only edge's row is hedged base_by="name" and not walked; an edge the +# evidence binds to ANOTHER same-named type adds no row and is counted transitive_other_base=. Python's `A.B` base is +# B (inside A), `Generic[K]`'s type argument and `metaclass=M` are no base. Red on 4d59c804 (the namesake rows listed). +# (UW) fix 2 (rule 5): a DIRECT row whose own extends clause is not proven (a cross-package Java import, a Kotlin import, a C# +# using, an ES import of a CommonJS base) is not walked below, and the answer SAYS so: the row carries unwalked="1", the +# counts those rows transitive_unwalked=N and names the first one's --lego as unwalked_next=; a present-only +# legend clause (full) and terms (compact), CLI and MCP. A direct name-only row with no subtype and a proven tree carry +# none of it. Red on d339adf0 (the stop was silent). Corpus uw/{java,kt,cs,js}. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/legotransitivefix" +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; } + +echo "legotransitivecheck: BIN=$BIN" +[ -x "$BIN" ] || { echo " FAIL no ripwire binary at $BIN"; exit 1; } + +# lego SEL [extra flags…] — the answer, one tag per line, in $OUT; returns non-zero (and FAILs) when the run failed or +# printed no root, so an absence arm never reads a crashed or empty run as "not listed". +OUT="" +lego(){ + local sel="$1"; shift + if ! "$BIN" "$FIX" --no-cache --lego="$sel" "$@" >"$DIR/out" 2>"$DIR/err" + then + no "--lego=$sel $* exited non-zero: $( head -3 "$DIR/err" )"; OUT=""; return 1 + fi + OUT="$( sed 's/>\n root"; return 1 + fi + return 0 +} +has(){ printf '%s\n' "$OUT" | grep -qF -- "$1"; } +count(){ printf '%s\n' "$OUT" | grep -cF -- "$1"; } +iface(){ printf '%s\n' "$OUT" | grep -m1 '^'; then ok "a direct row: p=\"py/formatters.py:7\" (the class line)"; else no "direct row: $( printf '%s\n' "$OUT" | grep ' row keeps its bare p= (unchanged)"; else no "iface p= changed: $( iface )"; fi +fi +# the ranked --for section is NOT the targeted answer: its rows keep the bare p= (byte identity outside the verb) +if "$BIN" "$FIX" --no-cache --for="formatter base class" --sections=lego,compose >"$DIR/for" 2>"$DIR/err" +then + if grep -q ' row gained :LINE"; else ok "--for rows keep p=\"file\""; fi + else + no "--for printed no row to check: $( head -c 300 "$DIR/for" )" + fi +else + no "--for exited non-zero: $( head -2 "$DIR/err" )" +fi + +echo "=== (S1) the page: --limit/--offset window the deeper rows; next= pages on ===" +if lego BaseFormatter --limit=2 +then + I="$( iface )" + if printf '%s' "$I" | grep -q 'transitive="3" transitive_shown="2" has_more="1"'; then ok "a cut page says transitive_shown=\"2\" has_more=\"1\""; else no "cut page: $I"; fi + N="$( printf '%s' "$I" | sed -n 's/.* next="\([^"]*\)".*/\1/p' )" + if [ "$N" = "--lego=py/formatters.py:BaseFormatter --limit=2 --offset=2" ]; then ok "next= replays the verb on the iface's file:name with the next offset"; else no "next= is '$N'"; fi + if has '/dev/null 2>"$DIR/err" +then + no "--limit=abc beside --lego was accepted" +else + if grep -q -- '--limit' "$DIR/err"; then ok "--limit=abc beside --lego is refused naming the flag"; else no "refusal text: $( head -2 "$DIR/err" )"; fi +fi +if "$BIN" "$FIX" --no-cache --lego=BaseFormatter --max-tokens=50 >"$DIR/out" 2>"$DIR/err" +then + if grep -q 'not read by --lego' "$DIR/err" && grep -q 'transitive="3"' "$DIR/out"; then ok "--max-tokens without a page is still the not-read notice, full answer"; else no "--max-tokens notice changed: $( head -2 "$DIR/err" )"; fi +else + no "--lego --max-tokens (no page) now refuses: $( head -2 "$DIR/err" )" +fi + +echo "=== (S1) MCP lego pages the same way ===" +mcp(){ printf '%s\n' "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"lego\",\"arguments\":{\"path\":\"$FIX\",\"type\":\"BaseFormatter\"$1}}}" | "$BIN" --mcp 2>/dev/null; } +M="$( mcp ',"limit":2' )" +if printf '%s' "$M" | grep -q 'transitive_shown=\\"2\\" has_more=\\"1\\"'; then ok "MCP limit:2 cuts the deeper rows the same way"; else no "MCP limit:2: $( printf '%s' "$M" | head -c 300 )"; fi +M="$( mcp '' )" +if printf '%s' "$M" | grep -q 'transitive=\\"3\\"' && ! printf '%s' "$M" | grep -q 'has_more'; then ok "MCP without a window: the whole closure"; else no "MCP default: $( printf '%s' "$M" | head -c 300 )"; fi +M="$( mcp ',"offset":-1' )" +if printf '%s' "$M" | grep -q '"error"'; then ok "MCP offset:-1 is refused"; else no "MCP offset:-1 accepted: $( printf '%s' "$M" | head -c 300 )"; fi + +echo "=== (M1) a Ruby module mixed in is an implements edge: include / extend ===" +if lego DSL +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 ' Pong) lists each type once and never the interface itself"; else no "Ping cycle rows: $( printf '%s\n' "$OUT" | grep '^' && has ''; then ok "BinOp lists Add (var, single-quoted) and Sub (const, double-quoted)"; else no "BinOp rows: $( printf '%s\n' "$OUT" | grep '^/dev/null 2>"$DIR/err" + then + no "--lego=$t resolved — the near miss became a class" + elif grep -q -- "--lego type not found: $t" "$DIR/err" + then + ok "--lego=$t is not a type (near miss)" + else + no "--lego=$t failed some other way: $( head -2 "$DIR/err" )" + fi +done +if lego TNode +then + if has ''; then ok "a TypeScript factory (with a type argument) lists under TNode"; else no "TNode rows: $( printf '%s\n' "$OUT" | grep '^/dev/null 2>"$DIR/err" + then + no "--lego=$t resolved — '$t' (no class receiver, no class body) became a class" + elif grep -q -- "--lego type not found: $t" "$DIR/err" + then + ok "--lego=$t is not a type (no class receiver in the file, no class body)" + else + no "--lego=$t failed some other way: $( head -2 "$DIR/err" )" + fi +done +if lego JBody +then + ok "--lego=JBody resolves: an imported receiver handed a class body (a method) is a class" +fi + +echo "=== (NS) fix 1: the closure walks PROVEN extends edges only — namesakes in every by-name language ===" +# nsarm SEL TRUE_ROW FALSE_ROW VIA LABEL — the true subtype is a plain depth-2 row via VIA, the namesake's row is absent +nsarm(){ + if lego "$1" + then + if printf '%s\n' "$OUT" | grep ""; then ok "$5: --lego=$1 lists $2 via=\"$4\" depth=\"2\", unhedged"; else no "$5: --lego=$1 true row: $( printf '%s\n' "$OUT" | grep "'; then ok "JavaScript: the class-body factory JBody is a proven depth-2 row"; else no "JBody row: $( printf '%s\n' "$OUT" | grep ''; then ok "C++: a qualified base ca::CMid is proven through its namespace"; else no "CQual row: $( printf '%s\n' "$OUT" | grep ''; then ok "Ruby: RTrue (RA::RMid) is a proven depth-2 row"; else no "Ruby RTrue: $( printf '%s\n' "$OUT" | grep ''; then ok "Python: a package re-export proves PExpLeaf (depth 2, unhedged)"; else no "PExpLeaf row: $( printf '%s\n' "$OUT" | grep ''; then ok "a name-only edge's row rides hedged: PStar base_by=\"name\""; else no "PStar row: $( printf '%s\n' "$OUT" | grep '"$DIR/full" 2>"$DIR/err" +then + if grep -q 'base_by=\\"name\\" on a deeper row' "$DIR/full" || grep -q 'base_by="name" on a deeper row' "$DIR/full"; then ok "the full legend defines base_by= and transitive_other_base= where they ride"; else no "full legend lacks the hedge clause"; fi +else + no "--lego=PBase --legend=full exited non-zero: $( head -2 "$DIR/err" )" +fi +if "$BIN" "$FIX" --no-cache --lego=BaseFormatter --legend=full >"$DIR/full" 2>"$DIR/err" && ! grep -q 'base_by' "$DIR/full"; then ok "an answer with no hedged row carries no hedge clause"; else no "the hedge clause rides an answer without a hedged row"; fi + +echo "=== (NS) Python base expressions: a dotted base is ONE base; a type argument or a keyword is none ===" +if lego POuter +then + if iface | grep -q 'implementors="0"'; then ok "class PInner(POuter.PChanged) is no subtype of POuter"; else no "POuter: $( iface ) $( printf '%s\n' "$OUT" | grep '^'; then ok "$3: --lego=$1 marks $2 unwalked=\"1\""; else no "$3: $2 row: $( printf '%s\n' "$OUT" | grep " counts it (transitive_unwalked=\"1\")"; else no "$3: no transitive_unwalked: $( iface )"; fi + if iface | grep -q "unwalked_next=\"--lego=[^\"]*:$2\""; then ok "$3: unwalked_next= names $2's own --lego"; else no "$3: unwalked_next: $( iface )"; fi + fi +} +uwarm UwKRoot UwKMid "Kotlin (import; the leaf in the same file)" +uwarm UwCRoot UwCMid "C# (using; the leaf in the same file)" +uwarm UwJsRoot UwJsMid "JavaScript (ES import of a CommonJS base)" +if lego UwJRoot +then + if iface | grep -q 'transitive_unwalked="2" unwalked_next="--lego=uw/java/a/UwJMid.java:UwJMid"'; then ok "Java: two unwalked direct rows counted, the first named (UwJMid)"; else no "Java UwJRoot: $( iface )"; fi + if printf '%s\n' "$OUT" | grep ''; then ok "Java: the second unwalked row UwJMid2 is marked too"; else no "UwJMid2 row: $( printf '%s\n' "$OUT" | grep '' && iface | grep -q 'transitive="1"'; then ok "…and its subtype is walked (transitive=\"1\" beside the unwalked count)"; else no "UwJNearLeaf: $( iface )"; fi + if has ' lists its subtype +if lego uw/java/a/UwJMid.java:UwJMid +then + if has '"$DIR/full" 2>"$DIR/err" +then + if grep -q 'ripwire lego: transitive_unwalked=N' "$DIR/full"; then ok "the full legend defines transitive_unwalked= where it rides"; else no "full legend lacks the unwalked clause"; fi +else + no "--lego=UwJRoot --legend=full exited non-zero: $( head -2 "$DIR/err" )" +fi +if "$BIN" "$FIX" --no-cache --lego=UwKRoot --legend=compact >"$DIR/compact" 2>"$DIR/err" +then + if grep -q 'transitive_unwalked=N: ' "$DIR/compact" && grep -q 'unwalked_next=: ' "$DIR/compact" && grep -q 'unwalked=1: ' "$DIR/compact"; then ok "the compact legend reads transitive_unwalked=, unwalked_next= and unwalked="; else no "compact legend lacks the unwalked terms: $( head -c 600 "$DIR/compact" )"; fi +else + no "--lego=UwKRoot --legend=compact exited non-zero: $( head -2 "$DIR/err" )" +fi +if "$BIN" "$FIX" --no-cache --lego=BaseFormatter --legend=full >"$DIR/full" 2>"$DIR/err" && ! grep -q 'unwalked' "$DIR/full"; then ok "an answer with no unwalked row carries no unwalked clause"; else no "the unwalked clause rides an answer without an unwalked row"; fi +M="$( printf '%s\n' "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"lego\",\"arguments\":{\"path\":\"$FIX\",\"type\":\"UwCRoot\"}}}" | "$BIN" --mcp 2>/dev/null )" +if printf '%s' "$M" | grep -q 'transitive_unwalked=\\"1\\" unwalked_next=\\"--lego=uw/cs/UwCMid.cs:UwCMid\\"' && printf '%s' "$M" | grep -q 'unwalked=\\"1\\"/>'; then ok "MCP lego carries the same count, next and row mark"; else no "MCP lego UwCRoot: $( printf '%s' "$M" | head -c 400 )"; fi + +echo "=== (M3) the focus is a CODE definition whenever one matched ===" +if lego Loader +then + if iface | grep -q 'p="js/loader.js"'; then ok "--lego=Loader picks js/loader.js over the docs/api.md heading"; else no "--lego=Loader picked: $( iface )"; fi + if has '"$DIR/around" 2>"$DIR/err" +then + if grep -o ']*n="Loader"[^>]*>' "$DIR/around" | grep -q 'js/loader.js\|t="cls"'; then ok "--around=Loader centres on the class (the same resolver)"; else no "--around=Loader rows: $( grep -o ']*n="Loader"[^>]*>' "$DIR/around" | head -2 )"; fi +else + no "--around=Loader exited non-zero: $( head -2 "$DIR/err" )" +fi + +echo "=== well-formed ===" +if command -v xmllint >/dev/null 2>&1 +then + for a in "--lego=BaseFormatter" "--lego=BaseFormatter --legend=full" "--lego=BaseFormatter --limit=1 --legend=full" "--lego=Top --legend=full" "--lego=PBase --legend=full" "--lego=JRoot --legend=full" "--lego=UwJRoot --legend=full" "--lego=UwJRoot --limit=1 --legend=full" + do + # shellcheck disable=SC2086 + if "$BIN" "$FIX" --no-cache $a 2>/dev/null | xmllint --noout - 2>/dev/null; then ok "xml well-formed: $a"; else no "xml malformed: $a"; fi + done +else + echo " SKIP xmllint absent" +fi + +[ "$fail" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" +exit "$fail" diff --git a/test/legotransitivefix/js/docs/api.md b/test/legotransitivefix/js/docs/api.md new file mode 100644 index 000000000..f0679e202 --- /dev/null +++ b/test/legotransitivefix/js/docs/api.md @@ -0,0 +1,9 @@ +# API + +## Loader + +A loader takes a template name and loads it. + +## Guide + +Only a heading: no code defines it. diff --git a/test/legotransitivefix/js/loader.js b/test/legotransitivefix/js/loader.js new file mode 100644 index 000000000..3b0a90190 --- /dev/null +++ b/test/legotransitivefix/js/loader.js @@ -0,0 +1,4 @@ +const { Obj } = require('./object'); +module.exports = class Loader extends Obj { + resolve(from, to) { return to; } +}; diff --git a/test/legotransitivefix/js/node-loaders.js b/test/legotransitivefix/js/node-loaders.js new file mode 100644 index 000000000..698251ac2 --- /dev/null +++ b/test/legotransitivefix/js/node-loaders.js @@ -0,0 +1,5 @@ +const Loader = require('./loader'); +class FileSystemLoader extends Loader { + getSource(name) { return null; } +} +module.exports = { FileSystemLoader }; diff --git a/test/legotransitivefix/js/nodes.js b/test/legotransitivefix/js/nodes.js new file mode 100644 index 000000000..2855c5a2f --- /dev/null +++ b/test/legotransitivefix/js/nodes.js @@ -0,0 +1,19 @@ +// The nunjucks node shape: a class FACTORY call names the class it makes. +const { Obj } = require('./object'); +class Node extends Obj { + init(lineno) { this.lineno = lineno; } +} +const Value = Node.extend('Value', { fields: ['value'] }); +const BinOp = Node.extend('BinOp', { fields: ['left', 'right'] }); +var Add = BinOp.extend('Add'); +const Sub = BinOp.extend("Sub"); +// near misses: none of these is a class +const settings = Node.extend({}, { a: 1 }); +const Renamed = Node.extend('Other'); +const Merged = Node.merge('Merged'); +const Spread = Node.extend(...parts); +function local() { + const Inner = Node.extend('Inner'); + return Inner; +} +module.exports = { Node, Value, BinOp, Add, Sub, settings, Renamed, Merged, Spread, local }; diff --git a/test/legotransitivefix/js/object.js b/test/legotransitivefix/js/object.js new file mode 100644 index 000000000..a9d398f59 --- /dev/null +++ b/test/legotransitivefix/js/object.js @@ -0,0 +1,6 @@ +class Obj { + static extend(name, props) { + return class extends this {}; + } +} +module.exports = { Obj }; diff --git a/test/legotransitivefix/ns/cpp/x.cpp b/test/legotransitivefix/ns/cpp/x.cpp new file mode 100644 index 000000000..e9fbf01bd --- /dev/null +++ b/test/legotransitivefix/ns/cpp/x.cpp @@ -0,0 +1,12 @@ +namespace ca { +struct CBase { virtual void run() = 0; }; +struct CMid : CBase { void run() override {} }; +} +namespace cb { +struct CMid { }; +struct CLeaf : CMid { }; +} +namespace ca { +struct CTrue : CMid { }; +struct CQual : ca::CMid { }; +} diff --git a/test/legotransitivefix/ns/java/a/JvBase.java b/test/legotransitivefix/ns/java/a/JvBase.java new file mode 100644 index 000000000..fa6457f6a --- /dev/null +++ b/test/legotransitivefix/ns/java/a/JvBase.java @@ -0,0 +1,2 @@ +package a; +public interface JvBase { void run(); } diff --git a/test/legotransitivefix/ns/java/a/JvMid.java b/test/legotransitivefix/ns/java/a/JvMid.java new file mode 100644 index 000000000..a2f3364fe --- /dev/null +++ b/test/legotransitivefix/ns/java/a/JvMid.java @@ -0,0 +1,2 @@ +package a; +public class JvMid implements JvBase { public void run() {} } diff --git a/test/legotransitivefix/ns/java/a/JvTrue.java b/test/legotransitivefix/ns/java/a/JvTrue.java new file mode 100644 index 000000000..fc366e2bb --- /dev/null +++ b/test/legotransitivefix/ns/java/a/JvTrue.java @@ -0,0 +1,2 @@ +package a; +public class JvTrue extends JvMid { } diff --git a/test/legotransitivefix/ns/java/b/JvLeaf.java b/test/legotransitivefix/ns/java/b/JvLeaf.java new file mode 100644 index 000000000..b922473d7 --- /dev/null +++ b/test/legotransitivefix/ns/java/b/JvLeaf.java @@ -0,0 +1,2 @@ +package b; +public class JvLeaf extends JvMid { } diff --git a/test/legotransitivefix/ns/java/b/JvMid.java b/test/legotransitivefix/ns/java/b/JvMid.java new file mode 100644 index 000000000..7336c1f63 --- /dev/null +++ b/test/legotransitivefix/ns/java/b/JvMid.java @@ -0,0 +1,2 @@ +package b; +public class JvMid { } diff --git a/test/legotransitivefix/ns/js/ja.js b/test/legotransitivefix/ns/js/ja.js new file mode 100644 index 000000000..5f4a4ac65 --- /dev/null +++ b/test/legotransitivefix/ns/js/ja.js @@ -0,0 +1,7 @@ +class JRoot { + run() {} +} +class JNode extends JRoot { + static extend(name, body) { return class extends this {}; } +} +module.exports = { JRoot, JNode }; diff --git a/test/legotransitivefix/ns/js/jb.js b/test/legotransitivefix/ns/js/jb.js new file mode 100644 index 000000000..36e174089 --- /dev/null +++ b/test/legotransitivefix/ns/js/jb.js @@ -0,0 +1,7 @@ +// Unrelated namesake JNode: does NOT extend JRoot. +class JNode { + static extend(name) { return class extends this {}; } +} +const JLeaf = JNode.extend('JLeaf'); +class JLeaf2 extends JNode {} +module.exports = { JNode, JLeaf, JLeaf2 }; diff --git a/test/legotransitivefix/ns/js/jc.js b/test/legotransitivefix/ns/js/jc.js new file mode 100644 index 000000000..ea99404d7 --- /dev/null +++ b/test/legotransitivefix/ns/js/jc.js @@ -0,0 +1,5 @@ +const { JNode } = require('./ja'); +class JTrue2 extends JNode {} +const JTrue = JNode.extend('JTrue'); // the receiver is imported: the parser cannot show it is a class +const JBody = JNode.extend('JBody', { render() { return 1; } }); // a class BODY: the parser shows the class +module.exports = { JTrue2, JTrue, JBody }; diff --git a/test/legotransitivefix/ns/js/jlog.js b/test/legotransitivefix/ns/js/jlog.js new file mode 100644 index 000000000..41b680ccf --- /dev/null +++ b/test/legotransitivefix/ns/js/jlog.js @@ -0,0 +1,9 @@ +// The `debug` package idiom: extend() derives a sub-NAMESPACE logger, not a class. +const createDebug = require('debug'); +const _ = require('lodash'); +const log = createDebug('app'); +const error = log.extend('error'); +const verbose = log.extend('verbose'); +const merged = _.extend('merged', { a: 1 }); +function run() { error('boom'); verbose(merged); } +module.exports = { run }; diff --git a/test/legotransitivefix/ns/py/ns_pa.py b/test/legotransitivefix/ns/py/ns_pa.py new file mode 100644 index 000000000..2a6c26a6b --- /dev/null +++ b/test/legotransitivefix/ns/py/ns_pa.py @@ -0,0 +1,16 @@ +# fix 1 namesakes: PMid here extends PBase; ns_pb.py holds an UNRELATED PMid with a PLeaf below it. +class PBase: + def run(self): + raise NotImplementedError + + +class PMid(PBase): + pass + + +class PBase2: + pass + + +class PMeta(type): + pass diff --git a/test/legotransitivefix/ns/py/ns_pb.py b/test/legotransitivefix/ns/py/ns_pb.py new file mode 100644 index 000000000..62dee0f90 --- /dev/null +++ b/test/legotransitivefix/ns/py/ns_pb.py @@ -0,0 +1,10 @@ +class PMid: + """Unrelated namesake: does NOT extend PBase.""" + + +class PLeaf(PMid): + pass + + +class PExp: + """Unrelated namesake of pkg._impl.PExp.""" diff --git a/test/legotransitivefix/ns/py/ns_pc.py b/test/legotransitivefix/ns/py/ns_pc.py new file mode 100644 index 000000000..c8712156c --- /dev/null +++ b/test/legotransitivefix/ns/py/ns_pc.py @@ -0,0 +1,32 @@ +from typing import Generic + +from ns_pa import PBase, PBase2, PMeta, PMid + + +class PTrue(PMid): + pass + + +class POuter: + class PChanged: + pass + + +class PInner(POuter.PChanged): + pass + + +class PKey: + pass + + +class PBox(Generic[PKey]): + pass + + +class PSub(PBase2[int]): + pass + + +class PWithMeta(PBase, metaclass=PMeta): + pass diff --git a/test/legotransitivefix/ns/py/ns_pd.py b/test/legotransitivefix/ns/py/ns_pd.py new file mode 100644 index 000000000..45c26e74b --- /dev/null +++ b/test/legotransitivefix/ns/py/ns_pd.py @@ -0,0 +1,5 @@ +from pkg import PExp + + +class PExpLeaf(PExp): + pass diff --git a/test/legotransitivefix/ns/py/ns_pe.py b/test/legotransitivefix/ns/py/ns_pe.py new file mode 100644 index 000000000..45d72436a --- /dev/null +++ b/test/legotransitivefix/ns/py/ns_pe.py @@ -0,0 +1,5 @@ +from ns_pa import * + + +class PStar(PMid): + """A star import binds no name this tool reads: which PMid is unproven (hedged, never asserted).""" diff --git a/test/legotransitivefix/ns/py/pkg/__init__.py b/test/legotransitivefix/ns/py/pkg/__init__.py new file mode 100644 index 000000000..95a3f3f89 --- /dev/null +++ b/test/legotransitivefix/ns/py/pkg/__init__.py @@ -0,0 +1 @@ +from pkg._impl import PExp diff --git a/test/legotransitivefix/ns/py/pkg/_impl.py b/test/legotransitivefix/ns/py/pkg/_impl.py new file mode 100644 index 000000000..b4e149dbd --- /dev/null +++ b/test/legotransitivefix/ns/py/pkg/_impl.py @@ -0,0 +1,6 @@ +class PExpBase: + pass + + +class PExp(PExpBase): + pass diff --git a/test/legotransitivefix/ns/rb/ra.rb b/test/legotransitivefix/ns/rb/ra.rb new file mode 100644 index 000000000..bbed2d06b --- /dev/null +++ b/test/legotransitivefix/ns/rb/ra.rb @@ -0,0 +1,8 @@ +module RA + class RTask + def invoke; end + end + + class RMid < RTask + end +end diff --git a/test/legotransitivefix/ns/rb/rb.rb b/test/legotransitivefix/ns/rb/rb.rb new file mode 100644 index 000000000..1b555a0ef --- /dev/null +++ b/test/legotransitivefix/ns/rb/rb.rb @@ -0,0 +1,7 @@ +module RB + class RMid + end + + class RLeaf < RMid + end +end diff --git a/test/legotransitivefix/ns/rb/rc.rb b/test/legotransitivefix/ns/rb/rc.rb new file mode 100644 index 000000000..59e5f11e4 --- /dev/null +++ b/test/legotransitivefix/ns/rb/rc.rb @@ -0,0 +1,4 @@ +module RA + class RTrue < RMid + end +end diff --git a/test/legotransitivefix/py/diamond.py b/test/legotransitivefix/py/diamond.py new file mode 100644 index 000000000..9f427a75a --- /dev/null +++ b/test/legotransitivefix/py/diamond.py @@ -0,0 +1,27 @@ +# A diamond: Bottom reaches Top through Left AND Right; it is ONE deeper row, at its shallowest depth. +class Top: + pass + + +class Left(Top): + pass + + +class Right(Top): + pass + + +class Bottom(Left, Right): + pass + + +class Deepest(Bottom): + pass + + +class Alone: + pass + + +class OnlyChild(Alone): + pass diff --git a/test/legotransitivefix/py/formatters.py b/test/legotransitivefix/py/formatters.py new file mode 100644 index 000000000..9be43337c --- /dev/null +++ b/test/legotransitivefix/py/formatters.py @@ -0,0 +1,24 @@ +# The flake8 formatter shape: Default/Pylint/FilenameOnly subclass SimpleFormatter, which subclasses BaseFormatter. +class BaseFormatter: + def format(self, error): + raise NotImplementedError + + +class SimpleFormatter(BaseFormatter): + error_format = None + + +class Default(SimpleFormatter): + error_format = "%(path)s" + + +class Pylint(SimpleFormatter): + error_format = "%(path)s:%(row)d" + + +class FilenameOnly(SimpleFormatter): + error_format = "%(path)s" + + +class Nothing(BaseFormatter): + pass diff --git a/test/legotransitivefix/rb/dsl_definition.rb b/test/legotransitivefix/rb/dsl_definition.rb new file mode 100644 index 000000000..50487f088 --- /dev/null +++ b/test/legotransitivefix/rb/dsl_definition.rb @@ -0,0 +1,8 @@ +module Rake + module DSL + def task(*args) + end + end +end + +self.extend Rake::DSL diff --git a/test/legotransitivefix/rb/task.rb b/test/legotransitivefix/rb/task.rb new file mode 100644 index 000000000..cb995a5a6 --- /dev/null +++ b/test/legotransitivefix/rb/task.rb @@ -0,0 +1,18 @@ +module Rake + class Task + def invoke + end + end + + class FileTask < Task + def needed? + true + end + end + + class MultiTask < Task + end + + class FileCreationTask < FileTask + end +end diff --git a/test/legotransitivefix/rb/tasklib.rb b/test/legotransitivefix/rb/tasklib.rb new file mode 100644 index 000000000..526c12800 --- /dev/null +++ b/test/legotransitivefix/rb/tasklib.rb @@ -0,0 +1,32 @@ +module Rake + class TaskLib + include Rake::DSL + end + + class MakefileLoader + include Rake::DSL + + def load(fn) + include Helpers + end + end + + class Singleton + extend Rake::DSL + end + + module Helpers + end + + module Ping + include Pong + end + + module Pong + include Ping + end + + class Pinged + include Ping + end +end diff --git a/test/legotransitivefix/ts/nodes.ts b/test/legotransitivefix/ts/nodes.ts new file mode 100644 index 000000000..9d95e3a43 --- /dev/null +++ b/test/legotransitivefix/ts/nodes.ts @@ -0,0 +1,6 @@ +// The same class factory in TypeScript (a type argument does not change the shape). +export class TNode { + static extend(name: string): T { return null as unknown as T; } +} +export const TBin = TNode.extend('TBin'); +export const TOther = TNode.extend('Mismatch'); diff --git a/test/legotransitivefix/ts/view.tsx b/test/legotransitivefix/ts/view.tsx new file mode 100644 index 000000000..1a464bc26 --- /dev/null +++ b/test/legotransitivefix/ts/view.tsx @@ -0,0 +1,3 @@ +import { TNode } from './nodes'; +export const TView = TNode.extend('TView'); +export const Panel = () =>
; diff --git a/test/legotransitivefix/uw/cs/UwCMid.cs b/test/legotransitivefix/uw/cs/UwCMid.cs new file mode 100644 index 000000000..07d03fcbb --- /dev/null +++ b/test/legotransitivefix/uw/cs/UwCMid.cs @@ -0,0 +1,2 @@ +using UwR; +namespace UwA { public class UwCMid : UwCRoot {} public class UwCLeaf : UwCMid {} } diff --git a/test/legotransitivefix/uw/cs/UwCRoot.cs b/test/legotransitivefix/uw/cs/UwCRoot.cs new file mode 100644 index 000000000..52e41af2e --- /dev/null +++ b/test/legotransitivefix/uw/cs/UwCRoot.cs @@ -0,0 +1 @@ +namespace UwR { public class UwCRoot {} } diff --git a/test/legotransitivefix/uw/java/a/UwJAlone.java b/test/legotransitivefix/uw/java/a/UwJAlone.java new file mode 100644 index 000000000..dfea15ba0 --- /dev/null +++ b/test/legotransitivefix/uw/java/a/UwJAlone.java @@ -0,0 +1,3 @@ +package a; +import r.UwJRoot; +public class UwJAlone extends UwJRoot {} diff --git a/test/legotransitivefix/uw/java/a/UwJLeaf.java b/test/legotransitivefix/uw/java/a/UwJLeaf.java new file mode 100644 index 000000000..779522b1d --- /dev/null +++ b/test/legotransitivefix/uw/java/a/UwJLeaf.java @@ -0,0 +1,2 @@ +package a; +public class UwJLeaf extends UwJMid {} diff --git a/test/legotransitivefix/uw/java/a/UwJLeaf2.java b/test/legotransitivefix/uw/java/a/UwJLeaf2.java new file mode 100644 index 000000000..7998d2017 --- /dev/null +++ b/test/legotransitivefix/uw/java/a/UwJLeaf2.java @@ -0,0 +1,2 @@ +package a; +public class UwJLeaf2 extends UwJMid2 {} diff --git a/test/legotransitivefix/uw/java/a/UwJMid.java b/test/legotransitivefix/uw/java/a/UwJMid.java new file mode 100644 index 000000000..02a3325f5 --- /dev/null +++ b/test/legotransitivefix/uw/java/a/UwJMid.java @@ -0,0 +1,3 @@ +package a; +import r.UwJRoot; +public class UwJMid extends UwJRoot {} diff --git a/test/legotransitivefix/uw/java/a/UwJMid2.java b/test/legotransitivefix/uw/java/a/UwJMid2.java new file mode 100644 index 000000000..ca392a4ef --- /dev/null +++ b/test/legotransitivefix/uw/java/a/UwJMid2.java @@ -0,0 +1,3 @@ +package a; +import r.UwJRoot; +public class UwJMid2 extends UwJRoot {} diff --git a/test/legotransitivefix/uw/java/r/UwJNear.java b/test/legotransitivefix/uw/java/r/UwJNear.java new file mode 100644 index 000000000..5b10074d6 --- /dev/null +++ b/test/legotransitivefix/uw/java/r/UwJNear.java @@ -0,0 +1,2 @@ +package r; +public class UwJNear extends UwJRoot {} diff --git a/test/legotransitivefix/uw/java/r/UwJNearLeaf.java b/test/legotransitivefix/uw/java/r/UwJNearLeaf.java new file mode 100644 index 000000000..c08cd3eb5 --- /dev/null +++ b/test/legotransitivefix/uw/java/r/UwJNearLeaf.java @@ -0,0 +1,2 @@ +package r; +public class UwJNearLeaf extends UwJNear {} diff --git a/test/legotransitivefix/uw/java/r/UwJRoot.java b/test/legotransitivefix/uw/java/r/UwJRoot.java new file mode 100644 index 000000000..5a72ef1cd --- /dev/null +++ b/test/legotransitivefix/uw/java/r/UwJRoot.java @@ -0,0 +1,2 @@ +package r; +public class UwJRoot {} diff --git a/test/legotransitivefix/uw/js/a/uwleaf.js b/test/legotransitivefix/uw/js/a/uwleaf.js new file mode 100644 index 000000000..867b9d595 --- /dev/null +++ b/test/legotransitivefix/uw/js/a/uwleaf.js @@ -0,0 +1,2 @@ +import { UwJsMid } from './uwmid.js'; +export class UwJsLeaf extends UwJsMid {} diff --git a/test/legotransitivefix/uw/js/a/uwmid.js b/test/legotransitivefix/uw/js/a/uwmid.js new file mode 100644 index 000000000..ceea302b1 --- /dev/null +++ b/test/legotransitivefix/uw/js/a/uwmid.js @@ -0,0 +1,2 @@ +import { UwJsRoot } from '../uwroot.js'; +export class UwJsMid extends UwJsRoot {} diff --git a/test/legotransitivefix/uw/js/uwroot.js b/test/legotransitivefix/uw/js/uwroot.js new file mode 100644 index 000000000..f1a67dec1 --- /dev/null +++ b/test/legotransitivefix/uw/js/uwroot.js @@ -0,0 +1,2 @@ +class UwJsRoot { run() {} } +module.exports = { UwJsRoot }; diff --git a/test/legotransitivefix/uw/kt/a/UwKMid.kt b/test/legotransitivefix/uw/kt/a/UwKMid.kt new file mode 100644 index 000000000..63b991aab --- /dev/null +++ b/test/legotransitivefix/uw/kt/a/UwKMid.kt @@ -0,0 +1,4 @@ +package a +import r.UwKRoot +open class UwKMid : UwKRoot() +class UwKLeaf : UwKMid() diff --git a/test/legotransitivefix/uw/kt/r/UwKRoot.kt b/test/legotransitivefix/uw/kt/r/UwKRoot.kt new file mode 100644 index 000000000..a70f50dfa --- /dev/null +++ b/test/legotransitivefix/uw/kt/r/UwKRoot.kt @@ -0,0 +1,2 @@ +package r +open class UwKRoot diff --git a/test/listingpagingcheck.sh b/test/listingpagingcheck.sh index 5a352bc84..99f623563 100755 --- a/test/listingpagingcheck.sh +++ b/test/listingpagingcheck.sh @@ -506,5 +506,244 @@ print( " PASS (F) mutation: a drift= that followed the window IS caught by the PY [ $? = 0 ] || fail=1 +# ── ARM B of the paging spec (upstream issue #294, corrected scope): the BUDGETED-bundle continuation ── +# `--for=TASK --limit=N` already pages (the file-grain widening page, forwidencheck owns it). What does +# NOT exist yet: the byte-budgeted bundle (--token-budget) under --offset/--limit — today the pair is +# refused, so an agent whose bundle was cut can only re-run blind. This arm is the SEAM contract for the +# budget-mode continuation once it exists: --offset=N --limit=M must be the EXACT continuation of the +# previous page over the ranked candidate set — page[0:3] + page[3:6] == page[0:6] in candidates, rank +# order, no gap, no overlap (the same contract arm (A) holds for rows, here measured on the sig rows +# of the paged budgeted bundle). RED on the pre-feature binary is the intended state: gate written +# BEFORE the code, red vs the pre-change binary — today both paged runs refuse and this records that. +echo "=== (G) budgeted-bundle candidate continuation: page[0:3] + page[3:6] == page[0:6] ===" +# at a NO-TRIM budget: the page's own legend rides the budget, so a small budget trims within every +# page and the pure-window identity (page[0:3]+page[3:6] == page[0:6]) cannot hold — the identity is +# the WINDOW's; the trim path is the (G4) trimming walk below (the #362 review's ask). +# on src/ (cliff 40 > 6: the single-tier clamp never fires; the repo ROOT clamps every head +# window to the 1-row cliff — a correct page, but not one this pure-window identity can test) +run "$ROOT/src" g_budget_p0 --for="rank symbols" --token-budget=100000 --limit=3 --offset=0 +run "$ROOT/src" g_budget_p3 --for="rank symbols" --token-budget=100000 --limit=3 --offset=3 +run "$ROOT/src" g_budget_p6 --for="rank symbols" --token-budget=100000 --limit=6 --offset=0 + +python3 - "$TMP" <<'PY' +import os, sys, xml.etree.ElementTree as ET + +tmp = sys.argv[1] +problems = [] + +def sigs(name): + path = os.path.join(tmp, name) + if not os.path.exists(path) or os.path.getsize(path) == 0: + problems.append(f"{name}: no output — the paged budgeted bundle does not exist yet " + "(today --token-budget refuses --offset/--limit; that refusal IS this arm's recorded red)") + return None + raw = open(path, encoding="utf-8", errors="replace").read() + try: + root = ET.fromstring(raw) + except ET.ParseError as e: + problems.append(f"{name}: not parseable XML ({e})"); return None + q = [k for k in ("shown", "total", "capped", "has_more", "next_offset") if k not in root.attrib] + if q: + problems.append(f'{name}: paged budgeted root lacks the quintet (missing: {", ".join(q)})'); return None + # the bundle's sig rows are elements (serialize.h's flat lens path) — the arm's first + # cut guessed /, matched nothing, and produced a VACUOUS green (0+0 == 0); a seam that + # compares no rows proves nothing, so assert the rows EXIST too. + rows = [ el.attrib.get("n") or el.attrib.get("name") or el.text or "" for el in root.iter() if el.tag == "d" ] + if not rows: + problems.append(f"{name}: the page carries no rows — the seam comparison below would be vacuous") + return rows + +p0 = sigs("g_budget_p0"); p3 = sigs("g_budget_p3"); p6 = sigs("g_budget_p6") +if p0 is not None and p3 is not None and p6 is not None: + joined, whole = p0 + p3, p6 + if len(joined) != len(whole): + problems.append(f"seam: page[0:3]+page[3:6] gives {len(joined)} rows, page[0:6] gives {len(whole)} — rows lost or duplicated across the seam") + else: + for i, (a, b) in enumerate(zip(joined, whole)): + if a != b: + problems.append(f"seam: row {i} disagrees across the seam (pages say {a!r}, whole says {b!r}) — not the exact continuation") + break + overlap = set(p0) & set(p3) + if overlap: + problems.append(f"seam: pages overlap on {sorted(overlap)[:3]} — a candidate served twice is a page that lies") + if not problems: + print(f" PASS (G) seam: {len(whole)} candidate rows, two pages == the whole, rank order, no overlap") + +# ARM B2 (issue #294 follow-up): the candidate page carries next= — the RESUMABLE ARGV, the same +# contract the file page's next= serves (forwidencheck arm (5) reproduces its page from it). The handle +# must name the offset the machine attribute next_offset= declares, so a client that pastes it walks the +# exact continuation. RED until the page grows the attribute (next_offset= alone is machine-only). +import re as _re +next_attr = None; next_offset_attr = None +for name in ("g_budget_p0", "g_budget_p3"): + path = os.path.join(tmp, name) + if not os.path.exists(path) or os.path.getsize(path) == 0: continue + raw = open(path, encoding="utf-8", errors="replace").read() + # scope to the ROOT OPENING TAG (rows carry their own next= expand hints — the arm's first cut + # read the row's handle and mis-filed it as the page's) + o = raw.find( "", o ) + opentag = raw[ o : oend ] if o >= 0 and oend > o else "" + m = _re.search(r' next="([^"]*)"', opentag); n = _re.search(r' next_offset="(\d+)"', opentag) + t = _re.search(r' next_tier="([^"]*)"', opentag) # ruling suggestion 2: the tier the NEXT page carries + if m: next_attr = m.group(1) + if n: next_offset_attr = n.group(1) + if next_attr and next_offset_attr: + if ( "--offset=" + next_offset_attr ) not in next_attr: + problems.append(f"next= does not name the declared continuation: next={next_attr!r} vs next_offset={next_offset_attr}") + else: + print(f" PASS (G2) next= names the exact continuation (--offset={next_offset_attr})") + break +if t and t.group(1) not in ("head", "below-cliff"): + problems.append(f"next_tier={t.group(1)!r} is not one of the two tier values") +if not t and next_attr: + problems.append("next= present but next_tier= absent — the ruling's suggestion 2 (the handle must make the next page's tier visible)") +if not next_attr: + problems.append("the candidate page root carries no next= — the resumable argv handle the file page's next= contract serves (next_offset= alone is machine-only)") + +# mutation: the seam shape can fail — an overlapping pair of fabricated pages must be DETECTED +mut_a, mut_b = ["a", "b", "c"], ["b", "c", "d"] +if len(set(mut_a) & set(mut_b)) != 2: + problems.append("mutation: an overlapping fabricated page pair was NOT detected — the seam arm cannot fail") +else: + print(" PASS (G) mutation: an overlapping fabricated page pair IS detected") + +for p in problems: print(" FAIL " + p) +sys.exit(1 if problems else 0) +PY +[ $? = 0 ] && ok '(G) budgeted-bundle pages are the exact continuation (candidates, rank order, no overlap)' \ + || no '(G) the budgeted-bundle continuation seam (upstream issue #294) does not hold — see FAIL lines above' + +# ── (G3) END-TO-END WALK (issue #294 ruling, suggestion 5): every page concatenated equals the full ── +# ranked candidate list — no gap, no overlap, same order — and the tier sequence is head* then +# below-cliff* (exactly one boundary). Walked at a budget large enough that no within-page trim fires, +# so the row NAMES are the ranked set itself. The un-paged bundle's served rows (top-40 of the same +# order, same budget) must be a PREFIX of the walk. +echo "=== (G3) end-to-end walk: all pages concatenated == the full ranked candidate list ===" +WALK_BUDGET=100000 +# on the big src/ corpus: enough candidates (878 for this query) that the walk spans multiple pages +# and crosses the tier boundary — the root corpus' 5 candidates fit one page and prove nothing about seams. +run "$ROOT/src" g3_bundle --for="rank symbols" --token-budget=$WALK_BUDGET +offset=0; page=0 +while [ "$page" -lt 12 ]; do + run "$ROOT/src" "g3_walk_$page" --for="rank symbols" --token-budget=$WALK_BUDGET --limit=200 --offset=$offset + shown="$( attr "$TMP/g3_walk_$page" sigs shown )"; has_more="$( attr "$TMP/g3_walk_$page" sigs has_more )" + nextoff="$( attr "$TMP/g3_walk_$page" sigs next_offset )"; tier="$( attr "$TMP/g3_walk_$page" sigs tier )" + total="$( attr "$TMP/g3_walk_$page" sigs total )" + [ -n "$shown" ] || { no "(G3) walk page $page produced no parseable root"; break; } + echo "$tier" >> "$TMP/g3_tiers.txt" + echo "$shown" >> "$TMP/g3_shown.txt" + # rows carry n= AFTER other attributes (", "", raw, flags=re.S ) # the legend DEFINES in prose: never count prose as rows +# candidate identity is the (file, line, name) TRIPLE — same-name definitions are distinct +# candidates even in ONE file (DYNMAP_DEFINE_RANK_32 sits at two lines of dynamic_map.hpp), +# so name or (file, name) alone false-flags them as seam overlap. n=/p=/l= may sit in any +# order on the tag — three sub-extracts per tag. +for tag in re.finditer( r"]*>", raw ): + t = tag.group( 0 ) + n = re.search( r"\sn=\"([^\"]*)\"", t ) + p = re.search( r"\sp=\"([^\"]*)\"", t ) + l = re.search( r"\sl=\"([^\"]*)\"", t ) + print( ( p.group( 1 ) if p else "?" ) + "\x1f" + ( l.group( 1 ) if l else "?" ) + "\x1f" + ( n.group( 1 ) if n else "?" ) ) +' "$TMP/g3_walk_$page" >> "$TMP/g3_rows.txt" + page=$(( page + 1 )) + [ "$has_more" = "1" ] || break + offset="$nextoff" +done +walk_pages="$page" + +python3 - "$TMP" "$walk_pages" "$total" <<'PY' +import os, sys +tmp, pages, total = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]) +problems = [] +rows = [ l.rstrip("\n") for l in open(os.path.join(tmp, "g3_rows.txt")) if l.strip() ] +tiers = [ l.rstrip("\n") for l in open(os.path.join(tmp, "g3_tiers.txt")) if l.strip() ] +shown_sum = sum( int( x ) for x in open(os.path.join(tmp, "g3_shown.txt")) if x.strip() ) +dups = len(rows) - len(set(rows)) +if dups: + problems.append(f"walk: {dups} duplicate candidates across pages — a row served twice is a page that lies") +# the tiling measure is the pages' own shown= arithmetic (candidates SERVED); the ) is counted as served but emits no row, +# so shown-sum == total is the contract, and the row list tiles the named-def subset in order. +# the tile under pseudo-symbols: a candidate slot can be SERVED without printing a row (), +# so shown-sum <= total with the difference consumed by pseudo-slots; the coherence is rows-extracted +# == shown-counted plus no duplicates plus the prefix below. +if shown_sum > total: + problems.append(f"walk: shown sums to {shown_sum} vs total={total} — the pages over-serve the candidate set") +if len(rows) != shown_sum: + problems.append(f"walk: {len(rows)} rows extracted vs shown summing {shown_sum} — the count and the rows disagree") +# tier sequence: all head pages precede all below-cliff pages (exactly one boundary) +seen_below = False +for i, t in enumerate(tiers): + if t not in ("head", "below-cliff"): + problems.append(f"walk page {i}: tier={t!r} is not one of the two values"); break + if t == "below-cliff": seen_below = True + elif seen_below: + problems.append(f"walk page {i}: tier=head AFTER a below-cliff page — the boundary moved mid-walk"); break +# the un-paged bundle's served rows are a PREFIX of the walk (same order, same budget) +bp = os.path.join(tmp, "g3_bundle") +if os.path.exists(bp) and os.path.getsize(bp) > 0: + import re + raw = open(bp, encoding="utf-8", errors="replace").read() + raw = re.sub( r'', '', raw, flags=re.S ) # the legend's prose is not a row + bundle = [] + for m in re.finditer( r']*>', raw ): + t = m.group( 0 ) + n = re.search( r'\sn="([^"]*)"', t ) + pp = re.search( r'\sp="([^"]*)"', t ) + ll = re.search( r'\sl="([^"]*)"', t ) + bundle.append( ( pp.group(1) if pp else "?" ) + "\x1f" + ( ll.group(1) if ll else "?" ) + "\x1f" + ( n.group(1) if n else "?" ) ) + if rows[:len(bundle)] != bundle: + problems.append("the un-paged bundle's served rows are NOT a prefix of the walk — the page and the bundle answer different orders") + else: + print(f" PASS (G3) the un-paged bundle's {len(bundle)} rows are a prefix of the walk (same order)") +for p_ in problems: print(" FAIL " + p_) +print(f" .. (G3) walked {pages} pages, {len(rows)} rows, total={total}, tiers: {' -> '.join(tiers)}") +sys.exit(1 if problems else 0) +PY +[ $? = 0 ] && ok '(G3) every page concatenated == the full ranked candidate list (no gap, no overlap, one tier boundary)' \ + || no '(G3) the end-to-end walk (issue #294 ruling, suggestion 5) does not hold — see FAIL lines above' + +# ── (G4) THE TRIMMING WALK (#362 review, item 2): the same walk at a budget that TRIMS within ── +# every page, asserting NO DUPLICATE candidate across the seam. The resume point is the last +# PRINTED row's candidate index + 1 (a pseudo-symbol consumes a slot without printing a row), so +# a served-count arithmetic lands one short and the next page re-serves a row — the walk catches it. +echo "=== (G4) trimming walk: a budget that trims, no duplicate candidate across the seam ===" +: > "$TMP/g4_rows.txt" +offset=0; page=0 +while [ "$page" -lt 12 ]; do + run "$ROOT/src" "g4_walk_$page" --for="rank symbols" --token-budget=600 --limit=40 --offset=$offset + has_more="$( attr "$TMP/g4_walk_$page" sigs has_more )" + nextoff="$( attr "$TMP/g4_walk_$page" sigs next_offset )" + [ -n "$nextoff" ] || { no "(G4) walk page $page produced no parseable root"; break; } + python3 -c ' +import re, sys +raw = open( sys.argv[1], encoding="utf-8", errors="replace" ).read() +raw = re.sub( r"", "", raw, flags=re.S ) # the legend DEFINES in prose: never count prose as rows +for tag in re.finditer( r"]*>", raw ): + t = tag.group( 0 ) + n = re.search( r"\sn=\"([^\"]*)\"", t ) + p = re.search( r"\sp=\"([^\"]*)\"", t ) + l = re.search( r"\sl=\"([^\"]*)\"", t ) + print( ( p.group( 1 ) if p else "?" ) + "\x1f" + ( l.group( 1 ) if l else "?" ) + "\x1f" + ( n.group( 1 ) if n else "?" ) ) +' "$TMP/g4_walk_$page" >> "$TMP/g4_rows.txt" + page=$(( page + 1 )) + [ "$has_more" = "1" ] || break + offset="$nextoff" +done +TRIM_DUPS=$( python3 - "$TMP/g4_rows.txt" < "$MF/d$i/s$i.sh" + printf 'def ok(x):\n return x\nok("py %s")\n' "$i" > "$MF/d$i/p$i.py" +done +memoBad=0 +for i in 1 2 9 17 18; do + for f in "d$i/s$i.sh" "d$i/p$i.py"; do + out="$( "$BIN" "$MF" --no-cache --callers="$f:ok" 2>/dev/null )" + if printf '%s' "$out" | grep -q 'count="1"' && printf '%s' "$out" | grep -q "p=\"$f:1\""; then :; else + memoBad=1; no "(10) $f:ok should have exactly its own file's module scope as caller: $( printf '%s' "$out" | grep -o ']*>' | head -1 )" + fi + done +done +rm -rf "$MF" +if [ "$memoBad" -eq 0 ]; then ok "(10) 36 same-named defs over two languages: every file's module-scope call binds its own file's ok (10 files sampled)"; fi + [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" exit "$fail" diff --git a/test/mapinventorycheck.sh b/test/mapinventorycheck.sh new file mode 100755 index 000000000..0f627ed44 --- /dev/null +++ b/test/mapinventorycheck.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# mapinventorycheck.sh — the default map's inventory tier (): no indexed file is silently absent from an +# orient map, and the program entry points are named. +# +# WHY. The ranked map prints the top-k (200) symbols, which covers ~30-90 files of a few hundred. On the round-1 +# comparison table that cut was the whole orient miss class (K19): tmux input.c / key-bindings.c, hono's +# validator/, textual's drivers/ and command.py, htop's SignalsPanel.c — every one an indexed file ranked just below +# the cut, with NOTHING in the answer saying the file existed. Entry points are worse: `main` has no in-repo caller, +# so PageRank puts it at the very bottom (htop main #2548 of 3206, tmux main #3220 of 6361) and no re-rank of the +# map can surface it. The tier appends, after the ranked rows and without touching them: rows for program +# entries by name convention, and rows naming (code) or counting (tests/docs/examples/config) every file the +# ranked rows did not show, grouped by directory. +# +# Fixture test/mapinvfix: C, Python, Go, Rust, Java and TS files; a test-dir main, an examples main, a Python main +# with a real caller (NOT an entry), a cli main reached from __main__.py (NOT an entry: the __main__ scope is). +set -u + +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +FAILED=0 +fail() { printf ' FAIL %s\n' "$*"; FAILED=$(( FAILED + 1 )); } +ok() { printf ' PASS %s\n' "$*" || { FAILED=$(( FAILED + 1 )); printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +[ -x "$BIN" ] || { echo "mapinventorycheck: no binary at $BIN — build first"; exit 2; } +echo "mapinventorycheck: BIN=$BIN" +cd "$ROOT" || exit 2 +FIX=test/mapinvfix +[ -d "$FIX" ] || { echo "mapinventorycheck: fixture $FIX missing"; exit 2; } + +run() { "$BIN" "$FIX" --no-cache "$@" 2>"$TMP/err"; } +run --top-k=3 >"$TMP/map"; rc=$? +[ "$rc" -eq 0 ] && [ -s "$TMP/map" ] || { echo "mapinventorycheck: the map run failed (rc=$rc)"; cat "$TMP/err"; exit 1; } +run --top-k=3 --legend=full >"$TMP/full" +run --top-k=3 --json >"$TMP/json" +run >"$TMP/whole" # default top-k: every file shown +run --top-k=3 --max-tokens=400 >"$TMP/maxtok" +run --top-k=3 --expand=run >"$TMP/expand" +run --top-k=3 --for="where does main start" >"$TMP/for" +run --top-k=3 --stable >"$TMP/stable" +run --top-k=3 --no-inventory --json >"$TMP/noinvjson" +run --top-k=3 >"$TMP/map2" + +# The tier, isolated (one line of minified XML). +INV="$( grep -o '"; else fail "(A) no on a map whose top-k=3 dropped 15 of 18 files"; fi + +# (B) it comes AFTER the ranked rows and the rows are untouched: … +if grep -q ']*>.*' "$TMP/map"; then ok "(B) follows the last ranked group and closes the root"; else fail "(B) is not appended after the ranked rows"; fi + +# (C) entries: exactly the program entries by convention, sorted by path +ENTRIES="$( printf '%s' "$INV" | grep -o ''; then ok "(D) prog.c main → run"; else fail "(D) prog.c main does not list its callee run"; fi +if printf '%s' "$INV" | grep -q ']*/>'; then ok "(D2) the __main__.py scope → main"; else fail "(D2) the __main__.py entry does not list main"; fi + +# (E) a low-ranked code file is NAMED in its directory's row +if printf '%s' "$INV" | grep -q '"; then ok "(F) $d/ rolls up count-only"; else fail "(F) $d/ is not a count-only row"; fi +done + +# (G) arithmetic: listed + Σ n = the header's files=, and listed = the groups the map printed +FILES="$( grep -o '', text ) + shown = re.search( r'\bshown=(\d+)', hdr.group( 0 ) ) if hdr else None + total = re.search( r'\bsymbols=(\d+)', hdr.group( 0 ) ) if hdr else None keys = set(); cur = "" for m in re.finditer( r'<(f|s) ([^>]*)>', text ): attrs = dict( re.findall( r'([a-z_]+)="([^"]*)"', m.group( 2 ) ) ) if m.group( 1 ) == "f": cur = attrs.get( "p", "" ); continue keys.add( attrs.get( "id" ) or f'{cur}::{attrs.get("t")}::{attrs.get("n")}' ) - return ( int( est.group( 1 ) ) if est else -1 ), keys -bareEst, bare = rows( sys.argv[ 1 ] ) -budEst, bud = rows( sys.argv[ 2 ] ) + return ( int( est.group( 1 ) ) if est else -1 ), keys, ( int( shown.group( 1 ) ) if shown else -1 ), ( int( total.group( 1 ) ) if total else -1 ) +bareEst, bare, _, _ = rows( sys.argv[ 1 ] ) +budEst, bud, shown, total = rows( sys.argv[ 2 ] ) problems = [] -if len( bud ) < 20: problems.append( f"budgeted map has only {len(bud)} rows (presence guard)" ) -if not bud <= bare: problems.append( f"{len(bud - bare)} budgeted row(s) absent from the bare map — not a head, a different ranking" ) -if not 0 < budEst <= 4500: problems.append( f"budgeted est_tokens={budEst}, ceiling 4500" ) -if not bareEst > 4500: problems.append( f"bare est_tokens={bareEst} is already under the 4500 ceiling — the recommendation saves nothing" ) -print( ( "FAIL " + "; ".join( problems ) ) if problems else f"OK bare={bareEst} budgeted={budEst} rows={len(bud)}/{len(bare)}" ) +if len( bud ) < 20: problems.append( f"cut map has only {len(bud)} rows (presence guard)" ) +if not bud <= bare: problems.append( f"{len(bud - bare)} cut-map row(s) absent from the plain map — not a head, a different ranking" ) +if not 0 < budEst < bareEst: problems.append( f"cut est_tokens={budEst} is not below the plain map's {bareEst} — the budget cut nothing" ) +if not ( 0 <= shown < total ): problems.append( f"the cut map's header does not disclose the cut (shown={shown} symbols={total})" ) +print( ( "FAIL " + "; ".join( problems ) ) if problems else f"OK plain={bareEst} cut={budEst} rows={len(bud)}/{len(bare)} shown={shown} of {total}" ) PY )" - case "$verdict" in - OK*) ok "(G4) $budget_flag keeps the bare map's head under the ceiling (${verdict#OK })" ;; - *) no "(G4) $budget_flag does not keep the promise the start-here comment makes: ${verdict#FAIL }" ;; - esac -fi +case "$verdict" in + OK*) ok "(G4) --max-tokens=3000 keeps the plain map's head and discloses the cut (${verdict#OK })" ;; + *) no "(G4) --max-tokens=3000 does not keep the promise the orient comment makes: ${verdict#FAIL }" ;; +esac # ── (H) SUMMARY-LINE NUMBERS ───────────────────────────────────────────────────────────────────── # WHY THIS ARM EXISTS. On 2026-09-07 the lineage section's read "34 repositories, 67 papers diff --git a/test/readmeexamplecheck.sh b/test/readmeexamplecheck.sh index bed2674e4..2d94acdf5 100755 --- a/test/readmeexamplecheck.sh +++ b/test/readmeexamplecheck.sh @@ -28,10 +28,12 @@ live = open(sys.argv[2], encoding="utf-8").read() # and say so above the block; they are not asserted here because nothing can keep them true. LINE_SUFFIX = re.compile(r":\d+$") -rows = re.findall(r'', live) +# CALLSITE-AT (2026-10-09): a caller row may carry attributes after p= (sites_at=, tested=, via=); the claim compared is +# still WHICH callers exist and in WHICH FILE, so the row pattern reads n= and p= and tolerates what follows. +rows = re.findall(r']*/>', live) assert rows, "live callers command returned no rows" readme_rows = { ( n, LINE_SUFFIX.sub( "", p ) ) - for n, p in re.findall(r'', readme) } + for n, p in re.findall(r']*/>', readme) } missing = [] for name, path in rows: if ( name, LINE_SUFFIX.sub( "", path ) ) not in readme_rows: diff --git a/test/regression.sh b/test/regression.sh index babf92104..a48095816 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 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 layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck 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 rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck 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 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 [ -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/reportcheck.sh b/test/reportcheck.sh index fec148894..335587043 100755 --- a/test/reportcheck.sh +++ b/test/reportcheck.sh @@ -20,6 +20,9 @@ # - the report and the XML map AGREE on symbol count (self-consistency: the same graph, two renderings) # - a "God files" section names hub.h (the most-included header) # - determinism +# - 7/9: a depended-on test-layer file (test/lib/helper.sh) is counted on a layer=test line, not ranked as the +# god file, and its --deps godfiles row says layer="test"; 8: --seams leaves README -> docs/ links out (doc_links=) +# - 10: a file under test/ with a PRODUCTION dependent is not a test helper: it stays ranked, no layer="test" # # Usage: RIPWIRE_BIN=build/ripwire bash test/reportcheck.sh | RIPWIRE_BIN=asan/ripwire bash … # Exits non-zero on any failure. Does NOT edit regression.sh. @@ -86,5 +89,151 @@ printf '%s' "$REP" | grep -qi 'cycle' \ [ "$( run --report )" = "$( run --report )" ] \ && ok "--report deterministic (byte-identical run-to-run)" || no "--report non-deterministic" +# ── 7) a test-layer helper is not the god file (seams-doc-links, 2026-10-09) ──────────────────────────── +# Every gate `source`s one shell library, so on this repo test/lib/clean-env.sh (185 dependents, all tests) +# outranked src/model.h. Fixture: test/lib/helper.sh sourced by 4 test scripts; src/core.h included by 3 files; +# a near-miss dir named `testing/` (not a built-in layer name) whose helper is sourced by 2 scripts. +T="$TMP/layer"; mkdir -p "$T/src" "$T/test/lib" "$T/testing" +printf 'int core();\n' > "$T/src/core.h" +for f in a b c; do printf '#include "core.h"\nint %s() { return core(); }\n' "$f" > "$T/src/$f.cpp"; done +printf 'helper() { echo hi; }\n' > "$T/test/lib/helper.sh" +for n in 1 2 3 4; do printf '#!/usr/bin/env bash\n. "$ROOT/test/lib/helper.sh"\nhelper\n' > "$T/test/g${n}check.sh"; done +printf 'tool() { echo t; }\n' > "$T/testing/tool.sh" +for n in 1 2; do printf '#!/usr/bin/env bash\n. "$ROOT/testing/tool.sh"\ntool\n' > "$T/testing/u$n.sh"; done +LREP="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$T" --report --no-cache 2>/dev/null )"; lrc=$? +GOD="$( printf '%s\n' "$LREP" | sed -n '/^## God files/,/^$/p' )" +if [ "$lrc" != 0 ] || [ -z "$GOD" ]; then + no "7: --report on the layer fixture failed (rc=$lrc) or printed no God files section" +else + FIRST="$( printf '%s\n' "$GOD" | sed -n 2p )" + case "$FIRST" in + '- `src/core.h` — 3 dependents') ok "7a: the first god file is the source header src/core.h, not the test helper" ;; + *) no "7a: first god-file row is '$FIRST' (want src/core.h — 3 dependents)" ;; + esac + printf '%s\n' "$GOD" | grep -qxF -- '- layer=test: 1 more depended-on files under a test directory whose dependents are all test paths, by path, not ranked above; top `test/lib/helper.sh` — 4 dependents' \ + && ok "7b: the test helper is counted on the layer=test line (4 dependents), not dropped" \ + || no "7b: no layer=test line naming test/lib/helper.sh with 4 dependents: $GOD" + printf '%s\n' "$GOD" | grep -qxF -- '- `testing/tool.sh` — 2 dependents' \ + && ok "7c: near miss: testing/ is not a built-in layer, so its helper is still ranked" \ + || no "7c: testing/tool.sh missing from the ranked rows: $GOD" + printf '%s\n' "$GOD" | grep -c '^- `' | grep -qx 2 \ + && printf '%s\n' "$GOD" | grep -q 'showing 2 of 2' \ + && ok "7d: the header counts the ranked files only (showing 2 of 2)" \ + || no "7d: header/rows disagree with the ranked set: $GOD" +fi +# 7e: no depended-on test-layer file → no layer=test line (the section is the single ranking it was) +if ! printf '%s' "$REP" | grep -q '^## God files'; then + no "7e: the hub fixture's --report printed no God files section" +elif printf '%s' "$REP" | grep -q '^- layer=test'; then + no "7e: a layer=test line appeared on a corpus with no test-layer dependents" +else + ok "7e: no layer=test line without a depended-on test-layer file" +fi + +# ── 8) --seams (same runStructureText): a README -> docs/ link is not an untested code seam ─────────────── +# Two README headings link docs/EVALS.md and docs/GUIDE.md; src/main.cpp calls lib/util.cpp (a real untested seam). +D="$TMP/doclinks"; mkdir -p "$D/docs" "$D/src" "$D/lib" +printf '# Proj\n\n## Measured\n\nSee [the evals](docs/EVALS.md).\n\n## Usage\n\nRead [the guide](docs/GUIDE.md).\n' > "$D/README.md" +printf '# EVALS\n\nNumbers.\n' > "$D/docs/EVALS.md" +printf '# GUIDE\n\nSteps.\n' > "$D/docs/GUIDE.md" +printf 'int helper();\nint run() { return helper(); }\n' > "$D/src/main.cpp" +printf 'int helper() { return 1; }\n' > "$D/lib/util.cpp" +SEAMS="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$D" --seams --no-cache 2>/dev/null )"; src_=$? +SROOT="$( printf '%s' "$SEAMS" | grep -o ']*>' )" +if [ "$src_" != 0 ] || [ -z "$SROOT" ]; then + no "8: --seams on the doc-link fixture failed (rc=$src_) or printed no root" +else + if printf '%s' "$SEAMS" | grep -q ' doc-section link is still listed as an untested seam to docs" + else + ok "8a: no from README section links" + fi + printf '%s' "$SROOT" | grep -q ' bridges="1" untested="1" doc_links="2" ' \ + && ok "8b: the two doc links are counted in doc_links=2, outside bridges=/untested=" \ + || no "8b: root counts wrong (want bridges=1 untested=1 doc_links=2): $SROOT" + printf '%s' "$SEAMS" | grep -q ' lib is still reported" \ + || no "8c: the code seam src -> lib went missing" + printf '%s' "$SEAMS" | grep -q 'doc_links=N:' \ + && ok "8d: the legend reads doc_links= where it rides" || no "8d: doc_links= rides with no legend reading" +fi +NOSEAMS="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$R" --seams --no-cache 2>/dev/null )" +{ printf '%s' "$NOSEAMS" | grep -q '/dev/null )"; ldrc=$? +GF="$( printf '%s' "$LDEPS" | grep -o ']*>.*' )" +if [ "$ldrc" != 0 ] || [ -z "$GF" ]; then + no "9: --deps on the layer fixture failed (rc=$ldrc) or printed no " +else + printf '%s' "$GF" | grep -q '' \ + && ok "9a: the test helper's godfiles row carries layer=\"test\"" || no "9a: no layer=\"test\" on test/lib/helper.sh: $GF" + printf '%s' "$GF" | grep -q '' \ + && ok "9b: near miss: testing/tool.sh has no layer= (not a built-in layer dir)" || no "9b: testing/tool.sh row wrong: $GF" +fi +# 9c/9d: the full legend reads godfiles layer= exactly when a shown row carries it (present-only) +LFULL="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$T" --deps --legend=full --no-cache 2>/dev/null )" +RFULL="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$R" --deps --legend=full --no-cache 2>/dev/null )" +printf '%s' "$LFULL" | grep -q "a godfiles row's layer= is its built-in arch layer" \ + && ok "9c: --deps --legend=full reads layer= where a godfiles row carries it" || no "9c: layer= rides with no full-legend reading" +if ! printf '%s' "$RFULL" | grep -q '" +elif printf '%s' "$RFULL" | grep -q "a godfiles row's layer="; then + no "9d: the layer= reading rides on a corpus with no layered godfiles row" +else + ok "9d: no layer= reading without a layered godfiles row" +fi + +# ── 10) near miss for 7/9: under test/, but depended on by production code (fix round 1) ─────────────────── +# The layer=test group needs EVERY dependent to be a test path (isTestPath), not only the file's own dir. +# src/p1..p4.cpp include ../test/lib/hooks.h (4 production dependents) → ranked first, no layer="test"; +# test/lib/mixed.h is included by test/t1.cpp, test/t2.cpp AND src/q.cpp (one production dependent) → ranked; +# app/a.cpp includes src/core.h. Nothing here is test-only, so no layer=test line and no layer= legend. +X="$TMP/claim"; mkdir -p "$X/src" "$X/app" "$X/test/lib" +printf '#pragma once\ninline int core(){ return 2; }\n' > "$X/src/core.h" +printf '#include "../src/core.h"\nint a(){ return core(); }\n' > "$X/app/a.cpp" +printf '#pragma once\ninline int hook(){ return 1; }\n' > "$X/test/lib/hooks.h" +for n in 1 2 3 4; do printf '#include "../test/lib/hooks.h"\nint prod%s(){ return hook(); }\n' "$n" > "$X/src/p$n.cpp"; done +printf '#pragma once\ninline int mixed(){ return 3; }\n' > "$X/test/lib/mixed.h" +for n in 1 2; do printf '#include "lib/mixed.h"\nint t%s(){ return mixed(); }\n' "$n" > "$X/test/t$n.cpp"; done +printf '#include "../test/lib/mixed.h"\nint q(){ return mixed(); }\n' > "$X/src/q.cpp" +XREP="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$X" --report --no-cache 2>/dev/null )"; xrc=$? +XGOD="$( printf '%s\n' "$XREP" | sed -n '/^## God files/,/^$/p' )" +if [ "$xrc" != 0 ] || [ -z "$XGOD" ]; then + no "10: --report on the near-miss fixture failed (rc=$xrc) or printed no God files section" +else + case "$( printf '%s\n' "$XGOD" | sed -n 2p )" in + '- `test/lib/hooks.h` — 4 dependents') ok "10a: a test/ file with 4 production dependents stays ranked first" ;; + *) no "10a: test/lib/hooks.h (4 production dependents) is not the first ranked row: $XGOD" ;; + esac + printf '%s\n' "$XGOD" | grep -qxF -- '- `test/lib/mixed.h` — 3 dependents' \ + && ok "10b: one production dependent among test dependents keeps test/lib/mixed.h ranked" \ + || no "10b: test/lib/mixed.h (1 production + 2 test dependents) missing from the ranked rows: $XGOD" + if printf '%s\n' "$XGOD" | grep -q '^- layer=test'; then + no "10c: a layer=test line appeared though no file has test dependents only: $XGOD" + else + printf '%s\n' "$XGOD" | grep -q 'showing 3 of 3' \ + && ok "10c: no layer=test line; all 3 depended-on files are ranked (showing 3 of 3)" \ + || no "10c: header does not count all 3 ranked files: $XGOD" + fi +fi +XFULL="$( perl -e 'alarm 15; exec @ARGV' "$BIN" "$X" --deps --legend=full --no-cache 2>/dev/null )"; xdrc=$? +XGF="$( printf '%s' "$XFULL" | grep -o ']*>.*' )" +if [ "$xdrc" != 0 ] || [ -z "$XGF" ]; then + no "10d: --deps --legend=full on the near-miss fixture failed (rc=$xdrc) or printed no " +else + printf '%s' "$XGF" | grep -q '' \ + && printf '%s' "$XGF" | grep -q '' \ + && ok "10d: neither test/ file with a production dependent carries layer=\"test\"" \ + || no "10d: a test/ file with a production dependent is labelled (or missing): $XGF" + if printf '%s' "$XFULL" | grep -qE "godfiles row's layer=|test dependents only"; then + no "10e: the full legend reads layer= / a test-dependents claim with no layered godfiles row" + else + ok "10e: no layer= reading and no test-dependents claim without a layer=test row" + fi +fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/ripwirepubliccheck.sh b/test/ripwirepubliccheck.sh index 0289eb33d..b565e4fea 100755 --- a/test/ripwirepubliccheck.sh +++ b/test/ripwirepubliccheck.sh @@ -1289,6 +1289,9 @@ fi # hash (a re-captured line that changes at all drops back into the sweep); a regenerated capture under a new date # re-keys these two entries. Added 2026-10-08 (lane/lean-answers-068 fix round 2); the tracker gap is a named # deferral (harmless: it can only over-scan recorded output, never miss a recipe). +# docs/captures/COMMANDS_showcase_2026-10-11.md:580,595 — the same two prose lines, byte for byte (same content hashes), in the +# train 27 re-capture: the recorded --recall run again follows a block that ends inside a truncated "```bash" fence. Re-keyed +# under the new date, as the entry above says a regenerated capture does. ARM9_OK='bench/cppbench/run_cppbench.py:e5d3f485c59b0187 bench/cppbench/run_cppbench.py:7e89ac16d50cd5d2 bench/cppbench/run_cppbench.py:5d65fe73e9406e01 @@ -1301,7 +1304,9 @@ bench/substitution_report.py:c3043030dc3e5473 docs/docs_commands_build.py:fa972c42dc369a5c docs/docs_commands_build.py:f10f0b8021b7b6c2 docs/captures/COMMANDS_showcase_2026-10-08.md:118fbfbb8f6823fe -docs/captures/COMMANDS_showcase_2026-10-08.md:22be6025a57af773' +docs/captures/COMMANDS_showcase_2026-10-08.md:22be6025a57af773 +docs/captures/COMMANDS_showcase_2026-10-11.md:118fbfbb8f6823fe +docs/captures/COMMANDS_showcase_2026-10-11.md:22be6025a57af773' python3 - "$TMP/tracked.z" "$ARM9_OK" > "$TMP/arm9" <<'PY' import hashlib, os, re, sys paths = [p.decode('utf-8', 'surrogateescape') diff --git a/test/rootspellingcheck.sh b/test/rootspellingcheck.sh index 58a3e8f41..b7110ae9b 100755 --- a/test/rootspellingcheck.sh +++ b/test/rootspellingcheck.sh @@ -167,7 +167,7 @@ invariant "pyfour --quality-delta" "$PY" --quality-delta every_spelling 'control: app/views.py resolves `from pkg.store import load` (pkg/store.py afferent="1")' \ "pyfour --deps" '' every_spelling 'control: handler is the one caller of pkg/store.py:load (the import bound it)' \ - "pyfour --callers=store" '' + "pyfour --callers=store" '' every_spelling 'control: the same-directory app/local.py:load has count="0" callers (the name ladder did not bind it)' \ "pyfour --callers=local" 'count="0"' every_spelling 'control: --impact names app/views.py as the importer of pkg/store.py (importers="1")' \ @@ -218,7 +218,7 @@ cwd_selector() key="$( printf '%s' "$label" | tr -c 'A-Za-z0-9' '_' )" for sp in $SPELLINGS; do pre="$( selector_prefix "$sp" "$d" "$form" )" - spell "$sp" "$d" "$TMP/out/$key.$sp" "$flag=$pre$rel" + spell "$sp" "$d" "$TMP/out/$key.$sp" "$flag=$pre$rel" ${CWD_EXTRA:+"$CWD_EXTRA"} rc="$( cat "$TMP/out/$key.$sp.rc" )" if [ "$rc" != 0 ]; then wrong="$wrong $sp(rc=$rc)"; continue; fi for pat in "$@"; do @@ -230,8 +230,14 @@ cwd_selector() } for form in typed abs cwdrel; do - cwd_selector "(6) --edit-check=<$form root>/pkg/store.py:load" "$PY" "$form" --edit-check pkg/store.py:load 'sym="load"' '' - cwd_selector "(6) --callers=<$form root>/pkg/store.py:load" "$PY" "$form" --callers pkg/store.py:load 'count="1"' '' + # lane lean-trio (idea #11): an unchanged, unflagged contract MAY be its verdict line (no rows), so the default + # answer is read off the root: p= is the definition the selector named, callers="1" its caller. + cwd_selector "(6) --edit-check=<$form root>/pkg/store.py:load" "$PY" "$form" --edit-check pkg/store.py:load 'sym="load"' 'p="pkg/store.py:1"' 'callers="1"' + # PROCESS rule 4 (fix round 1, rv-lean-trio item 4): the caller IDENTITY the arm asserted before idea #11, on a --limit=40 + # twin (an explicit window is always the full document), so a wrong-file caller cannot pass under any spelling. + CWD_EXTRA=--limit=40 cwd_selector "(6) --edit-check=<$form root>/pkg/store.py:load --limit=40" "$PY" "$form" --edit-check pkg/store.py:load \ + 'sym="load"' '' + cwd_selector "(6) --callers=<$form root>/pkg/store.py:load" "$PY" "$form" --callers pkg/store.py:load 'count="1"' '' cwd_selector "(6) --at=<$form root>/pkg/store.py:2" "$PY" "$form" --at pkg/store.py:2 'sym="load"' cwd_selector "(6) --affected=<$form root>/pkg/store.py" "$PY" "$form" --affected pkg/store.py 'seeds="1"' 'reached="1"' done diff --git a/test/rubyinheritcheck.sh b/test/rubyinheritcheck.sh index f29d5c91e..fb782e497 100755 --- a/test/rubyinheritcheck.sh +++ b/test/rubyinheritcheck.sh @@ -27,9 +27,9 @@ # an in-tree `Factory` pins it. # (b) a MIXIN (`include Helper` / `extend` / `prepend`) is an ANCESTOR for the call graph since parser version # 131 — the base walk, the CHA cone and a bare call's reach read it (ingest_relations.h captureRubyMixinBases, -# test/rubyreachcheck.sh) — but it mints no IMPLEMENTOR row: --lego lists the classes below a CLASS, and the -# inheritance overlay takes only class-like bases (graph.h namespaceCompatible), which a module is not. So -# --lego=Helper still lists no Mixed; a module's implementor row is a separate decision, not taken here. +# test/rubyreachcheck.sh) — and since lane lego-transitive it is an IMPLEMENTOR edge too: graph.h's implementors +# builder admits a Ruby module as a base (BuiltinMethodGate::isClassLike's Ruby-module rule), so --lego=Helper +# lists Mixed. Floor (b) is CLOSED; the arm below is inverted and says so (rake: `include Rake::DSL`). # (c) the base WALK is keyed by NAME. A base is found by name (its final segment, as every language's is) # and then SCOPED: the superclass as written is looked up the way Ruby looks it up — Module.nesting # innermost first, then the top level, `::X` absolute — against every class/module the tree opens (the @@ -350,8 +350,9 @@ then fi if runq LH "$FIX" --no-cache --lego=Helper then - echo "$LH" | grep -q ']*>' | tr '\n' ' ' )"; fi fi CM="$( rowOf 'n="call_mixin" ' )" echo "$CM" | grep -q '/dev/null )" -if echo "$MCU" | grep -q 'n="call_inherited"' +# an absence is read only off a run that produced its root (a crashed or empty run is not "not among them") +if ! echo "$MCU" | grep -q ' root, so the absence below cannot be read: $( echo "$MCU" | head -c 200 )" +elif echo "$MCU" | grep -q 'n="call_inherited"' then no "mutation: Unrelated::build lists call_inherited — a class whose lookup leaves the tree bound to a namesake" else diff --git a/test/rubyschemacheck.sh b/test/rubyschemacheck.sh new file mode 100755 index 000000000..cf442d451 --- /dev/null +++ b/test/rubyschemacheck.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# rubyschemacheck.sh — the Rails schema capture gate: RUBY db/schema.rb COLUMNS ARE Section DEFS. A +# Ruby file whose tree holds a `create_table "x", … do |t| … end` call is a rendered schema BY CONTENT +# (no path heuristic), and each `t. "name"` / `t. :name` receiver-call in +# the table block mints ONE SymKind::Section def at Lang::Ruby — the same data-kind slot as a doc +# heading / YAML key (model.h) — with span = the name token. Rails-generated attribute uses +# (`product.price`, pinned here as explicit-receiver model calls) resolve to the column as a def — +# DEFINITIONS ONLY by maintainer decision: the defs answer --uses/--grep/--whereis but admit NO call +# edges, only the restart share every def gets (graph.h buildGraph's byName skips Section-Ruby; §3 pins that). The DSL +# CALLS keep their reference posture exactly like the attr family (--uses=string/define/timestamps +# stay defs=0 external=1). The four id spellings and t.timestamps name columns too, per the rules +# below; a composite/symbol primary_key mints no implicit id. +# +# Fixture test/rubyschemafix — schema text from a real, running Rails 8.1 app's `bin/rails +# db:schema:dump` (verified db:schema:load-able), with the original domain tables scrubbed to spike_* +# names; two spellings no dump ever emits are restored BY HAND and marked in-file: `id: :uuid` (the +# dump itself notes "Could not dump … Unknown type 'uuid'") and a literal `t.timestamps` (rendered +# schemas expand it to t.datetime pairs) — runtime-semantics and shape notes in USECASES.md beside the +# fixture. +# name the 9 `t.string "name"` columns (→ defs=19 with the 9 attr/def defs + the yaml key) +# id the multi-def floor: EVERY table carries its key — 14 (12 implicit + id: :uuid + …) +# event_id `primary_key: "event_id"` renames the key off `id` +# ref `id: false` table's only column; NO id def there +# created_at/updated_at both the rendered t.datetime pair AND the literal t.timestamps call +# quote_symbol t.string :name + t.references :owner (floor) + t.index ["name"] (floor) + +# helper.string "unbound_column" — a column call on a DIFFERENT bare identifier +# names nothing (the receiver must be the block parameter, `t`) +# migration_string/migration_symbol/migration_columns the migration INTERFERENCE floors: a +# class-wrapped create_table (string- OR symbol-named) mints NOTHING — the schema is the only +# source of column names, indexing a migration beside its schema would double every def and its +# — and add_column/remove_column are argument data, never defs (a column added +# then dropped never registers) +# floor_case where("name = ?") — a string fragment stays opaque, never a def or a use +# all_four / pair_def_column / triple_* the def+attr+column(+yaml) collision matrix; the picker +# styles: `self.name` in the instance method PairDefColumn#lookup pins its own def (locality), +# `rec.name` on a local splits over the method/attr defs (graph_ambiguous), and +# `SingleColumn.new.name` binds nothing (no in-tree def on that model; never a column) +# unrelated_client an untyped `response.ref` (ref is a column-only name): a use, never a caller +# config/spike_names.yml the cross-language name key (counted, never edgeable) +# +# Usage: test/rubyschemacheck.sh | RIPWIRE_BIN=asan/ripwire test/rubyschemacheck.sh +# Exit: 0 = clean · 1 = an arm failed · 2 = usage / missing prerequisite + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/rubyschemafix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$FIX" ] || { echo "no test/rubyschemafix — fixture missing"; exit 2; } +echo "rubyschemacheck: BIN=$BIN FIX=$FIX" + +useshead(){ "$BIN" "$FIX" --uses="$1" --no-cache 2>/dev/null | grep -oE "of=\"[^\"]*\" defs=\"[0-9]+\" external=\"[0-9]+\" count=\"[0-9]+\"" | head -1; } +undefinable(){ "$BIN" "$FIX" --uses="$1" --no-cache 2>&1 | grep -q "matched no indexed definition"; } +callershead(){ "$BIN" "$FIX" --callers="$1" --no-cache 2>/dev/null | grep -oE "of=\"[^\"]*\" defs=\"[0-9]+\" count=\"[0-9]+\"" | head -1; } + +# ── 1. CAPTURE: columns are Section defs; the id rule; the timestamps pair ────────────────────────── +[ "$( useshead name )" = 'of="name" defs="19" external="0" count="3"' ] \ + && ok 'capture: name — 9 columns + 9 attr/def defs + 1 yaml key = 19; only the 3 consumers use it (the where() fragment is opaque)' \ + || no "capture: name: $( useshead name )" +[ "$( useshead id )" = 'of="id" defs="14" external="0" count="1"' ] \ + && ok 'capture: id — the multi-def floor: 12 implicit + the id: :uuid pair + the id-pair… 14 keys on 14 tables (only id_false and the renamed table have none)' \ + || no "capture: id: $( useshead id )" +[ "$( useshead event_id )" = 'of="event_id" defs="1" external="0" count="0"' ] \ + && ok 'capture: `primary_key: "event_id"` mints the renamed key def, and NO implicit id beside it' \ + || no "capture: event_id: $( useshead event_id )" +[ "$( useshead ref )" = 'of="ref" defs="1" external="0" count="1"' ] \ + && ok 'capture: the `id: false` table'"'"'s own column mints; the table has NO id def (its one use is the untyped unrelated_client.rb site)' \ + || no "capture: ref: $( useshead ref )" +[ "$( useshead created_at )" = 'of="created_at" defs="2" external="0" count="1"' ] \ + && ok 'capture: created_at — the rendered t.datetime column AND the literal t.timestamps call each mint one; the consumer call reaches them' \ + || no "capture: created_at: $( useshead created_at )" +[ "$( useshead updated_at )" = 'of="updated_at" defs="2" external="0" count="0"' ] \ + && ok 'capture: updated_at — both spellings def; no consumer calls it (floor proof the count is per-row, not padded)' \ + || no "capture: updated_at: $( useshead updated_at )" +[ "$( useshead origin )" = 'of="origin" defs="1" external="0" count="0"' ] \ + && ok 'capture: a COMPOSITE primary_key (["origin","destination"]) mints the rendered columns — and NO implicit id (the id defs=14 arm above is the regression guard)' \ + || no "capture: origin: $( useshead origin )" +[ "$( useshead destination )" = 'of="destination" defs="1" external="0" count="0"' ] \ + && ok 'capture: the composite key'"'"'s second column mints too' \ + || no "capture: destination: $( useshead destination )" +undefinable 'price > 0' \ + && ok "floor: t.check_constraint \"price > 0\", name: … — a constraint EXPRESSION names no column (would otherwise mint n=\"price > 0\")" \ + || no 'floor: price > 0 — a check_constraint expression minted a def' + +# ── 2. FLOORS: the DSL calls stay references; non-column DSL names no column ─────────────────────── +undefinable owner \ + && ok 'floor: t.references :owner names no column — owner stays undefinable (references is the reference, its arg is data)' \ + || no 'floor: owner — t.references minted a def' +[ "$( useshead index )" = 'of="index" defs="0" external="1" count="1"' ] \ + && ok 'floor: t.index ["name"] — the CALL is an external reference (DSL posture), the array arg defines nothing' \ + || no "floor: index: $( useshead index )" +undefinable email \ + && ok 'migration: a CLASS-wrapped string-named create_table ("spike_migrated") mints nothing — the same columns the migration created are already minted from the rendered schema; indexing both would DOUBLE the defs+PageRank' \ + || no 'migration: email — a class-wrapped (string-named) migration table minted a def' +undefinable widget_name \ + && ok 'migration: a symbol-named migration table (create_table :spike_widgets) mints nothing — both the class-nest and the string-name gates refuse' \ + || no 'migration: widget_name — a symbol-named migration table minted a def' +undefinable bla \ + && ok 'migration: add_column "bla" is argument data — the DSL call is a reference, its args are not defs (only the schema is the source of names)' \ + || no 'migration: bla — add_column minted a def' +undefinable gonna_die \ + && ok 'migration: a column added then REMOVED never registers (remove_column is read nowhere)' \ + || no 'migration: gonna_die — a removed column minted a def' +undefinable unbound_column \ + && ok 'receiver: a column call on a DIFFERENT bare identifier (`helper.string "unbound_column"`) names no column — the receiver must BE the block parameter (`t`) the create_table call bound' \ + || no 'receiver: unbound_column — a non-t receiver minted a def' +for dsl in string define datetime timestamps create_table; do + [ "$( useshead "$dsl" | grep -oE 'defs="[0-9]+" external="[0-9]+"' )" = 'defs="0" external="1"' ] \ + && ok "posture: the DSL call \`$dsl\` itself is still no def (reference capture posture)" \ + || no "posture: $dsl gained a def — the DSL call must stay a reference: $( useshead "$dsl" )" +done + +# ── 3. DEFINITIONS ONLY: columns admit NO call edges (maintainer decision, graph.h buildGraph byName) ── +[ "$( callershead id )" = 'of="id" defs="14" count="0"' ] \ + && ok 'defs-only: `IdDefault.new.id` matches the 14 defs but has NO caller row — columns take no edges' \ + || no "defs-only: id callers: $( callershead id )" +[ "$( callershead created_at )" = 'of="created_at" defs="2" count="0"' ] \ + && ok 'defs-only: `Timestamps.new.created_at` matches both defs but carries no call edge' \ + || no "defs-only: created_at callers: $( callershead created_at )" +"$BIN" "$FIX" --uses=ref --no-cache 2>/dev/null | grep -q 'p="unrelated_client.rb:7"' \ + && [ "$( callershead ref )" = 'of="ref" defs="1" count="0"' ] \ + && ok "defs-only false-binding: the untyped 'response.ref' in unrelated_client.rb:7 is a use of the column-only name ref, never its caller (RED without buildGraph's Section-Ruby byName skip: it binds by name alone)" \ + || no "defs-only: ref callers: $( callershead ref )" +"$BIN" "$FIX" --callers=name --no-cache 2>/dev/null >"$TMP/callers_name" +[ "$( callershead name )" = 'of="name" defs="19" count="2"' ] \ + && grep -q '' "$TMP/callers_name" \ + && grep -q '' "$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 )" + +# ── 4. AMBIGUITY + LOCALITY: the resolver splits honestly, pins on evidence ───────────────────────── +"$BIN" "$FIX" --uses=name --no-cache 2>/dev/null >"$TMP/uses_name" +grep -q 'of="name" defs="19" external="0" count="3"' "$TMP/uses_name" \ + && grep -q 'graph_ambiguous="1"' "$TMP/uses_name" \ + && ok 'ambiguity: the name gauge counts ONLY the 1 site that still splits over real (method/attr) defs (rec.name, consumer_ambiguous.rb:8) — the id/created_at column sites left the gauge once columns stopped taking edges, and self.name in PairDefColumn#lookup is pinned by its class' \ + || no 'ambiguity: name gauge/rows wrong' +grep -q 'p="consumer_ambiguous.rb:8"' "$TMP/uses_name" \ + && ok 'ambiguous arm: rec.name on a plain local SPLITS over the whole def set (its row is in the gauge)' \ + || no 'ambiguous arm: consumer_ambiguous.rb:8 row missing' +grep -q 'p="pair_def_column.rb:15"' "$TMP/uses_name" \ + && [ "$( "$BIN" "$FIX" --callees=lookup --no-cache 2>/dev/null | grep -oE ']*>' | tr -d '\n' )" = '' ] \ + && ok 'locality arm: self.name in the instance method PairDefColumn#lookup pins exactly its own class'"'"'s def name (pair_def_column.rb:6, no via=) and its use row is NOT in the ambiguous gauge — never a column' \ + || no "locality arm: pair_def_column.rb:15 row missing or lookup does not pin pair_def_column.rb:6 alone" +grep -q 'p="column_consumers.rb:7"' "$TMP/uses_name" \ + && ok 'binding arm: SingleColumn.new.name is a real use row (explicit model receiver)' \ + || no 'binding arm: column_consumers.rb:7 row missing' + +# ── 5. MAP KINDS: the defs carry t="sec" with the DSL row family merged by name ───────────────────── +"$BIN" "$FIX" --no-cache 2>/dev/null >"$TMP/map" +# The schema.rb file block, extracted once; every kind arm asserts INSIDE it (the yaml Section key is a +# different file and must never satisfy a schema assertion). +python3 - "$TMP/map" >"$TMP/schema_block" <<'PYEOF' +import re, sys +xml = open(sys.argv[1], encoding='utf-8').read() +m = re.search(r']*>(.*?)', xml, re.S) +sys.stdout.write(m.group(1) if m else "") +PYEOF +grep -q '"$TMP/m1" 2>/dev/null +"$BIN" "$FIX" --no-cache >"$TMP/m2" 2>/dev/null +if cmp -s "$TMP/m1" "$TMP/m2"; then ok "deterministic (two --no-cache runs byte-identical)"; else no "non-deterministic"; fi +"$BIN" "$FIX" --cache="$TMP/c.bin" >"$TMP/cold" 2>/dev/null +"$BIN" "$FIX" --cache="$TMP/c.bin" >"$TMP/warm" 2>/dev/null +if cmp -s "$TMP/cold" "$TMP/warm"; then ok "warm == cold (schema defs survive the cache round-trip)"; else no "warm != cold"; fi +if command -v xmllint >/dev/null 2>&1; then + if xmllint --noout "$TMP/m1" 2>/dev/null; then ok "xml well-formed"; else no "xml malformed"; fi +else + ok "xml well-formed (xmllint absent — skipped)" +fi + +[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; } \ No newline at end of file diff --git a/test/rubyschemafix/USECASES.md b/test/rubyschemafix/USECASES.md new file mode 100644 index 000000000..6cc6aba0e --- /dev/null +++ b/test/rubyschemafix/USECASES.md @@ -0,0 +1,57 @@ +# rubyschemafix — fixture fact sheet (the Rails schema-column capture) + +What this fixture is, and which claims in it are runtime-proven vs stated floors. + +## Provenance + +`schema.rb` is the rendered schema of a real, running Rails 8.1 application: its `bin/rails +db:schema:dump` output, with the original domain tables and all identifying markers stripped and the +remaining tables renamed to `spike_*`. The dump was verified to load back into that application +(`bin/rails db:schema:load` succeeded), so the shape vocabulary here — `force: :cascade`, `id: false`, +`primary_key:`, the option-pair spellings, the string/symbol column spellings, `null: false` — comes +from a working Rails install, not from a hand-invented guess. + +Two spellings no real dump ever emits are restored BY HAND in `schema.rb` and marked in-file: + +- `spike_id_uuids` with `id: :uuid` — the source app's dump FAILED for this table ("Could not dump + table … Unknown type 'uuid' for column 'id'": the dump host lacked the uuid extension). The table's + shape is the standard Rails rendering for a uuid key; the `id: :uuid` pair is the spelling ripwire's + id rule reads (the pair KEY names `id`, the uuid TYPE is not modelled — stated floor). +- `spike_timestamps_literal` with a bare `t.timestamps` — rendered schemas expand it into the two + `t.datetime` pairs (present above it as `spike_timestamps`); the DSL CALL itself exists only in + migrations. The column names it mints (`created_at`, `updated_at`) are part of Active Record's + documented timestamps contract. + +The model files mirror the source application's runtime shapes (`self.table_name`, `attribute`, +`def name`; a quoted `where("name = ?")` in `floor_case.rb` is the runtime pattern of a raw SQL +fragment). They are indexed as text; nothing here executes at gate time. + +## What the gate pins (all measured on the binary, per the repo's honesty rule) + +| Query | Pinned answer | Why | +|---|---|---| +| `--uses=name` | defs=19 external=0 count=3 | 9 columns + 9 attr/def defs + 1 yaml key; only the 3 consumers use it — the `where("name = ?")` fragment stays opaque | +| `--uses=id` | defs=14 external=0 count=1 | the multi-def floor: EVERY table carries its key (12 implicit, `id: :uuid`, and the explicit `id: false`-less ones); only `id: false` + the renamed table have none | +| `--uses=created_at` | defs=2 | the rendered `t.datetime "created_at"` column AND the literal `t.timestamps` call | +| `--uses=event_id` | defs=1 | `primary_key: "event_id"` renames the key off `id` | +| `--callers=created_at` | defs=2 count=0 | DEFINITIONS ONLY: the call site matches both defs but columns carry NO call edges (§3) | +| `graph_ambiguous` | 2 on `name` | only the sites that still split over real method/attr defs; the column sites exited the gauge when edges were switched off (§4) | + +## Stated floors (silence is stated, never silent) + +- `t.references :owner` / `t.belongs_to` / `t.polymorphic` / `t.index` name no column (the fixture + spells `references` + `index` in `spike_quote_symbol`; `owner` stays undefinable). +- A symbol-named table (`create_table :users`) is a migration spelling, not a rendered-schema shape — + no name-string node ⇒ no schema surface. +- Duplicate column names across tables stay SEPARATE defs (`id` on N tables → defs=N); the map row + merges them by name with `overloads`, never hiding the multiplicity. +- MIGRATIONS never interfere (the three migration fixtures, all pinned undefinable): a class-wrapped + `create_table "x"` / `create_table :x` — the migration shape — is refused by the class/module-nest + gate, so the schema is the ONE source of column names and indexing a migration beside its schema + cannot double any def; `add_column` / `remove_column` / `change_column` are argument data and read + nowhere — a column added then dropped (`gonna_die`) never registers. +- `where("price > ?")` string fragments are opaque to any static extractor. +- The DSL calls (`string`, `datetime`, `create_table`, …) stay external-surface references — no def + named `string` appears; only the NAME args gain defs (same posture as the attr family). +- A column call's receiver must BE the block parameter: `helper.string "unbound_column"` (a different + bare identifier) names no column — the gate accepts only `t.` (the `|t|` handle) forms. \ No newline at end of file diff --git a/test/rubyschemafix/all_four.rb b/test/rubyschemafix/all_four.rb new file mode 100644 index 000000000..423054b34 --- /dev/null +++ b/test/rubyschemafix/all_four.rb @@ -0,0 +1,14 @@ +# all-four arm: def + attribute + column in ONE model, and the yaml source in +# config/spike_names.yml. This is the canonical collision; runtime dispatch winner +# is the def, the static picker order (def > attr > column > yaml) is pinned in C. +module Spike + class AllFour < ApplicationRecord + self.table_name = "spike_all_fours" + + attribute :name + + def name + "def" + end + end +end diff --git a/test/rubyschemafix/column_consumers.rb b/test/rubyschemafix/column_consumers.rb new file mode 100644 index 000000000..b25eba7fa --- /dev/null +++ b/test/rubyschemafix/column_consumers.rb @@ -0,0 +1,17 @@ +# consumer arm: EXPLICIT model receivers — `Spike::SingleColumn.new.name` resolves to the +# column def set by name; `Spike::IdDefault.new.id` sits on the multi-table implicit id +# floor (14 id defs). Definitions-only per the review round: these calls match the defs +# but columns take NO call edges, so the gate's §3 arms are count="0" assertions. +module Spike + def self.single_name + SingleColumn.new.name + end + + def self.default_id + IdDefault.new.id + end + + def self.literal_stamp + Timestamps.new.created_at + end +end diff --git a/test/rubyschemafix/column_yaml.rb b/test/rubyschemafix/column_yaml.rb new file mode 100644 index 000000000..f794405e8 --- /dev/null +++ b/test/rubyschemafix/column_yaml.rb @@ -0,0 +1,6 @@ +# pair arm (column, yaml): column only; the yaml source lives in config/spike_names.yml. +module Spike + class ColumnYaml < ApplicationRecord + self.table_name = "spike_column_yamls" + end +end diff --git a/test/rubyschemafix/config/spike_names.yml b/test/rubyschemafix/config/spike_names.yml new file mode 100644 index 000000000..9e2e762a9 --- /dev/null +++ b/test/rubyschemafix/config/spike_names.yml @@ -0,0 +1,5 @@ +# spike_names.yml — the cross-language Section def for `name`. +# The all-four collision (def + attribute + column + yaml) in all_four.rb; the YAML +# key is counted, never edgeable (Ruby<->Yaml is not langCompatible). +name: + display: "Spike" \ No newline at end of file diff --git a/test/rubyschemafix/consumer_ambiguous.rb b/test/rubyschemafix/consumer_ambiguous.rb new file mode 100644 index 000000000..ffe21c986 --- /dev/null +++ b/test/rubyschemafix/consumer_ambiguous.rb @@ -0,0 +1,11 @@ +# consumer arm, spam: a call site with NO locality evidence — `rec.name` where rec is +# an ordinary local. Every in-repo def named `name` is a candidate (the 9 columns, the +# attr Vars, the method defs, the yaml key); the resolver SPLITS and says so (amb=K), +# never silently picks. The call edges land on the def set, not on nothing. +module Spike + class ConsumerAmbiguous < ApplicationRecord + def render_label( rec ) + rec.name + end + end +end \ No newline at end of file diff --git a/test/rubyschemafix/floor_case.rb b/test/rubyschemafix/floor_case.rb new file mode 100644 index 000000000..1e8dc422f --- /dev/null +++ b/test/rubyschemafix/floor_case.rb @@ -0,0 +1,12 @@ +# floor arm: a raw SQL string fragment. The `name` inside it is opaque to any +# static extractor — this is a DISCLOSED floor (no def, no use), proven only that +# the query runs. +module Spike + class FloorCase < ApplicationRecord + self.table_name = "spike_single_columns" + + def self.find_named( value ) + where( "name = ?", value ).to_a + end + end +end diff --git a/test/rubyschemafix/id_default.rb b/test/rubyschemafix/id_default.rb new file mode 100644 index 000000000..55f9e7ad3 --- /dev/null +++ b/test/rubyschemafix/id_default.rb @@ -0,0 +1,6 @@ +# id arm: implicit default id. +module Spike + class IdDefault < ApplicationRecord + self.table_name = "spike_id_defaults" + end +end diff --git a/test/rubyschemafix/id_false.rb b/test/rubyschemafix/id_false.rb new file mode 100644 index 000000000..732241389 --- /dev/null +++ b/test/rubyschemafix/id_false.rb @@ -0,0 +1,8 @@ +# id arm: create_table carried `id: false` — there is NO id column; the model +# declares its own primary key. +module Spike + class IdFalse < ApplicationRecord + self.table_name = "spike_id_false" + self.primary_key = "ref" + end +end diff --git a/test/rubyschemafix/id_renamed.rb b/test/rubyschemafix/id_renamed.rb new file mode 100644 index 000000000..08c02c84c --- /dev/null +++ b/test/rubyschemafix/id_renamed.rb @@ -0,0 +1,6 @@ +# id arm: `t.primary_key "event_id"` renames the key off `id`. +module Spike + class IdRenamed < ApplicationRecord + self.table_name = "spike_id_renamed" + end +end diff --git a/test/rubyschemafix/id_uuid.rb b/test/rubyschemafix/id_uuid.rb new file mode 100644 index 000000000..d0d21eaaa --- /dev/null +++ b/test/rubyschemafix/id_uuid.rb @@ -0,0 +1,6 @@ +# id arm: `id: :uuid` — the key is present, still named `id`, uuid type. +module Spike + class IdUuid < ApplicationRecord + self.table_name = "spike_id_uuids" + end +end diff --git a/test/rubyschemafix/migration_columns.rb b/test/rubyschemafix/migration_columns.rb new file mode 100644 index 000000000..c70a1fdbd --- /dev/null +++ b/test/rubyschemafix/migration_columns.rb @@ -0,0 +1,13 @@ +# migration arm, column lifecycle: an OLD migration adds "bla", a LATER one removes it — +# only the rendered schema is the source of column names. `add_column` / `remove_column` +# are not read anywhere (the DSL call is a reference, its arguments are data), so a column +# added then dropped never registers, and a column that STILL exists is registered by the +# schema, not by the migration that added it. `bla` and `gonna_die` stay undefinable. +class MoreColumns < ActiveRecord::Migration[8.1] + def change + add_column :spike_single_columns, "bla", :string + change_column :spike_single_columns, :ref, :text + add_column :spike_single_columns, "gonna_die", :string + remove_column :spike_single_columns, "gonna_die" + end +end \ No newline at end of file diff --git a/test/rubyschemafix/migration_string.rb b/test/rubyschemafix/migration_string.rb new file mode 100644 index 000000000..a25572c7e --- /dev/null +++ b/test/rubyschemafix/migration_string.rb @@ -0,0 +1,14 @@ +# migration arm, STRING-named table: a real migration wraps its create_table in a class body. +# The schema capture must NOT mint from it — the same columns the migration created are already +# minted from the rendered db/schema.rb, and indexing both would DOUBLE every def on corpora +# that carry migrations. The suppression is the +# class/module-nest gate: a rendered schema's tables live at file level or under +# ActiveRecord::Schema[].define, never inside a class. `email` here stays undefinable; the +# table's implicit id is NOT added to the id multi-def floor (the gate pins both). +class CreateUsers < ActiveRecord::Migration[8.1] + def change + create_table "spike_migrated" do |t| + t.string "email" + end + end +end \ No newline at end of file diff --git a/test/rubyschemafix/migration_symbol.rb b/test/rubyschemafix/migration_symbol.rb new file mode 100644 index 000000000..5345d1766 --- /dev/null +++ b/test/rubyschemafix/migration_symbol.rb @@ -0,0 +1,11 @@ +# migration arm, SYMBOL-named table: the other common spelling. Already floored by the +# string-name gate (a symbol-named create_table is a migration spelling, not a rendered +# schema), kept as a fixture so the floor is pinned from BOTH sides — the class-nest gate +# and the name-string gate both refuse, and the widget columns stay undefinable. +class CreateWidgets < ActiveRecord::Migration[8.1] + def change + create_table :spike_widgets do |t| + t.string "widget_name" + end + end +end \ No newline at end of file diff --git a/test/rubyschemafix/pair_def_column.rb b/test/rubyschemafix/pair_def_column.rb new file mode 100644 index 000000000..f564004bd --- /dev/null +++ b/test/rubyschemafix/pair_def_column.rb @@ -0,0 +1,18 @@ +# pair arm (def, column): the class def shadows the generated attribute reader. +module Spike + class PairDefColumn < ApplicationRecord + self.table_name = "spike_pair_def_columns" + + def name + "def" + end + + # locality-resolved arm: an INSTANCE method's `self.name`, in the class whose own + # `def name` sits in this same file — Ruby's method lookup pins THAT def (no via=), + # never the schema column or the yaml key. (A class-side `def self.lookup` would + # call the class object's own `name`, Module#name, and bind nothing in the tree.) + def lookup + self.name + end + end +end diff --git a/test/rubyschemafix/quote_double.rb b/test/rubyschemafix/quote_double.rb new file mode 100644 index 000000000..824666252 --- /dev/null +++ b/test/rubyschemafix/quote_double.rb @@ -0,0 +1,6 @@ +# quote-spelling arm: column declared t.string "name" (double quotes). +module Spike + class QuoteDouble < ApplicationRecord + self.table_name = "spike_quote_double" + end +end diff --git a/test/rubyschemafix/quote_single.rb b/test/rubyschemafix/quote_single.rb new file mode 100644 index 000000000..2b7cf31f4 --- /dev/null +++ b/test/rubyschemafix/quote_single.rb @@ -0,0 +1,6 @@ +# quote-spelling arm: column declared `t.string 'name'` (single quotes). +module Spike + class QuoteSingle < ApplicationRecord + self.table_name = "spike_quote_single" + end +end diff --git a/test/rubyschemafix/quote_symbol.rb b/test/rubyschemafix/quote_symbol.rb new file mode 100644 index 000000000..22cdf94ff --- /dev/null +++ b/test/rubyschemafix/quote_symbol.rb @@ -0,0 +1,6 @@ +# quote-spelling arm: column declared t.string :name (symbol). +module Spike + class QuoteSymbol < ApplicationRecord + self.table_name = "spike_quote_symbol" + end +end diff --git a/test/rubyschemafix/schema.rb b/test/rubyschemafix/schema.rb new file mode 100644 index 000000000..ced49257a --- /dev/null +++ b/test/rubyschemafix/schema.rb @@ -0,0 +1,90 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2000_01_01_000001) do + create_table "spike_all_fours", force: :cascade do |t| + t.string "name" + end + + create_table "spike_column_yamls", force: :cascade do |t| + t.string "name" + end + + create_table "spike_id_defaults", force: :cascade do |t| + end + + create_table "spike_id_false", id: false, force: :cascade do |t| + t.string "ref", null: false + end + + create_table "spike_id_renamed", primary_key: "event_id", force: :cascade do |t| + end + +# Could not dump table "spike_id_uuids" because of following StandardError +# Unknown type 'uuid' for column 'id' +# Restored by hand: the standard Rails rendering for a uuid table — `id: :uuid` (the value is a +# simple_symbol), the spelling the capture's id rule reads. + create_table "spike_id_uuids", id: :uuid, force: :cascade do |t| + end + + create_table "spike_pair_attr_columns", force: :cascade do |t| + t.string "name" + end + + create_table "spike_pair_def_columns", force: :cascade do |t| + t.string "name" + end + + create_table "spike_plain", force: :cascade do |t| + end + + create_table "spike_quote_double", force: :cascade do |t| + t.string "name" + end + + create_table "spike_quote_single", force: :cascade do |t| + t.string 'name' + end + +create_table "spike_quote_symbol", force: :cascade do |t| + t.string :name + t.references :owner + t.index ["name"], name: "idx_spike" + helper.string "unbound_column" + t.check_constraint "price > 0", name: "check_positive" + end + + create_table "spike_composite_keys", primary_key: ["origin", "destination"], force: :cascade do |t| + t.string "origin" + t.string "destination" + end + + create_table "spike_single_columns", force: :cascade do |t| + t.string "name" + end + + create_table "spike_timestamps", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +# Migration spelling, added by hand: the rendered dump expands `t.timestamps` into the two datetime +# columns above; the DSL CALL itself (the rule under test) names both columns literally. + create_table "spike_timestamps_literal", force: :cascade do |t| + t.timestamps + end + + create_table "spike_triples", force: :cascade do |t| + t.string "name" + end + +end diff --git a/test/rubyschemafix/single_column.rb b/test/rubyschemafix/single_column.rb new file mode 100644 index 000000000..1c89b79d4 --- /dev/null +++ b/test/rubyschemafix/single_column.rb @@ -0,0 +1,6 @@ +# single-source arm: column `name` only (no def, no attr). +module Spike + class SingleColumn < ApplicationRecord + self.table_name = "spike_single_columns" + end +end diff --git a/test/rubyschemafix/timestamps.rb b/test/rubyschemafix/timestamps.rb new file mode 100644 index 000000000..59fd081ba --- /dev/null +++ b/test/rubyschemafix/timestamps.rb @@ -0,0 +1,6 @@ +# timestamps arm: `t.timestamps` yields created_at / updated_at columns. +module Spike + class Timestamps < ApplicationRecord + self.table_name = "spike_timestamps" + end +end diff --git a/test/rubyschemafix/triple_plain.rb b/test/rubyschemafix/triple_plain.rb new file mode 100644 index 000000000..d920b8458 --- /dev/null +++ b/test/rubyschemafix/triple_plain.rb @@ -0,0 +1,14 @@ +# triple arm, spelling (a): plain `def name` alongside `attribute :name` +# and a real `name` column. Ruby lookup is by ancestry — the class-level def +# wins over the module-generated attribute reader regardless of declaration order. +module Spike + class TriplePlain < ApplicationRecord + self.table_name = "spike_triples" + + attribute :name + + def name + "def" + end + end +end diff --git a/test/rubyschemafix/triple_reversed.rb b/test/rubyschemafix/triple_reversed.rb new file mode 100644 index 000000000..c9a2881ec --- /dev/null +++ b/test/rubyschemafix/triple_reversed.rb @@ -0,0 +1,13 @@ +# triple arm, spelling (a) reversed: `def name` declared BEFORE `attribute :name`. +# Same winner as triple_plain — proof that lookup is by ancestry, not declaration order. +module Spike + class TripleReversed < ApplicationRecord + self.table_name = "spike_triples" + + def name + "def" + end + + attribute :name + end +end diff --git a/test/rubyschemafix/triple_super.rb b/test/rubyschemafix/triple_super.rb new file mode 100644 index 000000000..8f58e4612 --- /dev/null +++ b/test/rubyschemafix/triple_super.rb @@ -0,0 +1,15 @@ +# triple arm, spelling (b): `def name; super; end` + `attribute :name` + +# column. Bare `super` reaches the generated attribute reader, which reads the +# column-backed @attributes store. There is no separate "column method" — the +# column IS the storage. Expected chain: def -> super -> generated attr reader -> column. +module Spike + class TripleSuper < ApplicationRecord + self.table_name = "spike_triples" + + attribute :name + + def name + super + end + end +end diff --git a/test/rubyschemafix/unrelated_client.rb b/test/rubyschemafix/unrelated_client.rb new file mode 100644 index 000000000..6ef5baa73 --- /dev/null +++ b/test/rubyschemafix/unrelated_client.rb @@ -0,0 +1,9 @@ +# false-binding arm (definitions only): an UNTYPED receiver calls a method spelled like a +# COLUMN-ONLY name (`ref`, the `id: false` table's one column, schema.rb). Nothing proves the +# receiver is that table's model, so the call is a use of the name and never the column's caller. +# Without buildGraph's Section-Ruby byName skip this site binds to the column by name alone. +class UnrelatedClient + def fetch( response ) + response.ref + end +end diff --git a/test/runhintcheck.sh b/test/runhintcheck.sh index 7d3a3d9a5..f6c326daf 100755 --- a/test/runhintcheck.sh +++ b/test/runhintcheck.sh @@ -172,10 +172,15 @@ fi # test/cloneband_harness.cpp is named by test/clonebandcheck.sh and by nothing else. Stems differ, so # this is the MENTION path on a corpus with 255 candidate runner scripts — where a wrong tie-break or # an over-eager match would show up immediately. +# The seed is src/clones.h, the one header the harness includes and calls into (hops=1). It used to be +# src/graph.h, which reached the harness at hops=2 only through by-name edges from std::*.find()/size() calls +# onto graph.h namesakes; the C++ standard-member gate (graph.h StdMemberGate) declines those, so graph.h no +# longer reaches it — the arm still asks the same run= question of the same harness. RA="$( perl -e 'alarm 90; exec @ARGV' "$BIN" "$ROOT" --affected=src/graph.h 2>/dev/null )" -[ "$( runof 'cloneband_harness.cpp' "$RA" )" = "bash test/clonebandcheck.sh" ] \ +RC="$( perl -e 'alarm 90; exec @ARGV' "$BIN" "$ROOT" --affected=src/clones.h 2>/dev/null )" +[ "$( runof 'cloneband_harness.cpp' "$RC" )" = "bash test/clonebandcheck.sh" ] \ && ok "repo: cloneband_harness.cpp -> run=\"bash test/clonebandcheck.sh\"" \ - || no "repo: cloneband_harness.cpp run= wrong: '$( runof 'cloneband_harness.cpp' "$RA" )'" + || no "repo: cloneband_harness.cpp run= wrong: '$( runof 'cloneband_harness.cpp' "$RC" )'" [ "$( runof 'connectcore_harness.cpp' "$RA" )" = "bash test/connectcorecheck.sh" ] \ && ok "repo: connectcore_harness.cpp -> run=\"bash test/connectcorecheck.sh\"" \ || no "repo: connectcore_harness.cpp run= wrong: '$( runof 'connectcore_harness.cpp' "$RA" )'" diff --git a/test/rustqualcheck.sh b/test/rustqualcheck.sh index dafd0deb5..49ae5ae31 100755 --- a/test/rustqualcheck.sh +++ b/test/rustqualcheck.sh @@ -265,8 +265,10 @@ printf '%s' "$EXP" | grep -q 'pub fn new()' \ # RE-PINNED AGAIN at the wave-1 merge (capture-audit L4, H5/M15, floormarkcheck arms (9)/(11)): the targeted # root now carries counts_floor="1" and the resolver gauge pair graph_ambiguous=/graph_unresolved= (the map # header's own fold — 2/0 on this fixture); the row and its one are byte-identical to the pin above. +# RE-PINNED (lane lego-transitive, S2): the targeted row carries its definition line, p="src/lib.rs:10" (struct Widget); +# the inherit edge itself is unchanged — still exactly one implementor. LEGO_NEW="$( run "$FIX" --lego=Shape --no-cache | grep -o '' )" -[ "$LEGO_NEW" = '' ] \ +[ "$LEGO_NEW" = '' ] \ && ok "impl Shape for Widget still yields exactly its inherit edge (call qualifiers did not leak in)" \ || no "inherit edges CHANGED: $LEGO_NEW" if [ -d "$ROOT/$LEGO" ]; then diff --git a/test/safedeletecheck.sh b/test/safedeletecheck.sh index e8a17a464..e7f41d6fd 100755 --- a/test/safedeletecheck.sh +++ b/test/safedeletecheck.sh @@ -147,5 +147,55 @@ else echo " SKIP (6) xmllint not installed — well-formedness not checked" fi +# ── (7) CALLSITE-AT (2026-10-09): a caller row's sites_at= is its call sites as pasteable file:line tokens, cut at 16 per +# row with sites_total= and a next= that lists them all (the uses verb) even when no floor named one. RED on the base +# binary (it printed bare sites_l= lines, uncut). Near miss: an uncut answer carries neither sites_total= nor that next=. +SD7="$( mktemp -d )"; trap 'rm -rf "$WORK" "$SD7"' EXIT +{ printf 'static int sdmany( int x ) { return x; }\nint sdcaller( void )\n{\n int s = 0;\n'; i=0; while [ $i -lt 20 ]; do printf ' s += sdmany( %d );\n' $i; i=$((i+1)); done; printf ' return s;\n}\n'; } > "$SD7/m.c" +printf 'static int sdone( int x ) { return x; }\nint sdonecaller( void ) { return sdone( 1 ) + sdone( 2 ); }\n' > "$SD7/o.c" +SD7_OUT="$( "$BIN" "$SD7" --no-cache --safe-delete=sdmany 2>/dev/null )" +SD7_ROW="$( printf '%s' "$SD7_OUT" | sed 's///' | grep -oE ']*/>' )" +SD7_AT="$( printf '%s' "$SD7_ROW" | grep -oE ' sites_at="[^"]*"' | sed -e 's/^ sites_at="//' -e 's/"$//' )" +{ [ "$( printf '%s' "$SD7_AT" | wc -w | tr -d ' ' )" = 16 ] && [ "${SD7_AT%% *}" = "m.c:5" ] && printf '%s' "$SD7_ROW" | grep -q ' sites_total="20"' \ + && printf '%s' "$SD7_OUT" | grep -o ']*>' | grep -q ' next="--uses=sdmany"'; } \ + && ok "(7) a 20-site caller lists 16 file:line sites, sites_total=\"20\", and next=\"--uses=sdmany\" lists every one" \ + || no "(7) cut safe-delete row/root: $SD7_ROW $( printf '%s' "$SD7_OUT" | grep -o ']*>' )" +SD7_FULL="$( "$BIN" "$SD7" --no-cache --safe-delete=sdmany --legend=full 2>/dev/null )" +{ printf '%s' "$SD7_FULL" | grep -o '' | grep -q 'next= is that uses call' && printf '%s' "$SD7_FULL" | grep -q 'sites_total=N' \ + && printf '%s' "$SD7_OUT" | grep -o '' | grep -q 'next=: ' && printf '%s' "$SD7_FULL" | xmllint --noout - 2>/dev/null; } \ + && ok "(7) both legends define that next= and sites_total= where they ride (full: well-formed)" || no "(7) next=/sites_total= undefined in a legend of the cut answer" +SD7_ONE="$( "$BIN" "$SD7" --no-cache --safe-delete=sdone 2>/dev/null )" +{ printf '%s' "$SD7_ONE" | grep -q '' \ + && ! printf '%s' "$SD7_ONE" | grep -o ']*>' | grep -q ' next=' && ! printf '%s' "$SD7_ONE" | grep -q 'sites_total'; } \ + && ok "(7) near miss: an uncut answer carries sites_at= with neither sites_total= nor a next=" \ + || no "(7) uncut answer: $SD7_ONE" + +# (8) fix round 1 (review item 1), the --callers SA12 twin: in a caller that also holds a real call, a value use, &f, a +# block-scope prototype, a string and two comments spelled like the name are NOT call sites. RED when the call-role +# filter (editcheck.h forEachCallSite) is dropped. +cat > "$SD7/n.c" <<'SDEOF' +typedef int (*sdfp)( int ); +static int sdnoise( int x ) { return x; } +int sdnoisecaller( void ) +{ + /* sdnoise( 3 ) in a comment */ + // sdnoise( 4 ) in a line comment + const char *s = "sdnoise( 5 ) in a string"; + int sdnoise( int ); + sdfp p = sdnoise; + sdfp q = &sdnoise; + return sdnoise( 6 ) + ( s != 0 ) + p( 0 ) + q( 0 ); +} +SDEOF +SD8="$( "$BIN" "$SD7" --no-cache --safe-delete=sdnoise 2>/dev/null )" +SD8_ALL="$( printf '%s' "$SD8" | sed 's///' | grep -oE ' sites_at="[^"]*"' | sed -e 's/^ sites_at="//' -e 's/"$//' | tr '\n' ' ' | sed 's/ $//' )" +if ! printf '%s' "$SD8" | grep -q '"$TMP/wf.xml" 2>/dev/null "$WFQ/outer.hpp" +"$BIN" "$WFQ" --expand=Outer --no-cache --legend=full >"$TMP/wf.xml" 2>/dev/null ]*id="outer.hpp::' "$TMP/wf.xml"; then ok "(E2) whole-file --expand anchor rows carry sc= and no path-repeating id=" @@ -167,12 +173,12 @@ if grep -q 'mode="whole-file"' "$TMP/wf.xml"; then && ok "(E2) the whole-file root defines the composition (sc= is not an undefined first-screen attribute)" \ || no "(E2) the whole-file serving carries no legend defining sc=/the composition" # the composed id still RESOLVES, which is the whole contract - "$BIN" test/nestedqualfix --expand='outer.hpp::Outer::Outer' --no-cache >"$TMP/wf2.xml" 2>/dev/null "$TMP/wf2.xml" 2>/dev/null wrapper, no p= repetition) — pinned so the design # stays a decision rather than an omission nobody re-checked. @@ -205,7 +211,7 @@ else no "(E3) the probe did not serve the bundle shape (root says $E3_MODE) — this arm proves nothing" fi # …and the positive control: a REAL whole-file serving still selects expand-file. -"$BIN" test/nestedqualfix --expand=Outer --legend=compact --no-cache >"$TMP/e3b.xml" 2>/dev/null "$TMP/e3b.xml" 2>/dev/null "$R/alt.cpp" cat > "$R/DOC.md" <<'EOF' # Design @@ -123,10 +126,13 @@ grep -q 'head_labels="index"' "$W" \ && ok '§A7: head_labels="index" — HEAD rows are the parsed answer, and say so' \ || { no '§A7: head_labels="index" missing'; grep -o ']*>' "$W"; } -# ── §A7(ii) the ref-blob side keeps its LEXICAL heuristic — and now reads the same shape correctly ────── -grep -q '^feat-doc def DOC\.md:' "$TMP/rows" \ - && ok '§A7: a branch DOC quoting the signature still reads kind="def" (the lexical residual is intact, and now handles noexcept)' \ - || { no '§A7: the feat-doc DOC.md row is not a def row (the blob-side heuristic still misreads the house style)'; cat "$TMP/rows"; } +# ── §A7(ii) the ref-blob side reads the house style correctly ────────────────────────────────────────── +# Fix round 2 (rule 4, by design): this arm asserted the lexical residual — "a branch DOC quoting the signature still reads +# kind="def"" — and that residual is what the parser removed. Its two halves now: the doc's quoted signature is a ref on the +# branch (as on HEAD), and the noexcept-terminated definition on the branch is a def (the chooser keeps the house style). +{ grep -q '^feat-doc ref DOC\.md:' "$TMP/rows" && ! grep -q '^feat-doc def DOC\.md:' "$TMP/rows" && grep -q '^feat-doc def alt\.cpp:1$' "$TMP/rows"; } \ + && ok '§A7: on the branch, the DOC quoting the signature reads kind="ref" (as on HEAD) and the noexcept-terminated alt.cpp definition reads kind="def"' \ + || { no '§A7: the branch DOC.md row is still a def, or the noexcept-terminated branch definition lost its label'; cat "$TMP/rows"; } # ── §A7(iii) the scan denominator names its noun ──────────────────────────────────────────────────────── { grep -q 'refs_scanned="' "$W" && ! grep -q ']* refs="' "$W"; } \ @@ -134,16 +140,16 @@ grep -q '^feat-doc def DOC\.md:' "$TMP/rows" \ || { no '§A7: whereis root still spells the scan denominator refs='; grep -o ']*>' "$W"; } # determinism + well-formedness of the changed verb -# TWIN on the default listing: the same def rows (HEAD's exactly the index def site, the branch doc's lexical def), no +# TWIN on the default listing: the same def rows (HEAD's exactly the index def site, the branch's alt.cpp definition), no # ref row printed, and the ref rows of the whole list — core.cpp:11 among them — counted exactly by . WD="$TMP/wd.xml" "$BIN" "$R" --whereis=parseArgs --limit=200 >"$WD" 2>/dev/null tr '<' '\n' <"$WD" | sed -n 's/^hit ref="\([^"]*\)".* p="\([^"]*\)" l="\([0-9]*\)" kind="\([a-z]*\)".*/\1 \4 \2:\3/p' >"$TMP/rowsd" NREFALL="$( grep -c ' ref ' "$TMP/rows" )" { [ "$( grep ' def ' "$TMP/rowsd" )" = "$( grep ' def ' "$TMP/rows" )" ] && ! grep -q ' ref ' "$TMP/rowsd" \ - && [ "$( sed -n 's/^HEAD //p' "$TMP/rowsd" | grep '^def ' )" = "def core.cpp:4" ] && grep -q '^feat-doc def DOC\.md:' "$TMP/rowsd" \ + && [ "$( sed -n 's/^HEAD //p' "$TMP/rowsd" | grep '^def ' )" = "def core.cpp:4" ] && grep -q '^feat-doc def alt\.cpp:1$' "$TMP/rowsd" \ && grep -q '^HEAD ref core\.cpp:11$' "$TMP/rows" && grep -q "]*>' "$WD"; } "$BIN" "$R" --whereis=parseArgs --limit=200 --whereis-listing=all >"$TMP/w2.xml" 2>/dev/null diff --git a/test/selfcheckcheck.sh b/test/selfcheckcheck.sh index 4167ee89e..a6b1e3f75 100755 --- a/test/selfcheckcheck.sh +++ b/test/selfcheckcheck.sh @@ -152,6 +152,7 @@ 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", + "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 # really is a true invariant (the re-test below is dead defensive code that should eventually be deleted, not diff --git a/test/shapingflagcheck.sh b/test/shapingflagcheck.sh index 5ac0b39c8..b8b8d01ff 100755 --- a/test/shapingflagcheck.sh +++ b/test/shapingflagcheck.sh @@ -111,12 +111,16 @@ git status --porcelain 2>/dev/null | grep -vE '^\?\? (build|asan|tsan)' | LC_ALL # decide whether the ride-along note='s bytes should be charged to the search/verdict, the SAME predicate # `noteAppliesToBundle` already evaluates further down for the SAME two verbs (--expand/--outline). It does # not change which verbs honour --top-k, only fixes an existing verb's ceiling math, so this is a re-pin. +# expand-lean-k64 (2026-10-08, 21->22 / 14->15): ONE new read of each, the same kind as V1's. `leanEligible` (src/main.cpp) +# is the multi-definition twin of V1's exact-name predicate — it reads cfg.topKExplicit and cfg.maxTokens to decide whether +# the lean default applies (an explicit --top-k and a --max-tokens-sized map both stand it aside). --expand/--outline +# already honour both flags in kShapingVerbs, so this is a re-pin, not a column change. MAXSITES="$( grep -c 'cfg\.maxTokens\|c\.maxTokens' src/main.cpp src/verbs_*.h src/mcpserver.h 2>/dev/null | awk -F: '{s+=$2} END{print s+0}' )" TOPSITES="$( grep -c 'cfg\.topK\|c\.topK' src/main.cpp src/verbs_*.h src/mcpserver.h 2>/dev/null | awk -F: '{s+=$2} END{print s+0}' )" -[ "$MAXSITES" = 21 ] && ok "(A) --max-tokens has 21 read sites outside cli.h (grep 'cfg\\.maxTokens' src/main.cpp src/verbs_*.h src/mcpserver.h)" \ - || no "(A) --max-tokens read sites moved 21 -> $MAXSITES: a verb gained or lost the budget, so kShapingVerbs' honorsMaxTokens column must be re-decided (and this number re-pinned)" -[ "$TOPSITES" = 14 ] && ok "(A) --top-k has 14 read sites outside cli.h" \ - || no "(A) --top-k read sites moved 14 -> $TOPSITES: re-decide kShapingVerbs' honorsTopK column and re-pin this number" +[ "$MAXSITES" = 22 ] && ok "(A) --max-tokens has 22 read sites outside cli.h (grep 'cfg\\.maxTokens' src/main.cpp src/verbs_*.h src/mcpserver.h)" \ + || no "(A) --max-tokens read sites moved 22 -> $MAXSITES: a verb gained or lost the budget, so kShapingVerbs' honorsMaxTokens column must be re-decided (and this number re-pinned)" +[ "$TOPSITES" = 15 ] && ok "(A) --top-k has 15 read sites outside cli.h" \ + || no "(A) --top-k read sites moved 15 -> $TOPSITES: re-decide kShapingVerbs' honorsTopK column and re-pin this number" # no OTHER file may read them: a third file would be a verb family this table has never heard of. # 2026-08-29 main.cpp split: src/verbs_*.h are SECTIONS of main.cpp's own TU (RIPWIRE_MAIN_TU-guarded), # so they count as main.cpp in this derivation — the counts above sweep them, the exclusion below too. diff --git a/test/showcase_capture.py b/test/showcase_capture.py index 745f4c257..772ec407d 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -352,6 +352,7 @@ def add(section, cmd, what, **opts): S1 = "understand a codebase cold" add(S1, f"{BIN} .", "The default ranked symbol map — start here when landing cold in a repo.") add(S1, f"{BIN} . --top-k=5", "Same map, capped to the 5 highest-ranked symbols.") +add(S1, f"{BIN} . --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.") add(S1, f"{BIN} . --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.") add(S1, f"{BIN} . --top-k=0", "--top-k=0 with NO payload verb asked for — REFUSES (exit 1) naming the payload verbs, never an empty map.") add(S1, f"{BIN} . --max-tokens=1500", "SHAPE the map to fit ~1500 tokens (binary-search top-K).") @@ -404,7 +405,7 @@ def add(section, cmd, what, **opts): S3 = "zoom the detail ladder" add(S3, f'{BIN} . --for="pagerank power iteration" --detail=2', "Importance-weighted detail: FULL bodies for top-2, signatures for the rest.") -add(S3, f"{BIN} . --pack-signatures --top-k=10", "Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.") +add(S3, f"{BIN} . --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.") add(S3, f"{BIN} . --outline=rankGraphTeleport --top-k=0", "Control-flow skeleton of one symbol, payload-only via the new --top-k=0.") add(S3, f"{BIN} . --outline=rankGraphTeleport:1-10 --top-k=0", "CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).") add(S3, f"{BIN} . --expand=rankGraphTeleport --top-k=0", "Full body + inline callee signatures.") @@ -439,6 +440,9 @@ def add(section, cmd, what, **opts): add(S4, f"{BIN} . --quality-delta", onTree( "On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.", "Recorded against a DIRTY tree, so any row below is a real regression in the working copy — the recorded exit code says whether anything gated. The sandbox section below shows the same gating shape on a known, deliberate edit.")) +add(S4, f"{BIN} . --quality-delta --show-stale", onTree( + "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.", + "The same verdict with every stale acknowledgement printed as an row and the full header — recorded against a DIRTY tree, so the rows above it are the working copy's. Refused without --quality-delta.")) add(S4, f"{BIN} . --edit-check=rankGraphTeleport", onTree( "Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).", "Fast per-symbol post-edit contract check vs git HEAD — recorded against a DIRTY tree, so the verdict describes the working copy, not HEAD alone, and the recorded exit code is the working copy's.")) @@ -490,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} . --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.") add(S4, f"{BIN} . --flags --flip=RIPWIRE_ASAN", "Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.") diff --git a/test/staleackcheck.sh b/test/staleackcheck.sh index a42491369..3e85bba7c 100755 --- a/test/staleackcheck.sh +++ b/test/staleackcheck.sh @@ -93,7 +93,10 @@ fi cat > "$REPO/src/lib.cpp" <<'EOF' int stable( int a ) { return a + 1; } EOF -run --quality-delta >"$TMP/targetgone" 2>/dev/null; rcTG=$? +# lane lean-trio (idea #11): a regression-free answer collapses its stale rows to counts by default; the row arms below +# read them through the recovery flag, and arm (2c) asserts the collapse itself on the same tree. +run --quality-delta --show-stale >"$TMP/targetgone" 2>/dev/null; rcTG=$? +run --quality-delta >"$TMP/targetgone_c" 2>/dev/null; rcTGc=$? if grep -q 'stale="[1-9]' "$TMP/targetgone" && grep -q ']*why="target-gone"' "$TMP/targetgone"; then ok "(2) a deleted acked symbol is reported stale>0 including why=\"target-gone\" for its complexity ack" else @@ -106,12 +109,29 @@ else no "(2) exit code changed from the no-stale run ($rcClean -> $rcTG) — disclosure must never gate" fi +# ── (2c) the COLLAPSE (lane lean-trio, idea #11): with no regression the default answer prints no row, keeps stale= +# (the same count), splits it by kind in stale_by_kind=, names the recovery call in stale_next=, and exits the same. +# RED on main 0852bc0f: the default answer carries the rows and no stale_by_kind=. +tgStale="$( grep -o 'stale="[0-9]*"' "$TMP/targetgone" | head -1 )" +if [ -n "$tgStale" ] && grep -q "$tgStale" "$TMP/targetgone_c" && ! grep -q ']*>\|]*/>' "$TMP/targetgone_c" +fi +sed 's//dev/null 2>&1 && no "(2c) --show-stale alone was accepted" || ok "(2c) --show-stale alone is refused (it modifies --quality-delta)" + # ── (3) FINDING-GONE: knotty() survives (same canonId) but is simplified back under the complexity bar ── cat > "$REPO/src/lib.cpp" <<'EOF' int stable( int a ) { return a + 1; } int knotty( int a, int b, int c ) { return a + b + c; } EOF -run --quality-delta >"$TMP/findinggone" 2>/dev/null; rcFG=$? +run --quality-delta --show-stale >"$TMP/findinggone" 2>/dev/null; rcFG=$? if grep -q 'stale="[1-9]' "$TMP/findinggone" && grep -q ']*why="finding-gone"' "$TMP/findinggone"; then ok "(3) a simplified (still-existing) symbol is reported stale>0 including why=\"finding-gone\" for its complexity ack" else @@ -124,6 +144,26 @@ else no "(3) exit code changed from the no-stale run ($rcClean -> $rcFG) — disclosure must never gate" fi +# ── (2d) fix round 1 (owner ruling 5, rv-lean-trio item 5): a fold ships only when it PAYS (a quarter of the bytes, in both +# postures). ONE stale row on a dirty, regression-free tree costs less than the collapse's own attributes and legend +# sentence, so the default answer IS the full document — byte-identical to --show-stale, the row printed — in +# both postures. RED on 33873861: the default collapsed the one row (fx: 1512 B collapsed vs 1481 B full). +run --quality-delta >"$TMP/fg_default" 2>/dev/null; rcFGd=$? +run --quality-delta --legend=full >"$TMP/fg_full" 2>/dev/null +run --quality-delta --show-stale --legend=full >"$TMP/fg_full_show" 2>/dev/null +fgStale="$( grep -o 'stale="[0-9]*"' "$TMP/findinggone" | head -1 )" +if [ "$fgStale" = 'stale="1"' ] && grep -q 'at="[0-9a-f]*+dirty' "$TMP/findinggone"; then + if cmp -s "$TMP/fg_default" "$TMP/findinggone" && cmp -s "$TMP/fg_full" "$TMP/fg_full_show" && grep -q ']*>' "$TMP/fg_default" "$TMP/fg_full" + fi +else + no "(2d) premise: the finding-gone tree should be dirty with exactly one stale ack (got $fgStale)" +fi + # ── (5) M21(a): a row NAMES the ack that went stale, wherever the tree can still name it ────────── # capture-audit 2026-09-04 (lens 0 M0-2). `` # is a 16-hex hash of an identity the reader does not have: to act on it — retire the ack, or look at why diff --git a/test/stdmembergatecheck.sh b/test/stdmembergatecheck.sh new file mode 100755 index 000000000..2cbe23660 --- /dev/null +++ b/test/stdmembergatecheck.sh @@ -0,0 +1,361 @@ +#!/usr/bin/env bash +# stdmembergatecheck.sh — a C++ call named like a standard container or string member, on a receiver no rule ties to an in-tree class, never +# binds to an in-tree namesake by its name alone. +# +# test/stdmembergatecheck.sh # uses build/ripwire on test/stdmembergatefix +# RIPWIRE_BIN=asan/ripwire test/stdmembergatecheck.sh +# test/stdmembergatecheck.sh build_base/ripwire # the RED run (a pre-change binary) +# +# THE DEFECT. A C++ member call whose receiver no rule typed — `out.push_back( c )` on a `std::vector&` parameter, +# `s.size()` on a `std::string`, `getV().empty()` — is name-only, and the ladder kept every same-file, same-directory and +# included-file definition of the name as a via="name" row. For standard container names those rows were almost all +# calls on the standard types: on this repository's own map 14 of the top 20 PageRank rows were such names +# (svector::push_back, WidePath::c_str, ElixirResolver::append, every `empty` and `find` under src/), and +# `--callees=escapeXml` listed 8 svector/TreeIndexMemo/HunkBuffer rows beside its 4 real callees. +# +# THE FIX (graph.h StdMemberGate; the names are data, src/externalnames.h kCppStdMemberNames). Such a call is DECLINED +# after the ladder decided it — no edge, the header's declined=, declined_calls= on every definition it would have bound, +# a legend clause of its own — and the caller's --callees answer names those calls once in a line. A +# receiver a rule types (Rule 1/2/2b, `this`, an implicit-this call, FE-B's proof) is never touched, and neither is one +# whose recorded written type names an in-tree class that may define the callee (an alias of the in-tree container, the +# pointee of a smart pointer's `->`, a namespace-scope variable's type — arms C, H, W). +# +# THE FIXTURE (test/stdmembergatefix/, see its README). Arms: +# (A) typed receivers keep their edges, unhedged: a typed local, parameter, pointer, member field, a smart pointer's `->`, +# `this->` and an implicit-this call inside the class, `Solo so; so.append()` (the tree's ONLY append) and `Pool& p` +# (B) std-written or untyped receivers are declined and disclosed: std::vector local / parameter / field, `auto`, a call +# result, a std::string's append (ONE in-tree definition), a std::vector's empty (reaching an anonymous-namespace class +# in another file), a local declared with an alias of a standard map — each callees answer has no in-tree row, the +# exact declined_calls= and the exact line; a call the ladder itself declines (tier 3, far/) is on the line too +# (C) a local declared with an alias of the in-tree Vec keeps the ladder's via="name" hedge (the alias's cone reaches Vec) +# (H) field receivers: `this->items_` and `b.items_` (Box& b) written as Vec keep the hedge, std fields are declined; a +# subscript of a std::vector> reads its element type and keeps the hedge (arm X); a dereference, a range-for +# `auto` and a template parameter are the stated floor (declined) +# (W) written types beyond Rule 2's tables (wrap/): a `->` through std::unique_ptr / shared_ptr / optional of ns::List, an +# in-tree SP with its own operator->, a Qt or boost pointer (a class the tree does not define), a smart-pointer root +# one field hop off, namespace-scope / file-static / extern variables written ns::List, and a typedef or using alias of +# a std smart pointer (or of an undefined wrapper) keep the hedge; std-argument smart pointers, raw pointers to std +# containers (an alias of one included), `.` on std::vector, an in-tree class with no operator->, a +# std-written global and a global hidden by a local or a field stay declined; call results, an iterator, an +# unqualified template's arguments, a std::optional member field and a typedef inside a function body are the stated +# floor (declined) +# (X) element receivers (elem/): ONE element access — `c[ k ]`, `c.at( k )`, `c.front()`, `c.back()` — of a container whose +# recorded written type is a std sequence container or map of EvalString keeps the hedge: a class-scoped typedef'd map +# field through a std::unique_ptr local (`rule->bindings_[ k ].empty()`), a std::map / std::vector field, a local map, a +# const map parameter's at(), an unordered_map, a vector's back()/front(), a typedef (and a typedef of it) of a vector, +# an extern map, a bare and a `this->` field inside the class; near-misses stay declined: an element written in std +# (map, vector, map> — the element's own member), an element class with +# no candidate, the container's own member, an in-tree template's own operator[], a local hiding the field; two element accesses, an iterator's ->second and a range-for `auto` are +# the stated floor (declined) +# (D) controls: a name outside the table (`grow`) keeps the ladder; a bare `size( b )` binds the free function; a C +# function-pointer call (C has no member functions) and the Rust by-name split (stated scope) are unchanged +# (E) the callers side: Vec::push_back keeps exactly its typed callers and hedges and counts the ten declined calls; the one-def +# Solo::append keeps soloTyped and counts stdAppend; Pool::clear keeps poolTyped and counts two +# (F) disclosure surfaces: the map header's declined= equals the census's declined bucket, the full map legend, the +# callers legend and the callees legend each carry their clause, the compact callees legend defines stdm, the +# columnar callees form carries neither the line nor its sentence, JSON and the MCP find_symbol twin carry "stdm" +# (G) dead-code safety: on a copy outside the fixture paths (which are never dead by design), --quality-delta counts the +# definitions whose only callers were declined as declined-call-excluded=, and --safe-delete=Shelf::empty carries +# declined_calls="3" with dead_code_candidate="0" +# (S) 0 bytes on a tree the gate never declined in: test/builtinbindfix's map and callers legends carry no C++ clause +# (K) the predicates can fail: a via="name" row and a missing line are both caught +# (L) determinism x2, xmllint on every answer whose new clause rides, no degrade alert on stderr +# +# Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" # a gate that builds a repo must not inherit GIT_DIR/GIT_WORK_TREE (gitenvhermeticcheck D) +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary +CORPUS="$ROOT/test/stdmembergatefix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$CORPUS" ] || { echo "fixture missing: $CORPUS"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +echo "stdmembergatecheck: BIN=$BIN CORPUS=$CORPUS" + +cd "$CORPUS" || exit 2 # every file:name selector below is relative to the root `.` +rw(){ "$BIN" . --no-cache "$@" 2>/dev/null; } + +# ── helpers ─────────────────────────────────────────────────────────────────────────────────────────────────── +root_tag(){ grep -oE "<$2( [^>]*)?>" "$1" | head -1; } # first start tag +attr(){ printf '%s' "$1" | grep -oE " $2=\"[^\"]*\"" | head -1 | sed -E 's/^[^"]*"([^"]*)"$/\1/'; } +# the rows of a callers/callees document as sorted "p=:n=" items, "~" appended to a via="name" row +rows(){ grep -oE ']*/>' "$1" | python3 -c ' +import re, sys +out = set() +for line in sys.stdin: + n = re.search( r" n=\"([^\"]*)\"", line ); p = re.search( r" p=\"([^\"]*)\"", line ) + if n and p: + out.add( p.group( 1 ) + ":" + n.group( 1 ) + ( "~" if " via=\"name\"" in line else "" ) ) +print( " ".join( sorted( out ) ) )'; } +# the line as "names:calls" ("" when absent) +stdm(){ local t; t="$( grep -oE ']*/>' "$1" | head -1 )"; [ -n "$t" ] && printf '%s:%s' "$( attr "$t" n )" "$( attr "$t" calls )"; } +# hier(verb, selector, want rows, want declined_calls, want stdm): the exact rows, declined_calls= and line +hier(){ + local verb="$1" sel="$2" want="$3" wantd="$4" wants="$5" doc="$TMP/hier.xml" + rw --"$verb"="$sel" >"$doc" + local R; R="$( root_tag "$doc" "$verb" )" + HIER_GOT="rows [$( rows "$doc" )] declined_calls=\"$( attr "$R" declined_calls )\" stdm=\"$( stdm "$doc" )\"" + [ -n "$R" ] && [ "$( rows "$doc" )" = "$want" ] && [ "$( attr "$R" declined_calls )" = "$wantd" ] && [ "$( stdm "$doc" )" = "$wants" ] +} +check(){ # label | verb | selector | want rows | want declined_calls | want stdm + if hier "$2" "$3" "$4" "$5" "$6"; then ok "$1"; else no "$1 — got $HIER_GOT"; fi +} +stats(){ grep -oE '' "$1" | head -1; } +gauge(){ printf '%s' "$1" | grep -oE " $2=[0-9]+" | head -1 | grep -oE '[0-9]+$'; } +disp_in(){ grep -m1 '^# dispositions ' "$1" | grep -oE " $2=[0-9]+" | head -1 | grep -oE '[0-9]+$'; } +mcp_text(){ + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' "$1" \ + | "$BIN" --mcp 2>/dev/null | tail -1 | python3 -c ' +import sys, json +r = json.load( sys.stdin ) +print( "__ERROR__:" + r[ "error" ].get( "message", "" ) if "error" in r else r[ "result" ][ "content" ][ 0 ][ "text" ] ) +' +} +call(){ printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$1" "$2"; } + +# the census and the full-legend map the header arms read — written FIRST, so no arm reads a file that does not exist yet +"$BIN" . --no-cache --pin-census="$TMP/c.tsv" --legend=full >"$TMP/map.xml" 2>"$TMP/err" || no "(F) the map run exited non-zero" +[ -s "$TMP/c.tsv" ] || no "the census run wrote no census — the census arm below would be vacuous" +[ -s "$TMP/map.xml" ] || no "the map run wrote nothing — every header arm below would be vacuous" + +PB='lib/vec.h:8:push_back lib/vec.h:9:push_back' # Vec's two push_back overloads, as a callees answer lists them + +# ── (A) typed receivers keep their edges ────────────────────────────────────────────────────────────────────── +echo "=== (A) a receiver a rule types keeps its edge, unhedged ===" +check "(A) typed local Vec v; v.push_back()" callees typedLocal "$PB" '' '' +check "(A) typed parameter Vec& v" callees typedParam "$PB" '' '' +check "(A) typed pointer Vec* p; p->push_back()" callees typedPtr "$PB" '' '' +check "(A) member field Vec items_ (Rule 2b)" callees app/use.cpp:add "$PB" '' '' +check "(A) smart pointer field owned_->push_back() (Rule 2b, ->)" callees app/use.cpp:addOwned "$PB" '' '' +check "(A) implicit-this and this-> calls inside Vec" callees pushTwice "$PB" '' '' +check "(A) Solo so; so.append() keeps the tree's only append" callees soloTyped 'lib/vec.h:27:append' '' '' +check "(A) Pool& p; p.clear()" callees poolTyped 'lib/vec.h:22:clear' '' '' + +# ── (B) std-written or untyped receivers are declined and disclosed ─────────────────────────────────────────── +echo "=== (B) a receiver written in std, or typed by nothing, binds no in-tree namesake ===" +check "(B) std::vector v; v.push_back()" callees stdLocal '' 1 'push_back:1' +check "(B) std::vector& out / const std::string& s: push_back, reserve(s.size()), clear" \ + callees stdParam '' 3 'clear,push_back,size:3' +check "(B) auto v = std::vector(); v.push_back()" callees autoLocal '' 1 'push_back:1' +check "(B) getV().push_back() — the call result binds no Vec, getV stays" \ + callees chained 'app/use.cpp:28:getV' 1 'push_back:1' +check "(B) std::string& s; s.append() — ONE in-tree append, still declined" \ + callees stdAppend '' 1 'append:1' +check "(B) std::vector field raw_.push_back() (a field written in std)" \ + callees app/use.cpp:addRaw '' 1 'push_back:1' +check "(B) w.empty() on a std::vector reaches neither Vec/Pool nor another file's anonymous-namespace Shelf" \ + callees stdEmpty '' 1 'empty:1' +check "(B) StdMap m (an alias of std::map); m.clear() — the alias's cone reaches no candidate" \ + callees stdAliasClear '' 1 'clear:1' +check "(B) same-file anonymous-namespace Shelf::empty is not bound by w.empty() on a std::vector" \ + callees anyEmpty '' 1 'empty:1' +check "(B) the ladder's own tier-3 decline (no namesake in reach) is named on the line too" \ + callees farEmpty '' 1 'empty:1' + +# ── (C) an alias of the in-tree container keeps the hedge ──────────────────────────────────────────────────── +echo "=== (C) a written non-std type whose cone reaches a candidate keeps the ladder's hedge ===" +check "(C) SmallV a (an alias of Vec); a.push_back() stays via=\"name\" on Vec" \ + callees aliasTyped 'lib/vec.h:8:push_back~ lib/vec.h:9:push_back~' '' '' + +# ── (H) field and expression receivers (sib/) ─────────────────────────────────────────────────────────────── +echo "=== (H) a field written as the in-tree Vec keeps the hedge; std fields and expression receivers are declined ===" +check "(H) this->items_.push_back() (a field written Vec) keeps the ladder's via=\"name\" hedge" \ + callees sib/sib.cpp:thisField 'lib/vec.h:8:push_back~ lib/vec.h:9:push_back~' '' '' +check "(H) b.items_.push_back() on Box& b (one field hop off a typed root) keeps the hedge" \ + callees fieldOfVar 'lib/vec.h:8:push_back~ lib/vec.h:9:push_back~' '' '' +check "(H) this->raw_.push_back() (a field written in std) is declined" \ + callees sib/sib.cpp:thisRaw '' 1 'push_back:1' +check "(H) b.raw_.push_back() (a std field one hop off a typed root) is declined" \ + callees fieldOfVarRaw '' 1 'push_back:1' +check "(H) STATED FLOOR: ( *p ).push_back() — a dereference carries no written type, declined" \ + callees derefPtr '' 1 'push_back:1' +check "(H) vs[ 0 ].push_back() on std::vector>& — the element type reaches Vec, keeps the hedge (arm X)" \ + callees subscript 'lib/vec.h:8:push_back~ lib/vec.h:9:push_back~' '' '' +check "(H) STATED FLOOR: for( auto& v : vs ) v.push_back() — a range-for auto, declined" \ + callees rangeFor '' 1 'push_back:1' +check "(H) template parameter C& c; c.push_back() — the receiver may be anything, declined" \ + callees tmplParam '' 1 'push_back:1' + +# ── (W) written types beyond Rule 2's tables (wrap/) ───────────────────────────────────────────────────────── +echo "=== (W) a recorded written type that names ns::List keeps the hedge; one that does not stays declined ===" +LS='wrap/list.h:9:size~'; LF='wrap/list.h:10:find~'; LR='wrap/list.h:11:front~' +check "(W) std::unique_ptr up; up->size() — the smart pointer's argument keeps the hedge" callees uniqueLocal "$LS" '' '' +check "(W) const std::shared_ptr& p; p->size()" callees sharedParam "$LS" '' '' +check "(W) std::optional o; o->find()" callees optionalLocal "$LF" '' '' +check "(W) SP& p; p->size() — an in-tree class with its own operator->" callees inTreeWrapper "$LS" '' '' +check "(W) SP p; p->front() (a local)" callees inTreeWrapperLocal "$LR" '' '' +check "(W) QSharedPointer& p; p->front() — a class the tree does not define, its -> unknown" callees qtWrapper "$LR" '' '' +check "(W) boost::shared_ptr& p; p->find()" callees boostWrapper "$LF" '' '' +check "(W) std::unique_ptr b; b->items_.size() — a smart-pointer root, one field hop" callees smartRootField "$LS" '' '' +check "(W) ns::List g_list; g_list.size() — a namespace-scope variable" callees globalVar "$LS" '' '' +check "(W) namespace app { ns::List nsVar; } nsVar.front()" callees nsGlobalVar "$LR" '' '' +check "(W) static ns::List fileStatic; fileStatic.find()" callees fileStaticVar "$LF" '' '' +check "(W) extern ns::List g_ext (declared in the included list.h); g_ext.size()" callees externVar "$LS" '' '' +check "(W) near-miss: std::unique_ptr p; p->size() stays declined" callees uniqueStd '' 1 'size:1' +check "(W) near-miss: std::vector* pv; pv->size() stays declined" callees rawStdPtr '' 1 'size:1' +check "(W) near-miss: std::unique_ptr> up; up->size() stays declined" callees uniqueStdVec '' 1 'size:1' +check "(W) near-miss: std::vector v; v.size() — the container's own member, declined" callees vectorOfList '' 1 'size:1' +check "(W) near-miss: std::vector* pv; pv->size() — a raw pointer to the container, declined" callees rawVectorOfListPtr '' 1 'size:1' +check "(W) near-miss: Plain* p; p->size() — an in-tree class with no operator-> reaches no candidate" callees plainPtr '' 1 'size:1' +check "(W) near-miss: std::string g_str; g_str.size() — a namespace-scope variable written in std" callees globalStd '' 1 'size:1' +check "(W) near-miss: a local std::string g_list hides the namespace-scope ns::List g_list" callees shadowedGlobal '' 1 'size:1' +check "(W) near-miss: a std::string field g_list hides the namespace-scope ns::List g_list" callees fieldHidesGlobal '' 1 'size:1' +check "(W) near-miss: a std::vector field g_list (no recorded field type) hides it too" callees stdFieldHidesGlobal '' 1 'size:1' +check "(W) STATED FLOOR: h.list().size() — a call result carries no recorded type, declined" callees callResult 'wrap/use.cpp:53:list~' 1 'size:1' +check "(W) v.front().size() on std::vector& — the element keeps the hedge (arm X); v.front() itself is the container's member" callees frontOfVector "$LS" 1 'front:1' +check "(W) STATED FLOOR: auto it = v.begin(); it->size() — an iterator, declined" callees iteratorArrow '' 1 'size:1' +check "(W) STATED FLOOR: QList& l; l.size() — an unqualified template's arguments are not recorded" callees unqualifiedTemplate '' 1 'size:1' +check "(W) typedef std::shared_ptr ListSP; ListSP p; p->size() — a typedef reads its recorded alias target" callees typedefSharedLocal "$LS" '' '' +check "(W) const ListSP& p; p->size() (the typedef, a parameter)" callees typedefSharedParam "$LS" '' '' +check "(W) typedef std::unique_ptr ListUP; ListUP p; p->front()" callees typedefUniqueLocal "$LR" '' '' +check "(W) ListUP& p; p->find() (the unique_ptr typedef, a parameter)" callees typedefUniqueParam "$LF" '' '' +check "(W) using ListUsingSP = std::shared_ptr; p->size() — the same rule for using" callees usingSharedLocal "$LS" '' '' +check "(W) using QtListSP = QSharedPointer; p->front() — an alias of an undefined wrapper" callees usingQtAliasLocal "$LR" '' '' +check "(W) a field typed by a class-scoped typedef of std::shared_ptr; p_->size()" callees typedefField "$LS" '' '' +check "(W) Owner4::Ptr p; p->size() — a class-scoped typedef, a parameter" callees classScopedTypedef "$LS" '' '' +check "(W) accepted cost: typedef std::unique_ptr StrUP; p->size() keeps the hedge (the record holds no argument; as using StrPtr)" callees typedefStrUP "$LS" '' '' +check "(W) near-miss: typedef std::vector IntVec; IntVec v; v.size() stays declined" callees typedefVecDot '' 1 'size:1' +check "(W) near-miss: IntVec* p; p->size() — a raw pointer to a std alias, declined" callees typedefVecPtrArrow '' 1 'size:1' +check "(W) near-miss: using IntVecUsing = std::vector; IntVecUsing* p; p->size() — declined by the same rule" callees usingVecPtrArrow '' 1 'size:1' +check "(W) STATED FLOOR: std::optional o_ (a member field); o_->size() — a field's arguments are read for unique_ptr/shared_ptr alone" callees optionalField '' 1 'size:1' +check "(W) STATED FLOOR: a typedef inside a function body records no alias target; p->size() declined" callees localTypedef '' 1 'size:1' + +# ── (X) element receivers (elem/) ──────────────────────────────────────────────────────────────────────────── +echo "=== (X) one element access of a container whose recorded element type is EvalString keeps the hedge ===" +EE='elem/eval.h:14:empty~ elem/eval.h:19:empty~' # EvalString::empty and Piece::empty, the by-name candidates in reach +check "(X) auto rule = std::unique_ptr(…); rule->bindings_[ k ].empty() — a class-scoped typedef of std::map<…, EvalString, …>" \ + callees elem/use.cpp:ruleSubscript "$EE" '' '' +check "(X) Rule& r; r.direct_[ \"a\" ].empty() — a field written std::map" callees elem/use.cpp:refDirect "$EE" '' '' +check "(X) Rule& r; r.vec_[ 0 ].empty() — a field written std::vector" callees elem/use.cpp:refVector "$EE" '' '' +check "(X) a local std::map m; m[ \"a\" ].empty()" callees elem/use.cpp:localMap "$EE" '' '' +check "(X) const std::map<…, EvalString>& m; m.at( \"a\" ).empty()" callees elem/use.cpp:constMapAt "$EE" '' '' +check "(X) std::unordered_map m; m[ \"a\" ].empty()" callees elem/use.cpp:unorderedMap "$EE" '' '' +check "(X) std::vector& v; v.back().empty()" callees elem/use.cpp:vectorBack "$EE" '' '' +check "(X) std::vector& v; v.front().empty() — front itself is the container's member" callees elem/use.cpp:vectorFront "$EE" 1 'front:1' +check "(X) typedef std::vector EvalList; EvalList& v; v[ 0 ].empty()" callees elem/use.cpp:typedefList "$EE" '' '' +check "(X) typedef EvalList EvalList2; EvalList2& v; v.at( 0 ).empty() — alias to alias" callees elem/use.cpp:typedefOfTypedef "$EE" '' '' +check "(X) extern std::map g_table; g_table[ 1 ].empty()" callees elem/use.cpp:globalTable "$EE" '' '' +check "(X) bindings_[ k ].empty() inside Rule — a bare field of the caller's class" callees elem/eval.h:own "$EE" '' '' +check "(X) this->bindings_[ k ].empty() inside Rule" callees elem/eval.h:ownThis "$EE" '' '' +check "(X) near-miss: std::map field; r.strs_[ \"a\" ].empty() stays declined" callees elem/use.cpp:mapOfString '' 1 'empty:1' +check "(X) near-miss: std::vector& v; v[ 0 ].empty() stays declined" callees elem/use.cpp:vectorOfString '' 1 'empty:1' +check "(X) near-miss: std::map>& byName; byName[ k ].empty() — the element is a std vector, declined" callees elem/use.cpp:mapOfVector '' 1 'empty:1' +check "(X) near-miss: std::vector field; r.others_[ 0 ].empty() — Other's cone holds no candidate, declined" callees elem/use.cpp:elementNoMember '' 1 'empty:1' +check "(X) near-miss: r.vec_.empty() — the container's own member, declined" callees elem/use.cpp:containerItself '' 1 'empty:1' +check "(X) near-miss: ns::Grid& g; g[ 0 ].empty() — an in-tree template's own operator[], not a std element, declined" callees elem/use.cpp:inTreeTemplate '' 1 'empty:1' +check "(X) near-miss: a local std::vector bindings_ hides the field inside Rule; bindings_[ 0 ].empty() declined" callees elem/eval.h:localHidesField '' 1 'empty:1' +check "(X) STATED FLOOR: grid[ 1 ][ 0 ].empty() — two element accesses, declined" callees elem/use.cpp:twoLevels '' 1 'empty:1' +check "(X) STATED FLOOR: m.find( \"a\" )->second.empty() — an iterator (a call result), declined" callees elem/use.cpp:iteratorSecond '' 2 'empty,find:2' +check "(X) STATED FLOOR: for( auto& e : v ) e.empty() — a range-for auto, declined" callees elem/use.cpp:rangeForAuto '' 1 'empty:1' + +# ── (D) controls ────────────────────────────────────────────────────────────────────────────────────────────── +echo "=== (D) controls: outside the table, bare calls, C and Rust keep the ladder ===" +check "(D) v.grow() — grow is no standard member name, the ladder is unchanged" \ + callees notInTable 'lib/vec.h:14:grow~' '' '' +check "(D) size( b ) — a bare call binds the free size, never gated" \ + callees bareFree 'lib/vec.h:34:size' '' '' +check "(D) C o->size( o ) — C has no member functions, no gate" callees viaPointer 'c/ops.c:8:size~' '' '' +check "(D) Rust v.push() — stated scope, the by-name split is unchanged" \ + callees std_vec 'rs/lib.rs:3:push~ rs/lib.rs:7:push~' '' '' + +# ── (E) the callers side ────────────────────────────────────────────────────────────────────────────────────── +echo "=== (E) callers: the typed callers stay, the declined ones are counted ===" +check "(E) --callers=Vec::push_back is exactly its typed callers + the alias, Vec-field and element hedges; declined_calls=\"10\"" \ + callers Vec::push_back 'app/use.cpp:10:typedPtr app/use.cpp:16:add app/use.cpp:17:addOwned app/use.cpp:22:aliasTyped~ app/use.cpp:8:typedLocal app/use.cpp:9:typedParam lib/vec.h:15:pushTwice sib/sib.cpp:10:thisField~ sib/sib.cpp:13:fieldOfVar~ sib/sib.cpp:20:subscript~' 10 '' +check "(E) --callers=lib/vec.h:append (one definition) keeps soloTyped; declined_calls=\"1\"" \ + callers lib/vec.h:append 'app/use.cpp:20:soloTyped' 1 '' +check "(E) --callers=Pool::clear keeps poolTyped; declined_calls=\"2\"" callers Pool::clear 'app/use.cpp:21:poolTyped' 2 '' + +# ── (F) disclosure surfaces ─────────────────────────────────────────────────────────────────────────────────── +echo "=== (F) the header, the legends, the dialects ===" +S="$( stats "$TMP/map.xml" )"; hdrd="$( gauge "$S" declined )"; censd="$( disp_in "$TMP/c.tsv" declined )" +if [ -n "$hdrd" ] && [ "$hdrd" -ge 11 ] && [ "$hdrd" = "${censd:-x}" ]; then ok "(F) header declined=$hdrd (>= the 11 gated calls) equals the census's declined bucket" +else no "(F) header declined=\"$hdrd\" census declined=\"$censd\" — want >= 11 and equal"; fi +grep -q 'hdr:declined=also-counts-C++-calls-named-like-a-std-container' "$TMP/map.xml" \ + && ok "(F) the full map legend carries the C++ standard-member declined= clause" || no "(F) the full map legend lacks the C++ clause" +rw --callers=Vec::push_back --legend=full >"$TMP/cl.xml" +grep -q 'It also counts a C++ call named like a member of a standard container' "$TMP/cl.xml" \ + && ok "(F) the callers legend carries the declined_calls= C++ sentence" || no "(F) the callers legend lacks the C++ sentence" +rw --callees=stdParam --legend=full >"$TMP/ce.xml" +grep -q ': of declined_calls=' "$TMP/ce.xml" \ + && ok "(F) the full callees legend defines " || no "(F) the full callees legend does not define " +rw --callees=stdParam >"$TMP/cec.xml" +grep -q 'stdm n= calls=: of declined_calls=' "$TMP/cec.xml" \ + && ok "(F) the compact callees legend defines stdm n= calls=" || no "(F) the compact callees legend does not define stdm" +rw --callees=typedLocal --legend=full >"$TMP/cen.xml" +if [ -z "$( root_tag "$TMP/cen.xml" callees )" ]; then no "(F) --callees=typedLocal produced no root — the absence below would be vacuous" +elif grep -q 'stdm' "$TMP/cen.xml"; then no "(F) a callees answer with no declined standard-member call mentions stdm" +else ok "(F) a callees answer with no such call carries neither the line nor its sentence"; fi +rw --callees=stdParam --format=columnar --legend=full >"$TMP/col.xml" +[ -s "$TMP/col.xml" ] && ! grep -q 'stdm' "$TMP/col.xml" \ + && ok "(F) the columnar callees form carries neither the line nor its sentence (rows only, as with )" \ + || no "(F) the columnar callees form: $( grep -o 'stdm[^>]*' "$TMP/col.xml" | head -2 )" +rw --callees=stdParam --json >"$TMP/ce.json" +python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d.get("stdm")=={"n":"clear,push_back,size","calls":3} and d.get("declined_calls")==3 else 1)' "$TMP/ce.json" \ + && ok "(F) --json callees carries \"stdm\":{\"n\":\"clear,push_back,size\",\"calls\":3} (the XML attribute values)" || no "(F) --json callees: $( head -c 300 "$TMP/ce.json" )" +M="$( mcp_text "$( call find_symbol '{"path":"'"$CORPUS"'","symbol":"stdParam"}' )" )" +printf '%s' "$M" | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("stdm")=={"n":"clear,push_back,size","calls":3} else 1)' \ + && ok "(F) MCP find_symbol (the callees direction) carries the same \"stdm\"" || no "(F) MCP find_symbol: $( printf '%s' "$M" | head -c 300 )" +M="$( mcp_text "$( call find_referencing_symbols '{"path":"'"$CORPUS"'","symbol":"stdParam"}' )" )" +if ! printf '%s' "$M" | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if "symbol" in d else 1)' 2>/dev/null; then + no "(F) MCP find_referencing_symbols gave no answer — the absence below would be vacuous: $( printf '%s' "$M" | head -c 200 )" +elif printf '%s' "$M" | grep -q '"stdm"'; then no "(F) MCP find_referencing_symbols (callers) carries stdm" +else ok "(F) MCP find_referencing_symbols (the callers direction) carries no stdm"; fi + +# ── (G) dead-code safety ────────────────────────────────────────────────────────────────────────────────────── +echo "=== (G) a definition whose only callers were declined is no dead-code candidate ===" +# a fixture path is never dead by design (quality.h isFixturePath), so the dead-set arm runs on a copy in $TMP +QD="$TMP/qd"; rm -rf "$QD"; mkdir -p "$QD"; cp -R app lib far "$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 +printf '\n' >>"$QD/app/local.cpp" +( cd "$QD" && "$BIN" . --no-cache --quality-delta >"$TMP/qd.xml" 2>/dev/null ) +QR="$( root_tag "$TMP/qd.xml" quality-delta )" +if [ -n "$( attr "$QR" declined-call-excluded )" ] && [ "$( attr "$QR" declined-call-excluded )" -ge 1 ] 2>/dev/null; then + ok "(G) --quality-delta keeps the gated calls' targets out of the dead set: declined-call-excluded=\"$( attr "$QR" declined-call-excluded )\"" +else + no "(G) --quality-delta carries no declined-call-excluded= count: ${QR:-no root}" +fi +rw --safe-delete=Shelf::empty >"$TMP/sd.xml" +R="$( root_tag "$TMP/sd.xml" safe-delete )" +[ "$( attr "$R" declined_calls )" = "3" ] && [ "$( attr "$R" dead_code_candidate )" = "0" ] \ + && ok "(G) --safe-delete=Shelf::empty: declined_calls=\"3\" (two gated, one tier-3), dead_code_candidate=\"0\"" || no "(G) --safe-delete root: $R" + +# ── (S) zero bytes elsewhere ────────────────────────────────────────────────────────────────────────────────── +echo "=== (S) a tree the gate never declined in carries no C++ clause ===" +BB="$ROOT/test/builtinbindfix" +"$BIN" "$BB" --no-cache --legend=full >"$TMP/bbmap.xml" 2>/dev/null +"$BIN" "$BB" --no-cache --legend=full --callers=py/pool.py:get >"$TMP/bbcl.xml" 2>/dev/null +[ -s "$TMP/bbmap.xml" ] && [ -s "$TMP/bbcl.xml" ] && ! grep -q -e 'also-counts-C++' -e 'It also counts a C++ call named like' -e 'stdm' "$TMP/bbmap.xml" "$TMP/bbcl.xml" \ + && ok "(S) builtinbindfix's map and callers legends carry no C++ standard-member clause" || no "(S) a C++ clause rides a tree with no C++ decline" + +# ── (K) the predicates can fail ─────────────────────────────────────────────────────────────────────────────── +echo "=== (K) the predicates can fail ===" +printf '' >"$TMP/k.xml" +[ "$( rows "$TMP/k.xml" )" = 'lib/vec.h:8:push_back~' ] && [ -z "$( stdm "$TMP/k.xml" )" ] \ + && ok "(K) a via=\"name\" row reads as one and a missing line reads as empty" || no "(K) the row/stdm readers are blind" + +# ── (L) determinism, well-formedness, stderr ────────────────────────────────────────────────────────────────── +echo "=== (L) determinism x2, xmllint, stderr ===" +rw --legend=full >"$TMP/map2.xml" +"$BIN" . --no-cache --legend=full >"$TMP/map3.xml" 2>/dev/null +rw --callees=stdParam --legend=full >"$TMP/ce2.xml" +if cmp -s "$TMP/map2.xml" "$TMP/map3.xml" && cmp -s "$TMP/ce.xml" "$TMP/ce2.xml"; then ok "(L) map and callees byte-identical across two runs" +else no "(L) nondeterministic output"; fi +if command -v xmllint >/dev/null 2>&1; then + for f in map.xml cl.xml ce.xml cec.xml col.xml; do + if xmllint --noout "$TMP/$f" 2>/dev/null; then ok "(L) xmllint $f"; else no "(L) xmllint $f is not well-formed"; fi + done +else + echo " SKIP (L) xmllint not installed" +fi +grep -q -i -e 'DISCLOSE' -e 'buildGraph:' "$TMP/err" && no "(L) a degrade alert on stderr: $( head -c 200 "$TMP/err" )" || ok "(L) no degrade alert on stderr" + +if [ "$fail" -ne 0 ]; then echo "SOME FAILED"; exit 1; fi +echo "ALL PASS" diff --git a/test/stdmembergatefix/README.md b/test/stdmembergatefix/README.md new file mode 100644 index 000000000..32fc65629 --- /dev/null +++ b/test/stdmembergatefix/README.md @@ -0,0 +1,40 @@ +# stdmembergatefix — the C++ standard-member gate (test/stdmembergatecheck.sh) + +`lib/vec.h` defines in-tree classes whose members are named like standard container and string members: +`Vec` (two `push_back` overloads, `size`, `empty`, `clear`, `data`, `grow`), `Pool` (`size`, `empty`, `clear`), +`Solo` (the tree's ONLY `append`) and a free `size( const Bag& )`. `lib/alias.h` adds `SmallV`, an alias of `Vec`, +and `StdMap`, an alias of a standard map. + +`app/use.cpp` calls those names two ways: + +- through receivers a rule types (a typed local, parameter, pointer, member field, smart-pointer `->`, `this->`, an + implicit-this call inside `Vec`, a local declared with the in-tree alias): the edge must stay exactly as before; +- through receivers nothing types or that are written in `std` (`std::vector` locals and parameters, `auto`, a call + result, a `std::string`, a field written in `std`, a local declared with the standard-map alias): the call must be + declined and disclosed (declined_calls=, the callees answer's `` line), never bound to an in-tree namesake. + +Controls: a name outside the table (`grow`) keeps the ladder; a bare call `size( b )` reaches the free function; `c/` +holds a C function-pointer call (C has no member functions, so no gate); `rs/` is the Rust stated scope (no declared +receiver types there yet), whose by-name split must stay unchanged. `far/` calls `w.empty()` where no namesake is in +reach, so the ladder itself declines it (tier 3), and the `` line still names it. + +`sib/` holds the field and expression receivers: a field written as the in-tree `Vec` (`this->items_`, `b.items_` on a +`Box& b`) keeps the hedge, a field written in `std` is declined, a subscript of a `std::vector>` reads its element +type and keeps the hedge, and a dereference, a range-for `auto` and a template parameter are the stated floor (declined even +where the element is `Vec`). + +`elem/` holds the element receivers: one element access (`c[ k ]`, `c.at( k )`, `c.front()`, `c.back()`) of a container whose +recorded written type is a std sequence container or map of `EvalString` — through a class-scoped typedef'd map field on a +`std::unique_ptr` local, a std map/vector field, a local, a const parameter, an unordered_map, a typedef and a typedef of it, +an extern map, a bare and a `this->` field — keeps the ladder's hedge on `EvalString::empty` and `Piece::empty`; an element +written in std, an element class with no `empty`, and the container's own member stay declined; two element accesses, an +iterator's `->second` and a range-for `auto` are the stated floor. + +`wrap/` holds the written types Rule 2's tables do not carry: `ns::List` (the tree's only `size`/`find`/`front`) reached +through `std::unique_ptr`/`std::shared_ptr`/`std::optional`, an in-tree `SP` with its own `operator->`, Qt and boost +pointers, a smart-pointer root one field hop off, namespace-scope, file-static and extern variables, and `typedef` / +`using` aliases of a std smart pointer or of an undefined wrapper (`list.h` tail) — each keeps the ladder's hedge — beside +the near-misses that stay declined (a smart pointer of `std::string`, raw pointers to std containers or to an alias of one, +`.` on `std::vector`, an in-tree class with no `operator->`, a std-written global, a global hidden by a local or a +field) and the stated floor (call results, an iterator, an unqualified template's arguments, a `std::optional` member +field, a typedef inside a function body). diff --git a/test/stdmembergatefix/app/local.cpp b/test/stdmembergatefix/app/local.cpp new file mode 100644 index 000000000..b27a9b082 --- /dev/null +++ b/test/stdmembergatefix/app/local.cpp @@ -0,0 +1,11 @@ +#include + +namespace +{ +struct Shelf +{ + bool empty() const { return true; } +}; +} // namespace + +bool anyEmpty( const std::vector& w ) { return w.empty(); } diff --git a/test/stdmembergatefix/app/use.cpp b/test/stdmembergatefix/app/use.cpp new file mode 100644 index 000000000..a2505b1a8 --- /dev/null +++ b/test/stdmembergatefix/app/use.cpp @@ -0,0 +1,36 @@ +#include "../lib/alias.h" +#include "../lib/vec.h" +#include +#include +#include + +// ---- receivers a rule types: every edge stays ---- +void typedLocal() { Vec v; v.push_back( 1 ); } +void typedParam( Vec& v ) { v.push_back( 2 ); } +void typedPtr( Vec* p ) { p->push_back( 3 ); } +struct Holder +{ + Vec items_; + std::unique_ptr> owned_; + std::vector raw_; + void add() { items_.push_back( 4 ); } + void addOwned() { owned_->push_back( 5 ); } + void addRaw() { raw_.push_back( 6 ); } +}; +void soloTyped() { Solo so; so.append( 7 ); } +void poolTyped( Pool& p ) { p.clear(); } +void aliasTyped() { SmallV a; a.push_back( 8 ); } + +// ---- receivers nothing types, or written in std: declined, disclosed ---- +void stdLocal() { std::vector v; v.push_back( 9 ); } +void stdParam( std::vector& out, const std::string& s ) { out.push_back( 'x' ); out.reserve( s.size() ); out.clear(); } +void autoLocal() { auto v = std::vector(); v.push_back( 10 ); } +std::vector& getV(); +void chained() { getV().push_back( 11 ); } +void stdAppend( std::string& s ) { s.append( "x" ); } +void stdEmpty( const std::vector& w ) { if( w.empty() ) { return; } } +void stdAliasClear( StdMap& m ) { m.clear(); } + +// ---- controls ---- +void notInTable( std::vector& v ) { v.grow(); } +std::size_t bareFree( const Bag& b ) { return size( b ); } diff --git a/test/stdmembergatefix/c/ops.c b/test/stdmembergatefix/c/ops.c new file mode 100644 index 000000000..0b8051179 --- /dev/null +++ b/test/stdmembergatefix/c/ops.c @@ -0,0 +1,10 @@ +#include + +struct ops +{ + size_t ( *size )( struct ops* ); +}; + +size_t size( struct ops* o ) { return 0; } + +size_t viaPointer( struct ops* o ) { return o->size( o ); } diff --git a/test/stdmembergatefix/elem/eval.h b/test/stdmembergatefix/elem/eval.h new file mode 100644 index 000000000..01e7d20fe --- /dev/null +++ b/test/stdmembergatefix/elem/eval.h @@ -0,0 +1,52 @@ +#pragma once +// ELEMENT receivers (test/stdmembergatecheck.sh arm X): EvalString and Piece are the tree's only `empty` definitions in reach +// of elem/, so a call on an element of a container of EvalString keeps the ladder's two via="name" rows; a container whose +// element is written in std, or names a class with no `empty`, stays declined. Rule mirrors the shape that motivated it: a +// class-scoped typedef of a std::map field reached through a std::unique_ptr local. +#include +#include +#include +#include +#include + +struct EvalString +{ + bool empty() const { return text_.empty(); } + std::string text_; +}; +struct Piece +{ + bool empty() const { return len_ == 0; } + int len_ = 0; +}; +struct Other +{ + int n_ = 0; +}; +typedef std::vector EvalList; +typedef EvalList EvalList2; + +struct Rule +{ + typedef std::map> Bindings; + Bindings bindings_; + std::map direct_; + std::vector vec_; + std::map strs_; + std::vector others_; + bool own( const std::string& k ) { return bindings_[ k ].empty(); } + bool ownThis( const std::string& k ) { return this->bindings_[ k ].empty(); } + bool localHidesField() { std::vector bindings_{ "x" }; return bindings_[ 0 ].empty(); } +}; + +// an in-tree template whose own operator[] yields Other, whatever its argument: not a std container's element access +namespace ns +{ +template struct Grid +{ + Other& operator[]( int i ) { return cells_[ i ]; } + Other cells_[ 4 ]; +}; +} // namespace ns + +extern std::map g_table; diff --git a/test/stdmembergatefix/elem/use.cpp b/test/stdmembergatefix/elem/use.cpp new file mode 100644 index 000000000..8903a6174 --- /dev/null +++ b/test/stdmembergatefix/elem/use.cpp @@ -0,0 +1,27 @@ +#include "eval.h" + +// keep the hedge: the container's recorded element type is EvalString +bool ruleSubscript( const std::string& name ) { auto rule = std::unique_ptr( new Rule() ); return rule->bindings_[ name ].empty(); } +bool refDirect( Rule& r ) { return r.direct_[ "a" ].empty(); } +bool refVector( Rule& r ) { return r.vec_[ 0 ].empty(); } +bool localMap() { std::map m; return m[ "a" ].empty(); } +bool constMapAt( const std::map& m ) { return m.at( "a" ).empty(); } +bool unorderedMap() { std::unordered_map m; return m[ "a" ].empty(); } +bool vectorBack( std::vector& v ) { return v.back().empty(); } +bool vectorFront( std::vector& v ) { return v.front().empty(); } +bool typedefList( EvalList& v ) { return v[ 0 ].empty(); } +bool typedefOfTypedef( EvalList2& v ) { return v.at( 0 ).empty(); } +bool globalTable() { return g_table[ 1 ].empty(); } + +// near-misses: declined +bool mapOfString( Rule& r ) { return r.strs_[ "a" ].empty(); } +bool vectorOfString( std::vector& v ) { return v[ 0 ].empty(); } +bool mapOfVector( std::map>& byName, const std::string& k ) { return byName[ k ].empty(); } +bool elementNoMember( Rule& r ) { return r.others_[ 0 ].empty(); } +bool containerItself( Rule& r ) { return r.vec_.empty(); } +bool inTreeTemplate( ns::Grid& g ) { return g[ 0 ].empty(); } + +// stated floors: declined +bool twoLevels( std::map>& grid ) { return grid[ 1 ][ 0 ].empty(); } +bool iteratorSecond( std::map& m ) { return m.find( "a" )->second.empty(); } +bool rangeForAuto( std::vector& v ) { for( auto& e : v ) { return e.empty(); } return false; } diff --git a/test/stdmembergatefix/far/far.cpp b/test/stdmembergatefix/far/far.cpp new file mode 100644 index 000000000..eed361f88 --- /dev/null +++ b/test/stdmembergatefix/far/far.cpp @@ -0,0 +1,5 @@ +#include + +// No include reaches lib/ and no namesake lives in this directory: the ladder itself declines (tier 3), and the +// line still names the call. +bool farEmpty( const std::vector& w ) { return w.empty(); } diff --git a/test/stdmembergatefix/lib/alias.h b/test/stdmembergatefix/lib/alias.h new file mode 100644 index 000000000..8ae9bbff5 --- /dev/null +++ b/test/stdmembergatefix/lib/alias.h @@ -0,0 +1,6 @@ +#pragma once +#include "vec.h" +#include + +template using SmallV = Vec; +using StdMap = std::map; diff --git a/test/stdmembergatefix/lib/vec.h b/test/stdmembergatefix/lib/vec.h new file mode 100644 index 000000000..835a722d0 --- /dev/null +++ b/test/stdmembergatefix/lib/vec.h @@ -0,0 +1,34 @@ +#pragma once +#include + +template struct Vec +{ + T* d = nullptr; + std::size_t n = 0; + void push_back( const T& v ) { grow(); d[ n++ ] = v; } + void push_back( T&& v ) { grow(); d[ n++ ] = v; } + std::size_t size() const { return n; } + bool empty() const { return n == 0; } + void clear() { n = 0; } + T* data() { return d; } + void grow() {} + void pushTwice( const T& v ) { push_back( v ); this->push_back( v ); } +}; + +struct Pool +{ + std::size_t size() const { return 3; } + bool empty() const { return false; } + void clear() {} +}; + +struct Solo +{ + void append( int ) {} +}; + +struct Bag +{ + int k = 0; +}; +inline std::size_t size( const Bag& b ) { return std::size_t( b.k ); } diff --git a/test/stdmembergatefix/rs/lib.rs b/test/stdmembergatefix/rs/lib.rs new file mode 100644 index 000000000..c99af226b --- /dev/null +++ b/test/stdmembergatefix/rs/lib.rs @@ -0,0 +1,10 @@ +pub struct Stack { n: usize } +impl Stack { + pub fn push(&mut self, x: i32) { } +} +pub struct Queue { n: usize } +impl Queue { + pub fn push(&mut self, x: i32) { } +} +pub fn std_vec() { let mut v = Vec::new(); v.push(1); } +pub fn typed_local() { let mut s = Stack { n: 0 }; s.push(2); } diff --git a/test/stdmembergatefix/sib/sib.cpp b/test/stdmembergatefix/sib/sib.cpp new file mode 100644 index 000000000..febb39406 --- /dev/null +++ b/test/stdmembergatefix/sib/sib.cpp @@ -0,0 +1,22 @@ +#include "../lib/vec.h" +#include + +// Field receivers: a field whose written type is the in-tree Vec keeps the ladder's hedge (the type may be that +// container); a field written in std is declined. +struct Box +{ + Vec items_; + std::vector raw_; + void thisField() { this->items_.push_back( 1 ); } + void thisRaw() { this->raw_.push_back( 2 ); } +}; +void fieldOfVar( Box& b ) { b.items_.push_back( 3 ); } +void fieldOfVarRaw( Box& b ) { b.raw_.push_back( 4 ); } + +// STATED FLOORS: a receiver the resolver reads no written type for — a dereference, a range-for `auto`, a template +// parameter — is declined even where the element is the in-tree Vec. A subscript reads the container's element type +// (Vec) and keeps the hedge (stdmembergatecheck arm X). +void derefPtr( Vec* p ) { ( *p ).push_back( 5 ); } +void subscript( std::vector>& vs ) { vs[ 0 ].push_back( 6 ); } +void rangeFor( std::vector>& vs ) { for( auto& v : vs ) { v.push_back( 7 ); } } +template void tmplParam( C& c ) { c.push_back( 8 ); } diff --git a/test/stdmembergatefix/wrap/list.h b/test/stdmembergatefix/wrap/list.h new file mode 100644 index 000000000..870ed413c --- /dev/null +++ b/test/stdmembergatefix/wrap/list.h @@ -0,0 +1,42 @@ +#pragma once +// ns::List is the tree's only class defining size / find / front, so before the gate every call below bound to it by name +// (via="name"). SP is an in-tree wrapper with its own operator->; Plain is an in-tree class with neither. +namespace ns +{ +class List +{ +public: + int size() const { return 0; } + int find( int k ) const { return k; } + int front() const { return 1; } +}; +} // namespace ns + +template class SP +{ +public: + T* operator->() const { return p_; } + T* p_ = nullptr; +}; + +struct Plain : ext::Base +{ + int other() const { return 2; } +}; + +struct Box2 +{ + ns::List items_; +}; + +extern ns::List g_ext; + +// type aliases (test/stdmembergatecheck.sh arm W): a typedef is also a Struct symbol at its name, a `using` is not; the gate +// reads both through their recorded alias target +typedef std::shared_ptr ListSP; +typedef std::unique_ptr ListUP; +using ListUsingSP = std::shared_ptr; +typedef std::vector IntVec; +using IntVecUsing = std::vector; +typedef std::unique_ptr StrUP; +using QtListSP = QSharedPointer; diff --git a/test/stdmembergatefix/wrap/use.cpp b/test/stdmembergatefix/wrap/use.cpp new file mode 100644 index 000000000..6e729cb39 --- /dev/null +++ b/test/stdmembergatefix/wrap/use.cpp @@ -0,0 +1,83 @@ +#include "list.h" +#include +#include +#include +#include +#include +#include + +// ---- kept: the receiver's recorded written type names ns::List, so the ladder's via="name" hedge stays ---- +int uniqueLocal() { std::unique_ptr up; return up->size(); } +int sharedParam( const std::shared_ptr& p ) { return p->size(); } +int optionalLocal() { std::optional o; return o->find( 1 ); } +int inTreeWrapper( SP& p ) { return p->size(); } +int inTreeWrapperLocal() { SP p; return p->front(); } +int qtWrapper( QSharedPointer& p ) { return p->front(); } +int boostWrapper( boost::shared_ptr& p ) { return p->find( 2 ); } +int smartRootField() { std::unique_ptr b; return b->items_.size(); } +ns::List g_list; +int globalVar() { return g_list.size(); } +namespace app +{ +ns::List nsVar; +int nsGlobalVar() { return nsVar.front(); } +} // namespace app +static ns::List fileStatic; +int fileStaticVar() { return fileStatic.find( 3 ); } +int externVar() { return g_ext.size(); } + +// ---- still declined: nothing written names a class that defines the callee ---- +int uniqueStd() { std::unique_ptr p; return int( p->size() ); } +int rawStdPtr( std::vector* pv ) { return int( pv->size() ); } +int uniqueStdVec() { std::unique_ptr> up; return int( up->size() ); } +int vectorOfList( std::vector& v ) { return int( v.size() ); } +int rawVectorOfListPtr( std::vector* pv ) { return int( pv->size() ); } +int plainPtr( Plain* p ) { return p->size(); } +std::string g_str; +int globalStd() { return int( g_str.size() ); } +int shadowedGlobal() { std::string g_list; return int( g_list.size() ); } +struct Shelf2 +{ + std::string g_list; + int fieldHidesGlobal() { return int( g_list.size() ); } +}; +struct Shelf3 +{ + std::vector g_list; + int stdFieldHidesGlobal() { return int( g_list.size() ); } +}; + +// ---- STATED FLOOR: the index records no written type for these receivers ---- +struct Holder2 +{ + ns::List& list(); +}; +int callResult( Holder2& h ) { return h.list().size(); } +int frontOfVector( std::vector& v ) { return v.front().size(); } +int iteratorArrow( std::vector& v ) { auto it = v.begin(); return it->size(); } +int unqualifiedTemplate( QList& l ) { return l.size(); } + +// ---- aliases of a std smart pointer: the alias's recorded target decides, one rule for typedef and using ---- +int typedefSharedLocal() { ListSP p; return p->size(); } +int typedefSharedParam( const ListSP& p ) { return p->size(); } +int typedefUniqueLocal() { ListUP p; return p->front(); } +int typedefUniqueParam( ListUP& p ) { return p->find( 1 ); } +int usingSharedLocal() { ListUsingSP p; return p->size(); } +int typedefVecDot() { IntVec v; return int( v.size() ); } +int typedefVecPtrArrow( IntVec* p ) { return int( p->size() ); } +int usingVecPtrArrow( IntVecUsing* p ) { return int( p->size() ); } +int usingQtAliasLocal() { QtListSP p; return p->front(); } +struct Owner4 +{ + typedef std::shared_ptr Ptr; + Ptr p_; + int typedefField() { return p_->size(); } +}; +int classScopedTypedef( Owner4::Ptr p ) { return p->size(); } +int localTypedef() { typedef std::shared_ptr LocalSP; LocalSP p; return p->size(); } +int typedefStrUP() { StrUP p; return int( p->size() ); } +struct Holder3 +{ + std::optional o_; + int optionalField() { return o_->size(); } +}; diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 197f49e31..a2d0cc21b 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -11,6 +11,7 @@ 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; } +echo "taskroutecheck: BIN=$BIN" [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } REPO="$TMP/router repo" mkdir -p "$REPO" @@ -643,6 +644,26 @@ for f in --verify --connect --expand --grep --grep-context --edit-check --from-t --edit-plan --dry-run --handles --legend --doctor --agent=codex --test-gate --slice --slice-flow --at --uses --seams \ --rank-by= --limit=; do "$BIN" --help=all 2>&1 | grep -q -- "$f" || no "recommended flag absent from --help: $f"; done +# ── K50: a NON-ASCII prompt is not a sanitizer error (lowerAscii, the name-shape and FILE:LINE predicates) ────────────── +# `for( const unsigned char c : text )` over a string_view of `char` converts every byte >= 0x80 implicitly; G1's +# implicit-integer-sign-change makes that a hard abort on the sanitizer build, so every --help-task prompt with an +# accented letter, CJK or an emoji died there (K50). The arms below are RED only on a sanitizer binary (run the gate +# with BIN=asan/ripwire); on a plain build they prove the answer is still a router document and is unchanged by the +# non-ASCII bytes, which is also what a regression to a locale-sensitive tolower would break. +for kp in 'where is the résumé parser called from' 'what is at router.cpp:é9' 'how does `Ünï.cöde` work' 'explain computeBudget and 路由 plus 🙂'; do + routeRaw "$kp" >"$TMP/k50.out"; k50rc=$? + if [ "$k50rc" = 0 ] && grep -q ', no sanitizer report): $kp" + else + no "K50: a non-ASCII prompt died or reported (rc=$k50rc): $kp :: $( head -c 200 "$TMP/err" )" + fi +done +# The negative control: the non-ASCII bytes are noise to the router — the same prompt without them routes identically. +K50A="$( route 'how do alphaNode, betaNode and gammaNode all come together' )" +K50B="$( route 'how do alphaNode, betaNode and gammaNode all come together, café' )" +[ "${K50A%%resolved_symbols*}" = "${K50B%%resolved_symbols*}" ] && case "$K50B" in *'intent="connect-symbols"'*) ok "K50: a trailing non-ASCII word leaves the --connect route unchanged";; *) no "K50: the control did not route --connect: $K50B";; esac \ + || no "K50: a trailing non-ASCII word changed the route: [$K50A] vs [$K50B]" + # ── byte-compat: the verify-claim template must emit the SHIPPED --verify grammar byte-exactly ───────── # (PLAN 2026-08-13 addendum: gate against the real verb's PARSER, never a copy of its syntax.) # Route a claim through --help-task, unquote the recommended argv robustly (a real XML parse + a real diff --git a/test/tokenbudgetcheck.sh b/test/tokenbudgetcheck.sh index ef91a893c..8a97a432b 100755 --- a/test/tokenbudgetcheck.sh +++ b/test/tokenbudgetcheck.sh @@ -236,6 +236,34 @@ GATE_EST="$( grep -oE 'est_tokens=[0-9]+' "$TMP_TB_B_ERR" | grep -oE '[0-9]+' )" && ok "gate value ($GATE_EST) == map header's est_tokens ($HDR_EST) — one counter, not two" \ || no "gate/header est_tokens MISMATCH (header=$HDR_EST gate=$GATE_EST) — a second counter crept in" +# ── #9b (map-inventory-d7, 2026-10-08): the SAME equality on the boundary where the compact reprice drops a digit. The gate +# used to recount the compacted body's size AFTER the layer had rewritten the est digits (a 5-digit full price repriced to 4 +# digits is 2 bytes shorter in the root and the header), so there it said one token less than the header (7608d1f6: +# src --top-k=233, header 9965, gate 9964). The arm LOCATES that boundary on today's src/ (it moves with the tree): the +# smallest top-k whose FULL price reaches 10000, then every k up to 8 past it whose compact header is still 4 digits. +full_est(){ "$BIN" src --top-k="$1" --legend=full --token-budget=1 --no-cache 2>&1 >/dev/null | grep -oE 'est_tokens=[0-9]+' | grep -oE '[0-9]+'; } +lo=1; hi=4000 +if [ "$( full_est $hi )" -ge 10000 ] 2>/dev/null; then + while [ $(( hi - lo )) -gt 1 ]; do mid=$(( ( lo + hi ) / 2 )); if [ "$( full_est $mid )" -ge 10000 ] 2>/dev/null; then hi=$mid; else lo=$mid; fi; done + crossed=0; bad="" + for k in $( seq "$hi" $(( hi + 8 )) ); do + H="$( "$BIN" src --top-k="$k" --token-budget=999999 --no-cache 2>/dev/null | grep -oE 'est_tokens=[0-9]+' | head -1 | grep -oE '[0-9]+' )" + [ -n "$H" ] && [ "$H" -lt 10000 ] 2>/dev/null || continue + crossed=$(( crossed + 1 )) + G="$( "$BIN" src --top-k="$k" --token-budget=1 --no-cache 2>&1 >/dev/null | grep -oE 'est_tokens=[0-9]+' | grep -oE '[0-9]+' )" + [ "$G" = "$H" ] || bad="$bad k=$k(header=$H,gate=${G:-?})" + done + if [ "$crossed" -eq 0 ]; then + no "#9b premise: no top-k in $hi..$(( hi + 8 )) has a 5-digit full price and a 4-digit compact one — the arm tested nothing" + elif [ -z "$bad" ]; then + ok "#9b gate == header on all $crossed digit-crossing top-k (from $hi): the gate reads the printed price" + else + no "#9b gate/header MISMATCH where the compact reprice drops a digit:$bad" + fi +else + no "#9b premise: src --top-k=4000 prices under 10000 tokens — no digit boundary to probe" +fi + # ── #10: determinism — the same command re-run twice gives the same exit code + the same est_tokens ─── "$BIN" src --token-budget=1 --no-cache >/dev/null 2>"$TMP_TB_B2_ERR"; rc_tiny2=$? GATE_EST2="$( grep -oE 'est_tokens=[0-9]+' "$TMP_TB_B2_ERR" | grep -oE '[0-9]+' )" diff --git a/test/truncvocabcheck.sh b/test/truncvocabcheck.sh index 503aaf4c8..b8e013af8 100755 --- a/test/truncvocabcheck.sh +++ b/test/truncvocabcheck.sh @@ -502,5 +502,124 @@ else ok "G4: xmllint unavailable (skipped)" fi +# ── (H) ARM A of the paging spec (upstream issue #294, scope corrected by measurement) ────────── +echo +echo "── (H) ARM A: the BUDGETED bundle continuation carries the pageview quintet ──" +# SCOPE, corrected by what the tree already does (measured 2026-09-19, on this binary): +# * `--for=TASK --limit=N` (+offset=M) is the FILE-GRAIN WIDENING PAGE — it exists, it carries the +# full pageview quintet, and forwidencheck owns it. Re-gating it here would violate §6.4 (the gate +# that already owns the subject takes the arm; a duplicate arm is coverage-shaped noise). +# * The MCP `for` verb serves that widening page today (limit=5 → ); the MCP `explore` verb refuses limit ("unknown field"). +# * `--for --token-budget=N --offset=M` REFUSES (stdout empty). The byte-budgeted compact bundle — +# the thing whose cut IS disclosed (over_ceiling= reason=) but with NO continuation handle — is the +# actual open gap of issue #294. THIS arm guards exactly that: the budgeted bundle, paged by +# candidate offset, must carry the pageview quintet, and offset=0 must be byte-identical to the +# un-paged budgeted answer (the regression floor: existing callers must see identical output). +# RED on the pre-feature binary is the intended state — gate written BEFORE the code, red vs the +# pre-change binary, same discipline as runtracecheck.sh. It greens when the continuation ships. +paged(){ local name="$1"; shift; run "$@" > "$TMP/page_$name.xml"; } +paged for_budget_p0w "$BIG" --for="rank symbols" --token-budget=800 --limit=6 --offset=0 +paged for_budget_page "$BIG" --for="rank symbols" --token-budget=800 --offset=5 +paged pack_budget_page "$BIG" --pack-task="rank symbols by pagerank" --token-budget=800 --offset=5 +run "$BIG" --for="rank symbols" --token-budget=800 --offset=0 > "$TMP/page_for_budget_alone.xml" # offset=0 ALONE = the un-paged bundle +run "$BIG" --for="rank symbols" --token-budget=800 > "$TMP/page_for_budget_nopage.xml" + +python3 - "$TMP" <<'PY' +import os, sys, xml.etree.ElementTree as ET + +tmp = sys.argv[1] +QUINTET = ("shown", "total", "capped", "has_more", "next_offset") +problems = [] + +def load(name): + path = os.path.join(tmp, f"page_{name}.xml") + if not os.path.exists(path) or os.path.getsize(path) == 0: + problems.append(f"{name}: the paged run produced no output — the page arm asserted nothing " + "(today: --token-budget refuses --offset; that refusal IS the red this arm exists to record)") + return None + raw = open(path, encoding="utf-8", errors="replace").read() + try: + return ET.fromstring(raw), raw + except ET.ParseError as e: + problems.append(f'{name}: paged output is not parseable XML ({e}) — a paged task-lens answer must be a document') + return None + +paged_roots = {} +for name in ("for_budget_p0w", "for_budget_page", "pack_budget_page"): + loaded = load(name) + if loaded is None: continue + paged_roots[name] = loaded[0] + root, _ = loaded + a = root.attrib + missing = [k for k in QUINTET if k not in a] + if missing: + problems.append(f'{name}: paged budgeted root lacks the pageview quintet (missing: {", ".join(missing)}) — ' + "the continuation contract pageview.h owns for every row verb is absent on the budgeted bundle") + continue + if a["capped"] not in ("0", "1"): + problems.append(f'{name}: capped="{a["capped"]}" is not the 0|1 bit (rule 3)') + if int(a["shown"]) > int(a["total"]): + problems.append(f'{name}: shown={a["shown"]} > total={a["total"]} — a page cannot show more than the candidate set') + # issue #294 ruling, suggestions 1+3: above_cliff (the head-tier count, next to total=) and the + # at= index stamp (which index answered; a consumer compares across pages instead of mixing two + # rankings silently). above_cliff must be < total unless the whole set is head-tier. + if "above_cliff" not in a: + problems.append(f'{name}: paged root lacks above_cliff= — the ruling\'s suggestion 1 (decide from page 1 whether the remainder is worth fetching)') + elif int(a["above_cliff"]) >= int(a["total"]) and int(a["total"]) > 0: + problems.append(f'{name}: above_cliff={a["above_cliff"]} >= total={a["total"]} — the boundary is not inside the set') + if "at" not in a: + problems.append(f'{name}: paged root lacks at= — the index stamp the ruling\'s suggestion 3 names (a stale continuation must be detectable, not silent)') + # ARM C of the paging spec, housed here because THIS gate owns the rule-5 vocabulary (the + # §B8.3 sweep): within a candidate page, a body byte-trim must still disclose shown=/total=/capped= + # on — the candidate cursor advances the SET, rule 5 discloses the within-page byte trim. + for el in root.iter(): + ea = el.attrib + if ea.get("capped") == "1" and el.tag.lower().endswith("bodies"): + if "shown" not in ea or "total" not in ea: + problems.append(f'{name}: <{el.tag} capped="1"> within the page carries no ' + "shown=/total= — the within-page byte trim is undisclosed (rule 5)") + +# ARM F of the paging spec (CONTINGENT on the #294 ruling; it encodes the PROPOSED answer): the cliff +# boundary is computed ONCE over the full ranked candidate set — the boundary value reported must be +# IDENTICAL on every page of the same query. If maintainers pick the per-page re-cliff alternative, this +# assert is rewritten to that contract before any code lands (the spec does not assume its own question). +cliff_attrs = {} +for name, root in paged_roots.items(): + for el in root.iter(): + for key, val in el.attrib.items(): + if key in ("cliff_rank", "cliffrank", "cliff"): + cliff_attrs.setdefault(key, set()).add(val) +if cliff_attrs: + for key, vals in cliff_attrs.items(): + if len(vals) > 1: + problems.append(f"cliff {key} moved across pages of the same query ({sorted(vals)}) — the boundary " + "must be computed once over the full candidate set, not re-decided per page") + +alone = os.path.join(tmp, "page_for_budget_alone.xml") +nopage = os.path.join(tmp, "page_for_budget_nopage.xml") +if os.path.exists(alone) and os.path.getsize(alone) > 0 and os.path.exists(nopage) and os.path.getsize(nopage) > 0: + if open(alone, encoding="utf-8", errors="replace").read() != open(nopage, encoding="utf-8", errors="replace").read(): + problems.append("for_budget_alone: --offset=0 ALONE must be byte-identical to the un-paged budgeted " + "answer (the un-paged bundle is the byte-pinned ladder, forbudgetmonotoncheck's ground — " + "the page contract ADDS a windowed mode, it must not touch the un-paged bytes)") + +# mutation: the assertion shape can fail — a bare paged root with no continuation handle must be SEEN +mut = ET.fromstring('').attrib +if sorted(k for k in QUINTET if k not in mut) != ["has_more", "next_offset"]: + problems.append("mutation: a bare paged root with no has_more=/next_offset= was NOT detected — the arm cannot fail") +else: + print(' PASS (H) mutation: a bare paged root (no has_more=/next_offset=) IS detected') + +for p in problems: print(" FAIL " + p) +print(" .. (H) budgeted-bundle paged captures checked: 3, plus the offset=0 byte-identity floor") +sys.exit(1 if problems else 0) +PY +if [ $? = 0 ]; then + ok '(H) paged budgeted-bundle roots carry the full pageview quintet; offset=0 is byte-identical to the un-paged budgeted answer' +else + no '(H) the budgeted-bundle continuation contract (upstream issue #294, corrected scope) does not hold — see FAIL lines above' +fi + [ $fail = 0 ] && echo "ALL PASS" || echo "FAILURES" exit $fail diff --git a/test/usesselectorcheck.sh b/test/usesselectorcheck.sh index 2117c262c..4cab454c1 100755 --- a/test/usesselectorcheck.sh +++ b/test/usesselectorcheck.sh @@ -142,7 +142,12 @@ grep -q 'srcmut_sigchange' "$TMP/err" && no "the constant nonsense suggestion (s # fetch a canonical id; --outline always rides the classic 200-row map (no V1 lean default applies to # it) and its rows carry id= unconditionally, so it is the stable lookup path here. # row 6 (2026-09-12): the row prints the short id sc=; the canonical id composes as ::sc::n -CANON_ID="$( "$BIN" "$ROOT" --outline='src/notes.h:empty' --no-cache 2>/dev/null | python3 -c ' +# expand-lean-k64 (2026-10-08): --outline of definitions it fully serves no longer rides the map by default; the lookup asks +# for the classic ranked map explicitly with --top-k, which is what carries the id= rows read here. +# --top-k=100000, not 200: the lookup must not depend on the symbol's RANK. The 200-row default held NoteIndex::empty only while every +# std::*.empty() call in src/ bound to it by name; the C++ standard-member gate (graph.h StdMemberGate) declines those, its +# four typed callers stay, and the row fell out of the default window. The assertions below are unchanged. +CANON_ID="$( "$BIN" "$ROOT" --outline='src/notes.h:empty' --top-k=100000 --no-cache 2>/dev/null | python3 -c ' import re, sys doc, name, scope = sys.stdin.read(), sys.argv[1], ( sys.argv[2] if len( sys.argv ) > 2 else None ) for f in re.finditer( r"]*>(.*?)", doc, re.S ): diff --git a/test/wrapverbscheck.sh b/test/wrapverbscheck.sh index 903b595b4..270ceccf7 100755 --- a/test/wrapverbscheck.sh +++ b/test/wrapverbscheck.sh @@ -289,27 +289,35 @@ else fi echo -echo "=== 8. one-shot --for recipes budget with --token-budget, never --max-tokens ===" +echo "=== 8. one-shot --for recipes name no budget (never --max-tokens, never a --token-budget) ===" # --for does not read --max-tokens: it warns on stderr and emits the full, unbudgeted result. # --token-budget is the flag that actually shapes --for's output. The wrap recipes are the tool's # own advice, so they must not teach the inert pairing (found live 2026-08-20: wrap claude # recommended `--for="" --max-tokens=2000`, which produced an unbudgeted map + a warning). -for _agent in claude codex cursor windsurf gemini opencode aider; do +for _agent in claude codex cursor windsurf gemini opencode openclaw hermes aider; do _out="$( "$BIN" wrap "$_agent" 2>/dev/null )" - if echo "$_out" | grep -- '--for=' | grep -q -- '--max-tokens'; then + if ! echo "$_out" | grep -q -- '--for='; then + no "wrap $_agent printed no --for= line — the --max-tokens check below would pass on nothing" + elif echo "$_out" | grep -- '--for=' | grep -q -- '--max-tokens'; then no "wrap $_agent pairs --for with --max-tokens — inert advice, --for ignores that flag" else ok "wrap $_agent never pairs --for with --max-tokens" fi done -# the three recipes that ship a budgeted one-shot --for line must budget it with --token-budget= -for _agent in claude opencode aider; do +# K29 (2026-10-08): the one-shot --for recipes name NO budget: all five CLI-first agents (claude, opencode, codex, +# openclaw, hermes) print the same line, plus aider's repo-map line. A budget handed to an agent is a ceiling it +# pastes for every task: in round-2 mechanical (provisional) scoring --token-budget=2000 answered fewer of the +# measured questions than the default, and the owner rule is lean AND answered. The default budget shapes the +# answer; the recipe must not narrow it. +for _agent in claude opencode codex openclaw hermes aider; do _out="$( "$BIN" wrap "$_agent" 2>/dev/null )" - if echo "$_out" | grep -- '--for=' | grep -q -- '--token-budget='; then - ok "wrap $_agent one-shot --for recipe carries --token-budget=" + if ! echo "$_out" | grep -q -- ' --for=""'; then + no "wrap $_agent lost its one-shot --for=\"\" recipe line" + elif echo "$_out" | grep -- '--for=' | grep -q -e '--token-budget' -e '--max-tokens'; then + no "wrap $_agent one-shot --for recipe carries a budget flag — it must recommend the default budget" else - no "wrap $_agent one-shot --for recipe lost its --token-budget= budget" + ok "wrap $_agent one-shot --for recipe is present and names no budget flag" fi done

+ +````` + +## `./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.81s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [131 more display lines; full output is 16660 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.86s** + +````` +(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: 3.36s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-10 keep this branch-free — it sits inside the PageRank sort comparator 6009ca69336d0adceb8f7ce6019372f7fd413e5c integration/train-26c +````` + +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-10 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 6009ca69336d0adceb8f7ce6019372f7fd413e5c integration/train-26c +````` + +## `./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.45s** + +````` +{"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":12805,"graph_unresolved":12845,"graph_unindexed":256,"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":746,"graph_ambiguous":12805,"graph_unresolved":12845,"graph_unindexed":256,"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.60s** + +````` +{"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.75s** + +````` +{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], +"graph_ambiguous":12805,"graph_unresolved":12845,"graph_unindexed":256,"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":746,"graph_ambiguous":12805,"graph_unresolved":12845,"graph_unindexed":256,"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.59s** + +````` +(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.60s** + +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":12805,"graph_unresolved":12845,"graph_unindexed":256,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false, … [line truncated: 354 more bytes on this line] +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` + +*The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* + +**wall time: 3.36s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":12805,"graph_unresolved":12845,"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,"mul … [line truncated: 538 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], +"graph_ambiguous":12805,"graph_unresolved":12845,"graph_unindexed":256,"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: 11.14s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [125 more display lines; full output is 15757 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.05s** + +````` +(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 26062 per-symbol rows, 26905 indexed symbols total) +````` + +Artifact written: + +````` + 4419391 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer 01b0d59d63f2a402c75ee73a7c3b9e942e6094ebf92838a6833a4d824cfec943 +head 6009ca69336d0adceb8f7ce6019372f7fd413e5c +dirty 1 +absorbed 2 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699bdb 0 +ccx 7d8862be4e +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 10.00s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [120 more display lines; full output is 14641 bytes on 1 raw line(s)] +````` + + +--- + +# the MCP dialect — the same verbs over stdio JSON-RPC (one-shot exchange, not a persistent server) + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp` + +*initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 945 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). Hits carry text; enclosing rows a fetch_body handle. pattern = the literal; in=any lifts the span tiering (default: tightest non-empty tier; rest rides as suppressed_co … [line truncated: 837 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional ceiling (the CLI --for --token-budget). The header carries route= (which ranker answere … [line truncated: 1053 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 657 more bytes on this line] +{"name":"owners","description":"Bus-factor: recency-weighted (6-month half-life) author ownership per file. symbol = optional, restricts to the file that defines it; limit/offset page the rows. at= is the commit these numbers were computed at (+dirty = the working tree differed). An @FILE:LINE line- … [line truncated: 879 more bytes on this line] +{"name":"replace_symbol_body","description":"Replace a symbol's ENTIRE definition (signature through closing brace) with new_body — splices over the full def span, preserving every byte outside it verbatim; new_body must be a complete, well-formed definition. One trailing newline folds (trailing_n … [line truncated: 1323 more bytes on this line] +{"name":"insert_before_symbol","description":"Insert text immediately BEFORE a symbol's definition (its first byte); padded to the file's own blank-line seam (separator_padded=N). Same refusal contract as replace_symbol_body (not found / ambiguous / stale index / symlink / concurrent write, file unc … [line truncated: 877 more bytes on this line] +{"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] +{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] +{"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 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: 2117 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1385 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= 47143 (~tokens at 4 bytes/token: 11785 ) +explore desc_bytes= 1322 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'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +whereis desc_bytes= 701 schema_bytes= 851 required=['symbol'] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] +affected desc_bytes= 790 schema_bytes= 610 required=['files'] +from_trace desc_bytes= 829 schema_bytes= 559 required=['trace'] +find_symbol desc_bytes= 752 schema_bytes= 590 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +rank_by desc_bytes= 758 schema_bytes= 481 required=[] +for desc_bytes= 445 schema_bytes= 777 required=['task'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symbol'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] +grep desc_bytes= 445 schema_bytes= 562 required=['pattern'] +memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] +path_between desc_bytes= 357 schema_bytes= 506 required=['from', 'to'] +cochange desc_bytes= 388 schema_bytes= 476 required=['file'] +analyze desc_bytes= 546 schema_bytes= 318 required=[] +lego desc_bytes= 360 schema_bytes= 467 required=['type'] +mentions desc_bytes= 305 schema_bytes= 486 required=['symbol'] +quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/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: 2117 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The core manifest, summarised (name / description bytes / required args): + +````` +tools= 8 manifest_bytes= 14166 (~tokens at 4 bytes/token: 3541 ) +explore desc_bytes= 1322 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= 829 schema_bytes= 559 required=['trace'] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +quality_delta desc_bytes= 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.* + +````` +{"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: 6526 more bytes on this line] +"_index":"[index: files=2787 symbols=26903 hash=6fb46779]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [733 more display lines; full output is 23538 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 7895 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=9484 > 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.12s** + +````` + + + + +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::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& impl … [line truncated: 63 more bytes on this line] +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t> … [line truncated: 25 more bytes on this line] +inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +struct IngestFileScan +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 std::string spanTierMemoPath( const std::string& diskPath ) +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +inline std::uint64_t byteHash( const char* data, std::size_t n ) noexcept +… [48 more display lines; full output is 10067 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { +… [12 more display lines; full output is 4171 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.* + +````` + + + + + +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 +… [105 more display lines; full output is 16339 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.* + +````` + + + + + +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]).* + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…constexpr std::array<LangEntry, 53> kLangTable = +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 +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [53 more display lines; full output is 10020 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.* + +````` + + + + +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 +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [72 more display lines; full output is 10746 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.* + +````` + + + + +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 ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [75 more display lines; full output is 10738 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=2161 est_tokens=5489 + +━━ 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] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. When `--callers`, `--callees`, `--uses`, `--impact` or `--path` refuse a selector that matches no indexed definition, they also print an answer on stdout: the verb's element with `found="0"` and the name to retry with. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | +| 5 | The memory guard stopped the run: over `--max-memory` (default 65% of the machine's memory) with no partial answer possible, or a verb other than the default map facing a partial index. One stderr line names the limit and the override. | + +#### 6.3 JSON output + +`--json` emits JSON instead of XML, with keys mirroring the XML attribute names one to one. It is an +allow-list: the default map, `--for`, `--pack-task`, `--callers`, `--callees`, `--impact`, +`--quality-delta`, `--test-gate`, `--metrics`, and `--plan-lanes`, which is JSON-native. Every other +verb refuses on stderr with exit 1 rather than silently falling back to XML, so a verb added tomorrow +refuses by default. + + + +━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.445) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] +### Root spelling (maintainer-owned; reproduce it once so you recognise it) + +```bash +… [159 more lines, 14008 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 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.* + +````` +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] +### `--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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [367 more display lines; full output is 11163 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 1.21s** + +````` +(empty) +````` + +Artifact written: + +````` + 156658 /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 7913 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [159 more display lines; full output is 7614 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1905 more display lines; full output is 52114 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** + +````` + + +````` + +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 5589 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 5449 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [66 more display lines; full output is 6181 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).* + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: 50dfd49c3 + (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.09s** + +````` + + +````` + +## `./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.* + +````` + +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 3166 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.* + +````` + + + + + +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 +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / - … [line truncated: 48 more bytes on this line] +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +… [108 more display lines; full output is 14886 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [145 more display lines; full output is 15012 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 calls left unbound (no evidence chose one def). 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 10443 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [902 more display lines; full output is 23855 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 1.25s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5755 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 1.85s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [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: 1.17s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 1.02s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5104 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [706 more display lines; full output is 103199 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 1.58s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [689 more display lines; full output is 70339 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:395, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* + +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 16956 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=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [189 more display lines; full output is 9495 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [835 more display lines; full output is 33163 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +2849 files · 27441 symbols · 26516 edges · 3248 modules (12310 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 3292 declaration, 1982 header, 2365 source, 4671 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 +- **docs::`--top-k=N`@COMMANDS.md:59:9034 [pack,allow,insert]** — 181 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:1650:135191 [compute,read,clone]** — 74 symbols +- **src::sliceKindIs@slice.h:368:24239 [match,collect,push]** — 48 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,compute,verify]** — 42 symbols +- **src::ENSURES@infra/Diagnostics.h:388:25729 [pack,score,apply]** — 41 symbols +- **test/ownerhopfix/gain/pkg::target_01@fanout.py:74:1458 [push]** — 41 symbols +- **src::tokText@defectshape.h:128:5308 [scan,find,join]** — 40 symbols + +## God files (most depended-on; showing 10 of 210) +- `src/model.h` — 89 dependents +- `src/infra/emit.h` — 86 dependents +- `src/infra/Diagnostics.h` — 60 dependents +- `src/serialize.h` — 40 dependents +- `src/graph.h` — 38 dependents +… [34 more lines, 3899 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 10540 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
8097"] + n102["src/infra
1280"] + 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"] + n590["test/rubyattrsfix
85"] + n443["test/massfix
77"] + end + n73["docs
839"] + n2[".github
608"] + n0["(root)
560"] + 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: 1.20s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [922 more display lines; full output is 101179 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.12s** + +````` + + +````` + +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: 10.74s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [120 more display lines; full output is 14947 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: 7.36s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +````` + + + +````` + +## `./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: 2.27s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [48 more display lines; full output is 6303 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* + +**wall time: 9.39s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 2.63s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [167 more display lines; full output is 23726 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=.* + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./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: 51.97s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [33513 more display lines; full output is 4639990 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** + +````` +(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: 17.18s** + +````` + + + + + + + + + + + + + + + +````` + +## `./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: 5.34s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 5.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 13354 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --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.* + +**wall time: 5.20s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14367 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: 10.20s** + +````` + + + + + + + +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [378 more display lines; full output is 23271 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: 1.23s** + +````` +{"v":1,"verb":"plan-lanes","at":"50dfd49c3","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":2849,"symbols":27441,"edges":265 … [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":652,"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":1157,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":10,"churn":51,"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":3029,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":11,"churn":441,"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":689,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":441,"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":5489,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":381,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":47}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":32}, +{"p":"./src/editcheck.h","symbols":1,"churn":51,"ccx":10,"hotspot_rank":38}, +{"p":"./src/main.cpp","symbols":2,"churn":441,"ccx":12,"hotspot_rank":4}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":381,"ccx":2,"hotspot_rank":5}, +{"p":"./src/pageview.h","symbols":1,"churn":19,"ccx":2,"hotspot_rank":140}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":36}, +{"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/gitmine.h","n":"gitWindowBoundarySha","scope":"rw","key":"18627516699d7062","id":"./src/gitmine.h::rw::gitWindowBoundarySha","id_addressable":true,"id_collides_with":0,"l":1672 … [line truncated: 69 more bytes on this line] +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3282,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":488,"tested":0}, +… [71 more display lines; full output is 20743 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` + +*NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* + +**wall time: 1.33s** + +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":"50dfd49c3","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2849,"symbols":27441,"edges":26516,"ambiguous":2931,"unresolved":13056},"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":652,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":381,"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":3282,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":488,"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":5489,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":381,"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":488,"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":94}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":381,"ccx":2,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":57}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":4,"hotspot_rank":23}], +"symbols_total":3283,"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":121,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2903,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":488,"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":3282,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":488,"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":2982,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":488,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":121,"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":3047,"ord":0,"overloads":1,"amb":1,"cx":4,"ccx":3,"churn":488,"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 18967 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).* + +````` + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +… [233 more display lines; full output is 24820 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [259 more display lines; full output is 26019 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [212 more display lines; full output is 24730 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: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 + #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 9111 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 +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / - … [line truncated: 48 more bytes on this line] +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +… [62 more display lines; full output is 10562 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.05s** + +````` +(empty) +````` + +Artifact written: + +````` + 435410 /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 .* + +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 = +… [59 more display lines; full output is 43073 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: 3.34s** + +````` + + + + + + + + + + +````` + + +--- + +# 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: 1.21s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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 +1165,9156,9802,171,1423,1462,1749 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 +src/mcpindex.h:1268,src/graph.h:9159,src/graph.h:9853,src/eval.h:325,src/main.cpp:1437,src/main.cpp:1476 src/main.cpp:1477 src/main.cpp:1491,src/main.cpp:1869 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +````` + + + +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":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165","sites_at":["src/mcpindex.h:1268"]}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:9156","sites_at":["src/graph.h:9159"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:9802","sites_at":["src/graph.h:9853"]}, +{"t":"fn","n":"runEval","p":"src/eval.h:171","sites_at":["src/eval.h:325"]}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1423","sites_at":["src/main.cpp:1437"]}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1462","sites_at":["src/main.cpp:1476","src/main.cpp:1477","src/main.cpp:1491"]}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1749","sites_at":["src/main.cpp:1869"]}]} +````` + +## `./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