diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 31be4775c..f5ac63e68 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -240,6 +240,7 @@ "greptiercheck.sh": 16.6, "guardmsgcheck.sh": 1.1, "handoffcheck.sh": 12.6, +"handlerrulecheck.sh": 3.0, "hasacheck.sh": 2.7, "hazardpatterncheck.sh": 5.2, "headsnapcachecheck.sh": 19.9, diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 55160fb30..4dd14ff2d 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -121,6 +121,7 @@ ack api-surface 8a92173ded649e17 14 cid=f76e97c84ac7c168 by=src/* lane 2 of the ack api-surface 8d58de9bb922f582 4 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 api-surface 8fa8514c421e478d 4 cid=7826a06c6d682763 now=4 was=3 facet=contract-change p=src/editcheck.h:833 train 30 fcall-forward nit N1 (d62c00e0): ONE trailing span parameter, the graph's keptForwards, so this call-site reader applies the one model.h forwardCounts rule (--uses' own) instead of counting a forward buildGraph dropped; every caller passes g.keptForwards in the same commit; red-first in test/fcallforwardcheck.sh S1-S3. ack api-surface 925094be92085dae 3 cid=714cea1e1b31a1ae A6: rollbackMessage gained a 'cause' parameter one commit after this lane introduced it (57fe5fc). It is a header-inline helper in namespace rw::editplan with two callers, both in the same function in the same file; no consumer outside this lane ever saw the 2-arg form. The parameter is what lets the concurrent-write abort reuse the rollback disposition wording instead of growing a second copy of it. +ack api-surface 92784a33434e383d 2 cid=e19f01572dacdb87 now=2 was=1 facet=contract-change p=src/hopbody.h:121 fix1: intended internal signature changes (graph for the hop hedge, language for the comment rule, the answer-wide follow budget); every caller updated in the same commit ack api-surface 92ac9caf38b8aab0 4 root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack api-surface 93848617cae1fe4e 2 cid=a42863f577100fa6 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 95cc88ca4aab7039 4 cid=25c411d4871bda46 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. @@ -137,6 +138,7 @@ ack api-surface ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the l ack api-surface ad50354348e0b35e 7 cid=1ca210dbe9314b90 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 aeed75863f7b617d 3 --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 api-surface b496a273ae1564ef 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack api-surface b530db5496337fe8 4 cid=1a7084b3d1af69e4 now=4 was=3 facet=contract-change p=src/hopbody.h:370 fix1: intended internal signature changes (graph for the hop hedge, language for the comment rule, the answer-wide follow budget); every caller updated in the same commit ack api-surface b65c78efe3e73a34 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 b689422adfc04435 5 cid=cdb3a6ea16c83186 lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked ack api-surface b6a24afef32a68a8 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. @@ -172,6 +174,7 @@ ack api-surface e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 ack api-surface e514a69013d0934c 6 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 api-surface e57abf94a3b1f3a4 5 cid=788aa47a3ec3d3f2 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 e6bdc5187566f3d7 8 cid=615b35e39fe420e4 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 e9041801bd6f7cdb 4 cid=734066326027c1cd now=4 was=3 facet=contract-change p=src/forfold.h:382 fix1: intended internal signature changes (graph for the hop hedge, language for the comment rule, the answer-wide follow budget); every caller updated in the same commit ack api-surface ebaa8de80f3b5caa 2 cid=fbf73430469857ac A3 (review of #219), the other half of the TestRunnerIndex ack above: runHintClauseIfRows takes the root-relative predicate as well as the row count, because the clause's ROOT-RELATIVE sentence must answer to the same fact that spells run= itself — a multi-root run declares no root= and correctly keeps an absolute command, so a clause claiming relativity there is a wrong answer, not a cosmetic one. The parameter is the fix and there is deliberately no default: a defaulted false would let a caller silently drop the sentence, which is the silent-wrong-answer shape this lane exists to remove. All ten call sites pass the predicate they already hold (packtask, flipimpact, handoff, partition, prcontext x2, verbs_change x2, situ, testmap). Visible only now because #214 landed the one-parameter form onto main first; once this lane merges, main carries the two-parameter form and the row cannot recur. Proven by runhintcheck 2c/2d (the root-spelling matrix, and multi-root does not claim a relativity it does not have) and prcontextcheck 49 rows, full suite gates=629 pass=627 skip=2 fail=0. ack api-surface ed05ee0357d016f1 4 --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 api-surface eea83c3db0f03d69 20 cid=a4f7862584788fbd by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | 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. @@ -531,7 +534,7 @@ ack complexity efe685bb50482275 58 cid=10ded7c311552cd1 L10 finding 9b: buildStr ack complexity f1aa10922e41f258 27 cid=6670a895f0520c6a E3 (terminality round A, lane E): run_editsuite.py: the --cost-arith subcommand (E3's arithmetic, per task) ack complexity f307304e87733112 17 cid=76db6c1cce778585 fix-round follow-on: the recall-ceiling MCP branch (+3 ccx on the dispatcher), the apostrophe word-boundary guards in firstQuotedLiteral (the refusal logic IS the fix), and re-touch churn on the flag table / RecallShape comment / installer timeout re-pin ack complexity f8d747d123b4143d 39 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. -ack complexity fce48dedc40bc8eb 18 cid=4b3785b8a81e3973 now=18 was=15 p=src/mcpverbs.h:3166 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack complexity fce48dedc40bc8eb 42 cid=de50fbe583fd76d2 now=42 was=29 p=src/mcpverbs.h:2447 train 41: path-hop-body cumulative growth; split lane QD-PATHTEXT (train 42) extracts the shared body/route/alts assembly | prior: 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 fd4cc5c6c0ad17d9 34 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack dead-code:new-symbol 033294a4fc57eac8 0 cid=08917f8cd6a0890c timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack dead-code:new-symbol 0a892722cd821466 0 cid=e801e16ce70d1cd0 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -1613,6 +1616,7 @@ ack verbosity 28bbae8cfa29c022 86 cid=696dc4894e340595 by=src/* round-4 F-02 (te ack verbosity 2a2f2487082cc6d8 153 cid=ba772c32f101b4bb or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS | prior: mechanical flag-addition footprint of --scope: one help entry, one composition guard, one kViewFlags row and one Config member. printUsage/validateConfig grow by construction on every flag this tool adds. ack verbosity 2abd26fdd3d1e6df 215 cid=80501bdf3db6fba6 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. | prior: P3.2 plan-lint model correction (validated against the field-evidence source repo's own flagship plan): computePlanLint/writePlanLint grow in complexity/verbosity because status now resolves from the §Status ledger as well as the card body (the real house dialect), extracted into resolveLedgerStatus to keep the growth to the genuinely new logic; missingWhy clones gitmine::coPairAttr and testmap::affectedSeededBy — the same trivial-classifier-idiom false positive already acked for glyphName/shapeName (a 3-branch string-constant selector has almost no room to look structurally unlike another one) ack verbosity 2b0d39faec60fc08 91 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack verbosity 2b6cc710ac4a2c5f 104 cid=e2b0c305d9e41896 now=104 was=83 p=src/forfold.h:426 train 41: path-hop-body cumulative growth; split lane QD-PATHTEXT (train 42) extracts the shared body/route/alts assembly ack verbosity 2bea60141008e4d0 80 cid=0f6117d2b7b8058c now=80 was=0 p=src/receiverevidence.h:261 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 31105421c3ce88ae 249 cid=86626441189f2998 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack verbosity 3298be65bf6058ef 1265 cid=7327ba02472bb8c7 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. | prior: M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. diff --git a/README.md b/README.md index 96f8f50c1..e6d3443d1 100644 --- a/README.md +++ b/README.md @@ -982,9 +982,9 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers
-192 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin +193 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 192 long flags advertised in `--help`, across seven families — plus an MCP +Around the core sit 193 long flags advertised in `--help`, across seven families — plus an MCP server, so a coding agent can call any of them mid-task instead of grepping and reading whole files. `--help` prints one line per flag (~4.5K tokens); `--help=--FLAG` prints that flag's full entry with every caveat, `--help=SECTION` one family, and `--help=all` the whole catalog. @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-726 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +729 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 **726 gate scripts** and is the authoritative list; +`test/regression.sh` names **729 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a @@ -2684,7 +2684,7 @@ identity of the index, and says which one is at fault. ### 5. Command families -The `--help` output groups 192 long flags advertised in `--help` into seven families. The `--help=` +The `--help` output groups 193 long flags advertised in `--help` into seven families. The `--help=` column below is the argument that prints one family: `ripwire --help=navigate`. `ripwire --help=--FLAG` prints one flag's full text — the caveats, the units, what it refuses and why. `ripwire --help=all` is the whole catalog, about 46,000 tokens. @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 287 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 298 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/changelog.d/ci-flakes-3-memguard-b6b.md b/changelog.d/ci-flakes-3-memguard-b6b.md new file mode 100644 index 000000000..41f37d064 --- /dev/null +++ b/changelog.d/ci-flakes-3-memguard-b6b.md @@ -0,0 +1,18 @@ +### Fixed — three flaky gate arms: memguardcheck (B6b, B21) and pargatescheck (I control) + +None is a product change; each arm depended on something the gate did not control. + +- `test/memguardcheck.sh` (B6b): the arm sent a `find_symbol a_02` as the second MCP call against an index cut by the crawl + trip at entry 30. Which fixture files survive that cut is the filesystem's directory order (the crawl does not sort before it + stops): on macOS `a/f02.c` is kept; on a Linux runner whose directory order places it later, `a_02` is outside the cut index and + the answer is a not-found error, which carries no `_memory_stop` envelope field, so the arm read an empty string (red on main + and on both train-40 PR runs, ubuntu release shard 4/4, green on rerun). The follow-up call is now `grep`, whose answer is a + result on any partial index, so the arm checks the same thing (a later answer from the same cut index keeps the disclosure) on + every filesystem; the `find_symbol` call stays as a third probe asserted on the shape it returns, with a NOTE line when it fell outside the cut. +- `test/pargatescheck.sh` (I control): the control copy that puts the old "wait, then look at the stop" loop back waited 0.5 s + before looking, leaving 0.2 s for the probe gate to start and pass its 0.3 s mark; at load average 9 to 13 it failed three + runs of three on unmodified main. The control's first wait is now 3.0 s; the ordering defect it models is unchanged. +- `test/memguardcheck.sh` (B21): the arm required the refusal to return within 4 s of a 6 s git-log stream, but the shim's 60 + spawned sleeps stretch that stream on a busy host and the refusal took 5 s at load average 13 to 19. The arm now asks what a + join would do, which is wait for the stream's end: the shim writes a marker after its last line and the refusal must return + without it. diff --git a/changelog.d/defs-lookup-verb.md b/changelog.d/defs-lookup-verb.md new file mode 100644 index 000000000..f2fc8a9cb --- /dev/null +++ b/changelog.d/defs-lookup-verb.md @@ -0,0 +1,11 @@ +### Added — `--defs=NAME[,NAME…]`: which definitions a name binds, with the spelling that picks each one alone + +One call answers "which definitions does this name bind?" for up to 16 names in one index load. Each definition group (a C-family +prototype with the definition it declares is one; a TypeScript overload with its implementation is one) is a row with its +owner-qualified name, kind, `file:line`, language, `class=source|test` and its distinct graph callers (a floor), plus `sel=`: a +spelling every symbol verb accepts that the lookup re-resolved and proved binds that definition and nothing else (`name`, +`file:name`, `file:Owner.name`, then the `@file:line` seed; `sel_proven="0"` when none does). Source rows come first, then by +callers. A JSON/YAML key or a markdown heading of the same name is counted as `data_defs=` and never listed as code; a definition +the parser does not model (an assignment, `define_method`, `setattr`) is not one. More than 16 rows page with `--offset`; a single +name that binds nothing answers with the shared "not found" refusal. `--json` gives the same rows. The lookup lives in one header +(`src/defslookup.h`, `rw::lookupDefs`), reusing the definition-group rule the callers/impact tables use. diff --git a/changelog.d/for-wrapper-follow.md b/changelog.d/for-wrapper-follow.md new file mode 100644 index 000000000..6fbd96c1d --- /dev/null +++ b/changelog.d/for-wrapper-follow.md @@ -0,0 +1,13 @@ +### Added — the `--for` body fold follows a one-line wrapper into the function it calls + +When a body the `--for` fold serves is a thin delegator (one proven callee, no other call, at most 3 statement lines, such as +`fetch` returning `this.#dispatch(...)`), the callee's body is now printed right after it as ``, up to two +levels (`handle -> fetch -> #dispatch`). A callee bound by name alone, a body with a second call or a fourth statement line, and +a body the answer already carries are never followed. The followed bodies ride outside the fold's `budget_bytes=`, each whole up +to 4000 bytes (a longer one is cut and says so). `RIPWIRE_NO_HOP_BODY=1` turns it off. + +Measured: hono's default `--for "How does a request flow from app.fetch to a handler's Response?"` now carries `#dispatch`'s body +(router match, Context, the one-handler fast path, `compose`, error handling) after `fetch` (+1.9 KB). Over the in-sample tables +the follow fires on 3 of 163 S1 rows (+4.3 KB, all already complete), 2 of 39 MF1-DEV rows (+3.0 KB; ninja `Plan::AddTarget -> +AddSubTarget`) and 3 of 56 LARGE-DEV rows (+5.7 KB, one body cut at the 4000-byte guard); no row loses an item. +Gate: `test/hopbodycheck.sh`. diff --git a/changelog.d/message-handler-rule.md b/changelog.d/message-handler-rule.md new file mode 100644 index 000000000..8cadf9334 --- /dev/null +++ b/changelog.d/message-handler-rule.md @@ -0,0 +1,29 @@ +### Added — a posted message class names its handlers (`via="name-key"`, `` on `--path`) + +A Python message framework dispatches by a name the message's class carries: textual's `Message.__init_subclass__` writes +`cls.handler_name = f"on_{name}"`, and the pump looks the handler up with `cls.__dict__.get( method_name )`. That lookup takes a +parameter, so it can name every `on_*` method. The class is known where the message is posted, though: +`self.post_message( Button.Pressed( self ) )` names `Button.Pressed`, and its handler is `on_button_pressed`. Such a post site +is now a name-key hop to the methods of that name, with `cands=K` beside it. The hop shows in `--impact` +(`reaches_hedged_via="name-key"`), in the `--path` hedged bridge (at most 16 candidates), and in `near_sink`, where a +dispatched handler is now `why="declined:N" via="name-key"`, no longer `why="external"`. When `from=` is a poster and the +posted classes name a `to=` handler, a reachable="0" `--path` also prints ``. It holds the rule's `p=` and its +spelling, the functions that read the handler-name attribute with the method each one passes it to (textual: +`MessagePump._on_message` passes it to `MessagePump._get_dispatch_methods`, the MRO walk), and the posted classes, widest +first. The handler definitions are listed only when there are at most 16 of them. Above that, the answer gives a count and +`next=`, never a cut list. + +The rule is read from the repo's own source, never from a framework list. It needs a Python `__init_subclass__` with ONE +top-level write `cls.A = f"P{v}"` (not under an `if`/`else`/`for`/`while`/`try`/`match`), where every assignment of the local +`v` calls a camel-to-snake converter (a body holding the `[a-z][A-Z]` regex and `.lower()`) on `__qualname__`'s last two parts +or on `cls.__name__`. A `{namespace}_` branch reads a `namespace="x"` class keyword. A class takes the NEAREST rule base (the +most derived one); a class below an intermediate class whose own `__init_subclass__` also writes `cls.A` is not named, and two +unrelated rule bases decline. A base written by name that several classes carry binds only through the deriving file's own +import. Readers and posters are kept per rule. A post site is a call to a method of a class that reads `.A` outside test +paths, with a first argument that constructs a subclass: `X( … )`, `self.X( … )`, `Owner.X( … )` or `module.X( … )`. + +What it does NOT mean: the name is matched, not traced. Which candidate runs depends on the receiving object's class, and the +index does not decide that. A hop is never counted in `reaches=`, `reachable=` or `hops=`. The floors are: a variable +argument (`msg = Tick(); post_message( msg )`); a factory (`events.Click.from_event( … )`); a converter or qualname cut of any +other shape; `@on( Message )`-decorated handlers (already shown as value refs); and a class body's own `handler_name =` +(the rule runs after the class body and overwrites it). Gate: `test/handlerrulecheck.sh`. diff --git a/changelog.d/path-hop-body-fixes.md b/changelog.d/path-hop-body-fixes.md new file mode 100644 index 000000000..f3811a42b --- /dev/null +++ b/changelog.d/path-hop-body-fixes.md @@ -0,0 +1,9 @@ +### Fixed — the middle-hop body and the wrapper follow say what they prove + +- A middle hop reached by name (`obj.process(x)` with two `process` definitions) prints its body with `via="name" cands="2"` on + the ``, never as if it were the proven callee. +- A `--for` wrapper follow needs a function or method whose text holds exactly one call site of any kind, that call its last + statement: a declined, external or undefined call beside it, logic around it, a loop aggregator or a class field initializer + is no wrapper. A C `*out = v;` line counts as a statement; a Python docstring does not. +- `follow_of=` names the wrapper as its row does (`ChildRunner.launch`), and one answer follows at most 3 bodies; past that + `` counts the wrappers left and names the first callee. diff --git a/changelog.d/path-hop-body.md b/changelog.d/path-hop-body.md new file mode 100644 index 000000000..9a489a6a4 --- /dev/null +++ b/changelog.d/path-hop-body.md @@ -0,0 +1,16 @@ +### Added — a short `--path` answer prints the body of its middle hop + +A reached `--path` (and MCP `path_between`) whose chain has 2 hops now prints, after its hop rows, the body of its +middle hop inside ``: the same `` element `--expand` prints, without callee rows. The endpoints never get a +body. Each body is whole up to 4000 bytes; a longer one is cut at a line end and says so on the element (`truncated="1" +lines="1-K/T" next=` the call that serves the rest). A chain of more hops prints no body, and its root carries +`next="--expand=…"` naming the first middle hop. Inside a `--for` answer's graph-verb block on the CLI, a middle hop whose body the +answer already prints is not printed twice (MCP `for` has no served set to compare: a known divergence). `RIPWIRE_NO_HOP_BODY=1` turns it off. + +Why: `--path=fetch,compose` on hono named `Hono.fetch -> Hono.#dispatch -> compose` and stopped there, while the whole request +flow (path extraction, router match, Context, the one-handler fast path, compose, error handling) lives in `#dispatch`'s body. +Measured on the 163-row S1 replay: hono-12 goes from 0 to 6 of 7 required items (✗ -> ◐ by Rule C, ✗ -> ✓ legacy), 0 rows +lower; 9 of the 15 `--path` rows change, +3.1 KB in total (hono-12 +2.1 KB, kysely-14 +0.5 KB, six longer chains +~90 B of +`next=` each). The 2-hop gate is measured: 3 hops would add 1.6 KB on one already-complete row, 4 hops 13 KB on two more, and no +item. On LARGE-DEV one `--for` graph-verb path block gains a middle body (zed `get_any_active_multi_workspace`, +1.0 KB) and seven +gain only `next=`. Gate: `test/hopbodycheck.sh`. diff --git a/changelog.d/path-inline-alternatives.md b/changelog.d/path-inline-alternatives.md new file mode 100644 index 000000000..e1d509b71 --- /dev/null +++ b/changelog.d/path-inline-alternatives.md @@ -0,0 +1,9 @@ +### Changed — `--path` lists the other definitions of an endpoint whose pick is not provably unique + +`--path=fetch,compose` on hono printed `from_defs="7"`, bound one `fetch` and named none of the others, so an agent had to guess a +`file:name` spelling and retry. When an endpoint's name binds two or more definition groups, the answer (CLI and MCP +`path_between`) now ends with ``: one compact row per other definition with `p=`, `class=` and a +pasteable `sel=`, and `decl_only="1"` when the row has no body (a header declaration or an interface contract). Up to 16 rows; +past that `alts_shown=K defs=N next=--defs=NAME` pages the rest. A definition the answer already prints (the bound one, the +endpoint of a listed ``, or a function whose body sits in ``) is not listed again, and a C prototype no `#include` ties to a definition is not a second candidate +(the rule `from_defs`/`to_defs` use), so an endpoint that binds one definition, or a `*` glob endpoint, adds nothing. diff --git a/changelog.d/path-route-body.md b/changelog.d/path-route-body.md new file mode 100644 index 000000000..ff205e80f --- /dev/null +++ b/changelog.d/path-route-body.md @@ -0,0 +1,13 @@ +### Added — an unreached `--path` prints the bodies of the route it names + +A `reachable="0"` `--path` (and MCP `path_between`) that names a route it could not trace — the bridge rows (a poster, the queue +consumer, the dispatcher) and the `` readers — now prints those functions' bodies in ``: the bridge rows within 2 hops of `from=` (never `to=`), then the readers, at most 4 bodies (past that `capped="1"` +and `next=` the first one left out), each whole up to 4000 bytes. Every body is a hedge: one under a crossing carries that hop's +`via=` and `cands=` (`via="queue" cands="3"`), a reader's carries `via="name-key"`, and none of it enters `reachable=` or `hops=`. +Inside a `--for` answer's graph-verb block it stays off (that answer serves its own bodies; the one in-sample fire there, a +vscode `--for`, added 7.7 KB that named nothing the question asked). `RIPWIRE_NO_HOP_BODY=1` turns it off. + +Why: textual's `--path=post_message,on_*` named `post_message -> _process_messages_loop -> _dispatch_message` and the reader +`_on_message`, but the answer to "how does a posted message reach its handler" lives in their bodies (the queue put, the loop's +get, the bubble to `_parent`). Measured: textual-11 goes from 4 to 6 of 6 required items by Rule C (◐ -> ✓), +7.0 KB. diff --git a/changelog.d/private-member-selectors.md b/changelog.d/private-member-selectors.md new file mode 100644 index 000000000..d8ce7a64a --- /dev/null +++ b/changelog.d/private-member-selectors.md @@ -0,0 +1,7 @@ +### Fixed — a JS/TS private member resolves as the rows print it: `Hono.#dispatch` + +Train 40 started printing owner-qualified names (`n="Hono.#dispatch"`) on `--path`, `--callers` and `--callees` rows, but pasting +one back failed: `--callees=Hono.#dispatch` answered "not found" and `--path=Hono.#dispatch,compose` exited 1 on hono, because the +spelling splitter read the `#` as Ruby's `Class#method` separator and saw an empty segment. A `#` that starts a segment is now part +of the name, so `Owner.#name` and `file:Owner.#name` bind the one private member they name; Ruby `Cart#total` reads as before, and +`Owner.name` without the `#` still names nothing. Found while grading `--path` answers on hono. Gate: `test/writtenspellingcheck.sh` (1b). diff --git a/changelog.d/py-alias-split-marker.md b/changelog.d/py-alias-split-marker.md new file mode 100644 index 000000000..52d78095e --- /dev/null +++ b/changelog.d/py-alias-split-marker.md @@ -0,0 +1,11 @@ +### Fixed — a module `__getattr__` no longer withholds a renamed-import call to a function the module defines + +A Python call through a renamed import (`from m import f as g; g()`) stayed undecided whenever `m` defined a module-level +`__getattr__` (PEP 562), as if `__getattr__` could replace `m.f`. It cannot: Python consults it only for a name the module +does not bind. The call now binds `m.f` as a proven edge in that case too, and a module `__getattr__` in the calling file no +longer withholds it either. It still stays undecided when `f`'s definition in `m` sits inside an `if`, `try`, `with`, `match` or +loop at module scope (it may not run, and `__getattr__` then answers), when `m` writes its namespace dynamically (`globals()[...]`, +`setattr(...)`), and in every case listed for the renamed-import change. Known limit, unchanged: a module that rebinds a +name some other way (`vars()[...]`, `locals()[...]`, `exec(...)`, `__dict__[...]`, `sys.modules[__name__].name = ...`) is not +recognized as a dynamic write, so a pin into it can be wrong, now also in files that define `__getattr__`. On CPython's standard library this restores 11 +call sites (`re.sub`, `re.escape`, `re.compile`, `platform.system`, `platform.architecture`). Gate: `test/pyaliascallcheck.sh`. diff --git a/changelog.d/rust-fn-valued-slots.md b/changelog.d/rust-fn-valued-slots.md new file mode 100644 index 000000000..a22aefe48 --- /dev/null +++ b/changelog.d/rust-fn-valued-slots.md @@ -0,0 +1,22 @@ +### Added — Rust functions stored in struct fields are joined to the `(x.f)( … )` calls through those fields (`--callers`, `--callees`, `--for`) + +A Rust function stored in a struct field — a struct expression (`CallEval { eval: direct_eval }`, `Self { eval }`), a value +wrapped in `Box::new` / `Rc::new` / `Arc::new`, a field assignment (`self.h = h`), or an argument of a registrar reached by a +path call that stores its parameter there (`CallEval::new( stack, eval_block_with_early_return:: )` with `Self { …, eval }` +in its body; a method's `self` is position 0) — is now joined to the calls through that field, `(self.eval)( … )` (the only +spelling Rust has for calling a function-valued field). `--callers` gains `called_by= called_at= via= cands=K` on a `` +row, `--callees` lists ``, and `--for` prints `slot_by=` / +`slot_fields="F=K@C"` on shown rows. A slot type, a stored function and a registrar are the in-tree definition their path AS +WRITTEN reaches: a module path (`crate::a::Cfg`, `super::eval::f`, `mod a; a::f`) the one file it names, never the writing +file's own namesake; a path led by an in-tree crate's name that crate; a bare name its own file's, else its crate's, else the +repo's only one in a crate the file `use`s (`Self` is the impl type). `std` / `core` / `alloc`, an out-of-tree crate +(`use tokio::spawn`, `use extcrate::Hook`), a `use … as` rename or an unknown module reach no in-tree definition: no join. Two +crates' same-named structs are two slots; a bare name in a file that `use`s an out-of-tree glob (`use extcrate::*`) is its own +file's definition only, since another file's namesake is one guess beside the glob's (it joins by the field name, `via="slot-name"`); a `self.f` call is typed +(`via="slot"`), a call or store through another receiver joins by the field name only (`via="slot-name"`, `~` on `--for`). Not +joined (floors): a closure stored in a field, a method-call registrar (`r.set( f )`), a value inside a macro, an enum variant's +field, a value or registrar inside an inline `mod m { … }` (including `#[cfg(test)] mod tests`: its functions are module +members). Rust value references are read only as path-call arguments and field stores: a method-call argument +(`iter().map( helper )`), `let g = f`, an array element or a returned function is not read, so `--safe-delete` / `--dead-code` +can still call such a function unreferenced. Every row is a MAY-call clue, never a proven call and never in `count=`. Measured on nushell (`CallEval.eval`, called +by `CallEval::run` / `run_prebound`). Gate: `test/fnslotcheck.sh` (R*/N8-N12/X2/F3 arms). diff --git a/changelog.d/upward-2hop-ranked-caller-hop.md b/changelog.d/upward-2hop-ranked-caller-hop.md new file mode 100644 index 000000000..204611846 --- /dev/null +++ b/changelog.d/upward-2hop-ranked-caller-hop.md @@ -0,0 +1,13 @@ +### Added — a `--for` task about an error carries the callers chain of the narrow-funnel function it ranks + +A `--for` task that names an error (a whole word of error, exception, panic, raise, throw, fail, crash, segfault, Traceback and their inflections, or an error type written as one identifier such as `DependencyCycleError` or `dependency_cycle_error`; not a "How does ..." explanation question) but no function now +carries the same `` element the name-exact route carries, marked `by="rank"`, when one of the answer's r=1..3 rows is +ONE function (the callers verb's own resolution, exact or no row) with one to three callers: its 1-hop callers, then their +callers as `d="2"` rows (at most 4), `next=` the whole caller radius. The seed is read from the rows `` emitted; a row +whose every caller is a test path, or whose every caller was matched by name alone (an interface or duck-typed call), is +skipped; among the rest the seed is the first whose own body holds a string literal with two of the task's words (the function +that raises the message), else the first, and only for a statement, not a question. And the element has its own present-only legend clause saying it is a lead found +by rank, not a function the task named. Everything outside the element and its clause is byte-identical to the answer without +it (`RIPWIRE_NO_CALLER_HOP=1`); `--json`, `--signatures-only` and an explicit `--token-budget` carry none. Measured on in-sample +tasks: three error-message questions (two Go large-repository rows, one C++) now name the chain above the function that +detects the error; it fires on 0 of 163, 1 of 39 and 5 of 56 questions in three in-sample sets (0 %, +0.15 % and +0.35 % bytes). Gate: `test/forcallerhopcheck.sh` (R arms). diff --git a/changelog.d/versions.md b/changelog.d/versions.md index b2dab9258..19cb7525c 100644 --- a/changelog.d/versions.md +++ b/changelog.d/versions.md @@ -1,6 +1,6 @@ ### Changed — the versions this change moves, stated once -`kParserVer` 206 → 251 (file tag 252): the Rust written-type receivers carried 202 (203) on their branch and the struct-field +`kParserVer` 206 → 259 (file tag 260): the Rust written-type receivers carried 202 (203) on their branch and the struct-field slot join 204 (205), and its fix rounds changed what 204 extracts, so a cache from that branch would replay the older slot rows; the interface-dispatch records carried 210 and 212 on theirs; the JS/TS written-binding records carried 218 and the Python rebind-withholding records a 224 placeholder on theirs; train 34 took 226; the Kotlin trailing-lambda capture (#155) @@ -10,10 +10,11 @@ records (a Python queue call's keyword arguments on its call reference) carried train 38 took 235 (236); the receiver typing of TS `#private` fields and of Python annotations and `mod.X( … )` constructors carried a 237 (238) placeholder on its branch and train 39 took 239 (240); the JS/TS naming records (a bare `module.exports` function, a named object literal's binding) carried 238, the name-keyed lookup qualifier 241 (242) and the Go function-valued -slots 247 (248) on their branches, and the Rust slot join carries 249 (250) on its, so 251 sits above every live branch and -pull-request number. A cache +slots 247 (248) on their branches and train 40 took 251 (252); the Rust slot join carried 249 (250) and the Python conditional +module-scope definition record (a `def` or `class` inside an `if`/`try`/`with`/loop at module scope) 255 (256) on theirs, and the +C/C++ slot join carries 257 (258) on its, so 259 sits above every live branch and pull-request number. A cache written by 0.6.8 is re-parsed once. `kCacheVersion` 36 → 46: a Rust binding typed only through a trait (`&dyn T`, `impl T`, a bound), a Rust `impl T for X` block and a JS/TS binding written after its declaration are new binding records, as is a JS/TS module-scope object literal's binding name, a Kotlin call's trailing-lambda byte (#155) widens the reference record, and a file's parse-health record gains the Rust re-parse count, so a cache written before them is rejected and rebuilt (the branches that added them used 37, 38, 39, 41, 42 and 45). The quality-snapshot scheme moves 22 → 24 (#228: Django and -unittest hooks and closure self-dispatch change the dead set; old snapshots are not used; the branch used 23). The session legend dictionary is `dictv=a3aaaa12b102080c entries=958`. +unittest hooks and closure self-dispatch change the dead set; old snapshots are not used; the branch used 23). The session legend dictionary is `dictv=05b48ed93c807ee2 entries=981`. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 6209e15fc..ee5f75790 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-27.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-28.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. @@ -40,7 +40,7 @@ Two limits apply to nearly everything here and are not repeated in every section **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) +**navigate / answer a question** — [`--around`](#--aroundsym) · [`--callers`](#--callerssym) · [`--callees`](#--calleessym) · [`--uses`](#--usessym) · [`--graph-query`](#--graph-queryexpr) · [`--external-surface`](#--external-surface) · [`--path`](#--pathsrcdst) · [`--defs`](#--defsnamename) · [`--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) @@ -71,17 +71,17 @@ $ ./build/ripwire . --top-k=5 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -107,17 +107,17 @@ $ ./build/ripwire . --top-k=5 --no-inventory - + - + - - - - + + + + ... [9 more line(s); run it to see the whole thing] ``` @@ -141,10 +141,10 @@ $ ./build/ripwire . --max-tokens=1500 - + - + @@ -171,7 +171,7 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + ``` **Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--sections`, `--recall`, `--handoff`, `--pr-context`, `--from-trace` @@ -216,14 +216,14 @@ _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` @@ -253,7 +253,7 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - + @@ -280,14 +280,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` @@ -315,7 +315,7 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - + @@ -347,15 +347,15 @@ _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] @@ -379,7 +379,7 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - + @@ -410,11 +410,11 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - - + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() @@ -440,14 +440,14 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - + - - + + L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -inline std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at ... [22 more line(s); run it to see the whole thing] ``` @@ -470,7 +470,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -501,7 +501,7 @@ _The repo's best-in-class instance to imitate before writing new code (picked by $ ./build/ripwire . --exemplar="format byte sizes for humans" - + /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 -━━ commands.md (relevance 11.012) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 588226 B; lines="556-581"; dropped_by_budget=163] [truncated: 1854 of 4257 bytes] +━━ commands.md (relevance 11.020) ━━ [sections: 1 of 165 selected (193 in doc), section-granular; whole doc 595976 B; lines="557-582"; dropped_by_budget=164] [truncated: 1854 of 4257 bytes] #### Pattern: a directory of dumped tool output as a knowledge base **Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a @@ -629,19 +629,19 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - + - - - - - + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -734,17 +734,17 @@ $ ./build/ripwire . --no-stable --top-k=3 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -768,7 +768,7 @@ $ ./build/ripwire . --around=rankGraphTeleport - + @@ -819,18 +819,18 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - + + - - - - - - + + + + + + ``` @@ -854,16 +854,16 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + - - + + - - + - - + + + ``` @@ -888,16 +888,16 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + - + - - + + - + @@ -925,19 +925,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] ``` @@ -959,18 +959,54 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - - - - + + + + + + + + + + + + + +... [12 more line(s); run it to see the whole thing] ``` **Shaped by:** `--connect`, `--limit` +### `--defs=NAME[,NAME...]` + +**Answers:** list the definitions each name binds (up to 16 names), with the spelling that picks each alone per definition: file:line, kind, class=source|test, callers, and sel= — paste it into any SYM verb + +**Try it** + +_Which definitions each name binds, one row per definition group with sel= (the spelling that picks it alone in any verb), class=source|test and in= callers; source first._ + +``` +$ ./build/ripwire . --defs=main,rankGraphTeleport + + + + + + + + + + + + + + +... [10 more line(s); run it to see the whole thing] +``` + +**Shaped by:** `--limit` + ### `--connect=A,B,C` **Answers:** show how 2..16 symbols relate: the smallest subgraph that joins them minimal connecting subgraph over 2..16 symbols: terminals + fewest joining intermediaries + call edges in TRUE direction (finds the shared-caller join a directed --path can't) [--connect-radius=N (1..12, default 6)] @@ -982,10 +1018,10 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - + @@ -1008,7 +1044,7 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - @@ -1092,7 +1128,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1151,7 +1187,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: 19beec1c7 +at: c8b49812b (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1170,18 +1206,18 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1205,7 +1241,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1290,16 +1326,16 @@ $ ./build/ripwire . --grep=DISCLOSE --and=cache CDATA text (/ context around it); complete=1 only for an exhaustive literal scan (beside pattern_meta=, of the literal reading only); = off-index. window: shown= capped= (capped=1 cut). hits_capped=1: hits= is a floor. root=: p= relative to it. parse_degraded=1: E … [line truncated: 1494 more bytes on this line] - + - - - - + + + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1501,14 +1537,14 @@ $ ./build/ripwire . --query="teleport pagerank" --top-k=5 - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1532,7 +1568,7 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + @@ -1568,15 +1604,15 @@ $ ./build/ripwire . --pack-signatures --top-k=10 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. pr_iters=N: PageRank iterations. declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry. external=K: K calls proven … [line truncated: 1465 more bytes on this line] - - + + - + - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1598,7 +1634,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)" ); @@ -1625,11 +1661,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + - + of ; resolved callees. window: shown= total= capped= (capped=1 cut). sibs_capped=/inc_capped=: 1 = cut. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : that callee matched by name alone (receiver unproven); it does NOT mean the edge is fal … [line truncated: 443 more bytes on this line] -: 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= lcom4= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, class cohesion, 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=). --> - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1801,19 +1837,19 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - - + + + + + - - + + + - - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1838,17 +1874,17 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - + + - - - - - + + + + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1866,17 +1902,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1897,13 +1933,13 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - + + + - - - + + @@ -1930,7 +1966,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1965,12 +2001,12 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - + @@ -1996,17 +2032,17 @@ _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] @@ -2033,12 +2069,12 @@ _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] ``` @@ -2064,7 +2100,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2076,7 +2112,7 @@ $ ./build/ripwire . --naming-calibration

-

+

... [14 more line(s); run it to see the whole thing] ``` @@ -2099,16 +2135,16 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - + + + + - + - + - + @@ -2136,8 +2172,8 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - + + @@ -2145,10 +2181,10 @@ $ ./build/ripwire . --lint --naming-locals - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2171,7 +2207,7 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + @@ -2204,16 +2240,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - + + - - + - + + - - + ... [22 more line(s); run it to see the whole thing] ``` @@ -2236,16 +2272,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] ``` @@ -2262,7 +2298,7 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + @@ -2270,7 +2306,7 @@ $ ./build/ripwire . --cochange --cochange-groups - + @@ -2291,19 +2327,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] ``` @@ -2329,7 +2365,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2359,7 +2395,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2385,7 +2421,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2412,8 +2448,8 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - - + + @@ -2421,10 +2457,10 @@ $ ./build/ripwire . --lint - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2521,8 +2557,8 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2530,10 +2566,10 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2552,7 +2588,7 @@ $ ./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=4 -goto done; +goto done; goto epilogue; goto epilogue; goto epilogue; @@ -2635,19 +2671,19 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + + - - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2666,7 +2702,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2691,19 +2727,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] ``` @@ -2728,16 +2764,16 @@ $ ./build/ripwire . --report # ripwire architecture report -3902 files · 33122 symbols · 31168 edges · 3871 modules (15563 call-graph isolated) +4056 files · 33884 symbols · 31822 edges · 3979 modules (15964 call-graph isolated) Root: `.` -Call-graph isolate provenance: 4125 declaration, 2508 header, 3690 source, 5240 document; 0 connected Louvain singletons +Call-graph isolate provenance: 4203 declaration, 2539 header, 3947 source, 5275 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3871) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1341 symbols -- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 555 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 269 symbols +## Modules (call-graph clusters; showing 12 of 3979) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1373 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 556 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 259 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2754,8 +2790,8 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - + @@ -2785,17 +2821,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"] - n102["src
9790"] + n102["src
10011"] n103["src/infra
1301"] end subgraph sg1 ["test"] - n104["test
5590"] - n602["test/legochargefix
546"] - n415["test/fixtures/recallpassage
314"] - n1029["test/slotjoinfix
175"] - n839["test/ownerhopfix/py/pkg
158"] - n352["test/expandmodefix
151"] - n354["test/expandsibsfix
149"] + n104["test
5600"] + n642["test/legochargefix
546"] + n420["test/fixtures/recallpassage
314"] + n1069["test/slotjoinfix
175"] + n879["test/ownerhopfix/py/pkg
158"] + n357["test/expandmodefix
151"] + n359["test/expandsibsfix
149"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2814,8 +2850,8 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - + + @@ -2845,7 +2881,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2910,7 +2946,7 @@ _The same clean verdict with what it folded printed back: every stale acknowledg ``` $ ./build/ripwire . --quality-delta --show-stale - @@ -2939,7 +2975,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2964,7 +3000,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2989,7 +3025,7 @@ _A ref range run for two of the twelve kinds only: the named kinds' rows are exa ``` $ ./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code -

--callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +**wall time: 9.89s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +**wall time: 1.74s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8267 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +**wall time: 1.79s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [66 more display lines; full output is 8487 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* + +**wall time: 4.14s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +**wall time: 4.39s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8503 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: 4.30s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8503 bytes on 1 raw line(s)] +````` + +Artifact written: + +````` + 30040005 /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).* + +**wall time: 1.81s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [19 more display lines; full output is 4470 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: 4.34s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 17.12s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 1.2% 1.3% 9.9% + BM25 13.4% 14.6% 16.1% + BM25sub 9.3% 14.3% 21.6% + BM25body 5.2% 14.2% 20.9% + fused 2.3% 10.3% 18.0% + anchored 4.3% 14.2% 20.9% + same-dir 2.8% 4.7% 10.1% + random 0.1% 0.2% 0.5% <- floor (random ranking over F=4056 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: 171.44s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=8654 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.674 53.7% 83.7% 89.2% + subtoken doc-phrase 0.877 85.5% 89.7% 91.0% + name-exact name 0.886 80.2% 95.0% 97.4% + name-exact doc-phrase 0.011 0.5% 1.6% 2.2% + anchored name 0.678 55.0% 83.0% 87.7% + anchored doc-phrase 0.873 84.9% 89.6% 90.3% + routed name 0.887 80.4% 95.1% 97.5% + routed doc-phrase 0.875 85.5% 89.4% 90.6% + note: routing chose name-exact on 3989/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: 54.50s** + +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.427) + for-routed 33.3% 33.3% 0.556 1.000 50.0% (th=0.663) + random 6.2% 12.5% 0.211 0.500 <- floor (uniform-random ranking; auc 0.5 by definition) + provenance hit@1 (bm25-desc): router 0/0, desc 0/0, judged 2/3 (desc rows quote the descriptions - expect them easiest; judged is the honest number) + judged-only hit@1 per arm: overlap 2/3, name 1/3, bm25-desc 2/3, bm25-full 1/3, for-routed 1/3 + router-magnet: with ripwire-router ADMITTED as a candidate it takes top-1 on 1/3 positive prompts (bm25-desc arm) - why it is excluded above + per-skill (bm25-desc): name / permitted-rows / won / pos-fires / false-fires / neg-fires + ripwire-before-you-build 0 0 1 1 0 + ripwire-change-check 1 0 0 0 0 + ripwire-find-bug 0 0 0 0 0 + ripwire-fresh-eyes 0 0 0 0 1 + ripwire-graph-query 0 0 0 0 0 + ripwire-handoff 0 0 0 0 0 + ripwire-layers 0 0 0 0 0 + ripwire-mcp 0 0 0 0 0 + ripwire-navigate 1 1 1 0 0 + ripwire-opt-remarks 0 0 0 0 0 + ripwire-orient 1 1 1 0 0 + ripwire-perf-target 0 0 0 0 0 + ripwire-quality-bar 0 0 0 0 0 + ripwire-reuse-first 0 0 0 0 0 + ripwire-security-scan 0 0 0 0 0 + ripwire-write-tests 0 0 0 0 0 + misses (overlap): + line 3 want=ripwire-change-check got=ripwire-before-you-build "plan parallel worktrees so the lanes do not collide" +… [19 more lines, 3665 bytes total] +````` + +## `./build/ripwire wrap claude` + +*Print the recipe to wire ripwire into Claude Code as an MCP server.* + +````` +# ripwire -> Claude Code (CLI-first) +# RECOMMENDED — this agent can run shell commands, so call the CLI directly. It costs +# nothing until you invoke it, and it reads CLAUDE.md, so the paste block below IS the wiring: +ripwire . --for="" +# +# ...every follow-up call answers with the compact legend by default (terse definitions of only the +# attributes the answer carries; the payload is byte-identical either way). Add --legend=full when a +# definition's reasoning is needed -- `ripwire --help` carries the measured saving (one place, +# gate-held) -- this line does not repeat it, because two copies of a number is one that goes stale: +# ripwire . --callers=SYM --legend=full +# +# ALTERNATIVE — register the MCP server instead, for a warm index across calls: +claude mcp add ripwire -- ripwire --mcp +# verbs the agent can then call mid-task (33 total): +# read: analyze, rank_by, find_symbol, find_referencing_symbols, grep, cochange, memory_recall, situational_awareness, mentions, for, lego, owners, fetch_body, batch, flags, doc_drift, slice +# flagship reflex: exemplar, quality_delta, quality_baseline, impact, uses, affected, path_between, connect, explore, from_trace, edit_check, whereis, stray_content +# edit: replace_symbol_body, insert_before_symbol, insert_after_symbol +bash skills/install.sh # deploy to ${CLAUDE_CONFIG_DIR:-~/.claude}/skills (drift-gated) +bash skills/install.sh --hook # RECOMMENDED: advisory Read/Grep -> ripwire CLI nudge + session primer (opt-in, never blocks) +# +# context wiring — a binary on PATH is invisible to an agent until its rules file says when +# to reach for it. Paste the block below into CLAUDE.md: +# --- paste into CLAUDE.md --- +## ripwire — deterministic codebase maps (on PATH as `ripwire`) +Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after that warm, ~0.1s. +- Orient on a task: `ripwire --for=""` — ranked, quality-annotated + signatures. Paste symbol/file names from the issue verbatim; named mentions get anchored. +- One task: `--pack-task="" --legend=compact`; before parallel agents: `--plan-lanes=N --task=""`, then read `lanes[].execution`. +- Have a stack trace / build error: `ripwire --from-trace=FILE --legend=compact` (`-` = stdin) — + paste the error, don't paraphrase it into a query. +… [14 more lines, 4058 bytes total] +````` + +## `./build/ripwire --version` + +*Version + short build info.* + +````` +ripwire 0.6.8 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=c8b49812b) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:11993` + +*Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* + +**wall time: 1.35s** + +````` + + + + +````` + +## `./build/ripwire . --callers=@src/graph.h:11993` + +*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* + +**wall time: 1.41s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --at=src/graph.h:999999` + +*A seed past the end of the file — the refusal shape for a faulted location.* + +**exit code: 1** — **wall time: 1.16s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 17132 lines — the seed asked for line 999999) +````` + +## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` + +*VERIFY a closed claim in one call: three-valued verdict (confirmed / refuted / not-established) with the evidence rows inline.* + +**wall time: 1.50s** + +````` + + + + + +````` + +## `./build/ripwire . --verify="unused(rankGraphTeleport)"` + +*A claim that is FALSE — the refuted shape, with the references that refute it.* + +**wall time: 1.47s** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --verify="contains(src/graph.h, \"no such literal anywhere\")"` + +*A literal-scan absence: refuted only with complete= evidence, never on a partial scan.* + +**wall time: 1.43s** + +````` + + + +````` + +## `./build/ripwire . --verify="frobnicate(x)"` + +*An unparseable claim — the refusal names the accepted shapes.* + +**exit code: 1** — **wall time: 1.36s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. The claim language is CLOSED; the shapes are: calls(A, B) does A transitively call B · uses(SYM) is SYM referenced anywhere · unused(SYM) is SYM referenced nowhere · contains(FILE, "LITERAL") do FILE's bytes contain t … [line truncated: 218 more bytes on this line] +````` + +## `./build/ripwire . --slice=rankGraphTeleport` + +*Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* + +**wall time: 2.89s** + +````` + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport` + +*Intra-procedural def-use slice of ONE variable: one row per line touching it, k=def|use|both, reaching definitions flow-sensitive (reach=cfg).* + +**wall time: 3.63s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=back --slice-depth=3` + +*TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* + +**wall time: 4.41s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + pw = biasPrior( g, p );]]> + + +& p, float alpha = 0.85f )]]> + + +& p, float alpha = 0.85f )]]> + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd` + +*Forward flow: which statements the seed's value reaches, at the default depth bound.* + +**wall time: 4.72s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + + + + + + + + + + + 0.0 )]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:nosuchvar` + +*A variable the definition does not bind — the refusal shape, naming the inventory.* + +**exit code: 1** — **wall time: 2.29s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice: no occurrence of 'nosuchvar' in rankGraphTeleport — sliceable locals: alpha, g, p, pw, N, teleport, rankDouble, run, teleportMass, value, inverseMass, r (bare --slice=rankGraphTeleport lists them with first-def lines) +````` + +## `./build/ripwire . --slice-depth=3` + +*--slice-depth without --slice-flow is refused loudly rather than silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire --slice=parseArgs:argIndex --slice-flow=fwd --slice-depth=4) +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --legend=compact` + +*The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* + +**wall time: 4.47s** + +````` + + + + + + + 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: 4.12s** + +````` + +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: 5.50s** + +````` + +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 17776 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** — **wall time: 1.71s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --pattern: this pattern collapses to a single bare token, which is a TEXT search, not a structural one — a clean parse is not enough (a bare word parses fine in most grammars and means nothing structural). Give it a shape (foo($X), $A + $B, if ($C) { ... }) or use the grep flag for litera … [line truncated: 176 more bytes on this line] +````` + +## `./build/ripwire . --grep=DISCLOSE --and=cache` + +*Boolean grep: hits where BOTH literals share the matched line (--grep-scope=line is the default).* + +**wall time: 3.51s** + +````` + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 +inline bool hasPytestTable( std::string_view source ) +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [139 more display lines; full output is 19724 bytes on 44 raw line(s)] +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose` + +*Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling).* + +**wall time: 7.87s** + +````` + + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 +inline bool hasPytestTable( std::string_view source ) +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [146 more display lines; full output is 19775 bytes on 44 raw line(s)] +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies` + +*Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* + +**wall time: 8.44s** + +````` + + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 +inline bool hasPytestTable( std::string_view source ) +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [137 more display lines; full output is 20499 bytes on 57 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp"` + +*Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* + +**wall time: 9.41s** + +````` + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +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: 5 more bytes on this line] +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::stri… +inline const char* kNotesFile = ".ripwire_notes" +inline void writePlanLint( std::FILE* out, const LintResult& res ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOf … [line truncated: 16 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… … [line truncated: 4 more bytes on this line] +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root, bool showStale = false ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +inline std::string qualityAcksPath( const std::string& root ) +struct QualityDeltaOutcome +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string gitHeadSha( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +… [193 more display lines; full output is 23058 bytes on 45 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention` + +*The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* + +**wall time: 5.69s** + +````` + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +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: 5 more bytes on this line] +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::stri… +inline const char* kNotesFile = ".ripwire_notes" +inline void writePlanLint( std::FILE* out, const LintResult& res ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOf … [line truncated: 16 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… … [line truncated: 4 more bytes on this line] +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root, bool showStale = false ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +inline std::string qualityAcksPath( const std::string& root ) +struct QualityDeltaOutcome +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string gitHeadSha( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +… [193 more display lines; full output is 22880 bytes on 45 raw line(s)] +````` + +## `./build/ripwire . --safe-delete=rankGraphTeleport` + +*"Can I delete this?" — callers + transitive impact + every use site + how much of the radius is tested, composed in ONE call; risk= names what was found, never a verdict.* + +**wall time: 2.02s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --safe-delete=DoesNotExist` + +*Unknown-symbol refusal shape for --safe-delete.* + +**exit code: 1** — **wall time: 3.08s** + +````` +(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: 15.08s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 14.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [459 more display lines; full output is 49456 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --no-ignore --top-k=3` + +*Crawl paths the repo's own .gitignore covers (default honours it and discloses ignored_files=/ignored_dirs= only when it dropped anything — this repo's crawl drops nothing, so the header is identical to the default map's; --skipped's ignore_mode= says which rule applied).* + +**wall time: 2.20s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8503 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.* + +**wall time: 2.80s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8503 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** — **wall time: 2.42s** + +````` + + + + + + const&, float) src/graph.h:11993 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:12034 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1755 + #3 0x102e01a44 in main src/main.cpp:4177 + #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_DESC … [line truncated: 289 more bytes on this line] +````` + +## `./build/ripwire . --run-trace="true"` + +*A command that exits 0: a minimal success record (exit, measured duration, disclosed output tail) and NO bundle — nothing failed, nothing to map.* + +**wall time: 3.44s** + +````` + + + + +````` + +## `./build/ripwire . --run-trace="sleep 30" --run-timeout=2` + +*A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* + +**exit code: 4** — **wall time: 3.76s** + +````` + + + + +````` + +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: 15.53s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [188 more display lines; full output is 15661 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --biggest-first --limit=8` + +*Per-function size ranking, largest Halstead volume/token entropy/lines first — a size proxy, not a readability order (withdrawn, docs/EVALS.md §8) — a RANKING lens, not a grade.* + +**wall time: 3.32s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --comment-coherence --limit=8` + +*Functions WITH a doc comment, most name-restating first: c_coeff (high = the comment repeats the name) and cic (Jaccard of comment vs identifier vocabulary), both reported, never collapsed.* + +**wall time: 2.56s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --context-ratio --limit=8` + +*The local-reasoning lens: to understand this symbol, how much must you know that is NOT in front of you (ent_ratio= edge share, read_ratio= token-weighted).* + +**wall time: 3.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 11168 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: 7.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 7601 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --ensemble --limit=8` + +*The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* + +**wall time: 10.83s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 7464 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: 5.49s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [514 more display lines; full output is 46163 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity=Symbol` + +*The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* + +**wall time: 3.84s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --naming-consistency --limit=8` + +*The corpus's OWN case-convention vote per (language, kind) group; off-convention names get a mechanical propose= (a suggestion, never a blind rename).* + +**wall time: 2.52s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 4431 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: 23.88s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [171 more display lines; full output is 17431 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: 5.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [639 more display lines; full output is 72719 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-catalog` + +*The built-in rule registry — one row per rule with sev=/category=/rationale/lang=/since=; no corpus needed.* + +**wall time: 1.54s** + +````` + + +a C-style cast — cppcoreguidelines-pro-type-cstyle-cast prefers the explicit static_cast/const_cast/reinterpret_cast +a goto statement — cppcoreguidelines-avoid-goto +a do/while loop shape (on Swift, its do/catch block shares the same grammar node and also matches) +a call to an unbounded C string function (strcpy/strcat/sprintf/gets) +a call to a broken hash or cipher (MD5/SHA1/MD4/RC4) +a doubly-parenthesized expression — readability-redundant-parentheses +an if-body that is just `;` — bugprone-suspicious-semicolon +a C-style typedef struct/union where `using` is preferred +a non-trivial numeric literal inside a function body, outside a const/constexpr init +a catch block with an empty body +x = x — almost always a copy-paste bug +a function body over 80 lines +brace nesting depth over 4 inside a function body +a bare `return;` mixed with a value-returning return in the same function +a statement after an unconditional return/break/continue/throw/raise in the same block +a 1-2 letter Function/Method/Var name — visible far beyond any tiny scope [Beniamini/Hofmeister] +more than 5 split tokens in one name [Butler; AlSuhaibani] +foo1/foo2/... digit-suffix siblings sharing a base name in one scope [Butler] +internal consecutive underscores, or a C-family reserved __x/_X form [Butler] +snake_case and camelCase mixed inside one name [Butler] +an is/has/can/should/was-prefixed name whose KNOWN return type is not bool-like [LAPD A2] +a set-prefixed name whose KNOWN return type is not void-like [LAPD A3] +a co-visible pair within edit distance <=2, reordered tokens, or a bare/digit-suffixed twin [Namesake] +every split subtoken is corpus-ubiquitous (BM25 idf) on a body past the size floor — fires only at the low end [Sparck Jones 1972] +the comma operator inside an expression (never a for-header comma) [Gopstein FSE 2017] +++/-- evaluated inside a larger expression (never a whole statement) [Gopstein FSE 2017] +an assignment whose VALUE is consumed (a condition, an argument, ...) [Gopstein FSE 2017] +a conditional expression inside a conditional expression [Gopstein FSE 2017] +… [12 more display lines; full output is 7800 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --legend-dict` + +*The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* + +````` +ripwire legend dictionary ripwire.dict/v1 dictv=05b48ed93c807ee2 entries=981 +: 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 +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.defs/v1 : per the parsed definitions the name binds (defs= groups; a prototype with its definition is one): , sel= binds that one alone in any SYM verb, in= distinct callers (a floor); source first, then in=, then path +ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none +… [952 more display lines; full output is 105532 bytes on 982 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: 5.43s** + +````` + + + + + + + + + + +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] ] +… [607 more display lines; full output is 73635 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: 4.34s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [720 more display lines; full output is 66458 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: 5.28s** + +````` + + + +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: 5.04s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto error; +goto error; +goto error; +goto done; +goto done; + +````` + +## `./build/ripwire . --lint --lint-select=cach-` + +*An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* + +**exit code: 1** — **wall time: 6.06s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-'?) — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --lint-select=nosuchfamily` + +*A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* + +**exit code: 1** — **wall time: 4.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: 5.10s** + +````` +{"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}}, +… [13933 more display lines; full output is 1976705 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 6962 +````` + +## `./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: 12.08s** + +````` + + +

+

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

+

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

+

+ +````` + +## `./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: 9.52s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [845 more display lines; full output is 56219 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: 16.61s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 13.26s** + +````` +(empty) +````` + +Artifact written: + +````` + 156091 /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: 13.68s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (30040005 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (61456848 bytes, rich family) +````` + +Artifact written: + +````` + 30040005 /aux/ci_index.lean.ripwirecache + 61456848 /aux/ci_index.rich.ripwirecache + 91496853 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.* + +**wall time: 3.59s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8503 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.* + +**wall time: 3.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8503 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. + 135034 /aux/pin_census.tsv +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` + +*The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when a card or ledger row gates.* + +**exit code: 2** — **wall time: 5.26s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave_ledger.md` + +*The ledger-shaped fixture through the same check (exit 2: one gating card).* + +**exit code: 2** — **wall time: 6.43s** + +````` + + + + + + + +````` + +## `./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: 36.86s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 15.74s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: c8b49812b+dirty + change_set: granularity=file changed_symbols=13 file_symbols=13 files_narrowed=0 files_file_granular=1 diff_hunks=7 diff_hunks_located=6 diff_hunks_unlocated=1 — seeds = symbols a diff -U0-vs-HEAD hunk overlaps (function/method body spans only); an unlocated hunk (module-level, deletion, rename) … [line truncated: 34 more bytes on this line] + [1] blast radius: 255 symbols across 60 files transitively depend on these changes (showing 8 of 60 files — shown=8 total=60 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=60) + counts_floor=1 graph_ambiguous=3015 graph_unresolved=15989 graph_unindexed=275 (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 (33 dependent symbols) + src/quality.h (15 dependent symbols) + src/verbs_for.h (15 dependent symbols) + src/verbs_quality.h (14 dependent symbols) + src/verbs_navigate.h (11 dependent symbols) + src/editcheck.h (9 dependent symbols) + src/resolve.h (9 dependent symbols) + src/graph.h (8 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=797 — 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="5607": + (none — 5607 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: 9.26s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 51.03s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [151 more display lines; full output is 18268 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: 28.78s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [151 more display lines; full output is 23260 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: 29.41s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":169,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"c8b49812b+dirty","renames":62,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 240 more bytes on this line] +{"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"duplication","members":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","tokens":59,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"nesting","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":6,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"new-clone-of-reused-helper","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","was":0,"now":4,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"params","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","was":0,"now":8,"p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"verbosity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":8,"now":47,"p":"src/infra/sortutil.h:49","sev":"minor"}], +"sa":[{"kind":"api-surface","key":"085e3d408c2c4a35","why":"target-gone"}, +{"kind":"api-surface","key":"155d74d341a49f3c","why":"target-gone"}, +{"kind":"api-surface","key":"1a15386c2d1e47af","why":"target-gone"}, +{"kind":"api-surface","key":"298e798c7f075715","why":"target-gone"}, +{"kind":"api-surface","key":"56acdf9b5c314a14","why":"target-gone"}, +{"kind":"api-surface","key":"5a07390012b46e06","why":"target-gone"}, +{"kind":"api-surface","key":"6eef859578c8c376","why":"target-gone"}, +{"kind":"api-surface","key":"6f2394762f82a855","why":"target-gone"}, +{"kind":"api-surface","key":"7f2c3eefdf6e512e","why":"target-gone"}, +{"kind":"api-surface","key":"802e513731103806","why":"target-gone"}, +{"kind":"api-surface","key":"b6a24afef32a68a8","why":"target-gone"}, +{"kind":"api-surface","key":"c923e661c197b265","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1926b0d9e94541a0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1d3814ba687a4aef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1db76028879244ef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"25c9f0504fd376a4","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"36e39c7ab0fc1686","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"4bce64bd920de0c3","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"61f0e361ef7dee27","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, +… [148 more display lines; full output is 16876 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: 15.67s** + +````` +(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, 213 re-derived, 21 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 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: 17.99s** + +````` +(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, 213 re-derived, 21 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: 22.46s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [150 more display lines; full output is 17971 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: 8.21s** + +````` + + + + + + +````` + +## `./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: 7.05s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 16.45s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [80 more display lines; full output is 8065 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: 9.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 10.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [308 more display lines; full output is 16263 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: 16.58s** + +````` + + + + + +````` + +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: 21.85s** + +````` + + + + +````` + +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: 20.94s** + +````` + + +

+

+ +````` + +## `./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: 51.83s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [153 more display lines; full output is 18238 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: 34.18s** + +````` +(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: 20.42s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --note-add="lessByScoreDescId: keep this branch-free — it sits inside the PageRank sort comparator"` + +*Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* + +**wall time: 8.34s** + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-28 keep this branch-free — it sits inside the PageRank sort comparator c8b49812b1ae3eaad7b5ac05fcf5cf8903b7780c integration/train-41 +````` + +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.)* + +**wall time: 2.77s** + +````` + + + + + + + + + + + + + + + + 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** — **wall time: 2.16s** + +````` +(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** — **wall time: 2.18s** + +````` +(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.* + +**wall time: 6.61s** + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-28 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort c8b49812b1ae3eaad7b5ac05fcf5cf8903b7780c integration/train-41 +````` + +## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` + +*Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* + +**wall time: 3.56s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 12.67s** + +````` +{"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":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":797,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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: 6.56s** + +````` +{"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: 20.31s** + +````` +{"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":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":797,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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: 7.51s** + +````` +(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: 9.24s** + +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":3015,"graph_unresolved":15989,"graph_unindexed":275,"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: 23.68s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":3015,"graph_unresolved":15989,"graph_unindexed":275,"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** — **wall time: 4.59s** + +````` +(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: 76.98s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [147 more display lines; full output is 17335 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: 37.73s** + +````` +(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: 38.53s** + +````` +(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 32860 per-symbol rows, 33886 indexed symbols total) +````` + +Artifact written: + +````` + 5561300 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer 633cd66b1d7dcb157a0f07192be0a24084a59085643ee69a1f0e4daca636402f +head c8b49812b1ae3eaad7b5ac05fcf5cf8903b7780c +dirty 1 +absorbed 2 +ccx 67c15011ee8b 0 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699b +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 38.19s** + +````` + + +````` + + +--- + +# 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.* + +**wall time: 1.33s** + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 945 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). Hits carry text; enclosing rows a fetch_body handle. pattern = the literal; in=any lifts the span tiering (default: tightest non-empty tier; rest rides as suppressed_co … [line truncated: 837 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional soft row target (the CLI --for --token-budget). The header carries route= (which ranker … [line truncated: 1061 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 841 more bytes on this line] +{"name":"owners","description":"Bus-factor: recency-weighted (6-month half-life) author ownership per file. symbol = optional, restricts to the file that defines it; limit/offset page the rows. at= is the commit these numbers were computed at (+dirty = the working tree differed). An @FILE:LINE line- … [line truncated: 879 more bytes on this line] +{"name":"replace_symbol_body","description":"Replace a symbol's ENTIRE definition (signature through closing brace) with new_body — splices over the full def span, preserving every byte outside it verbatim; new_body must be a complete, well-formed definition. One trailing newline folds (trailing_n … [line truncated: 1323 more bytes on this line] +{"name":"insert_before_symbol","description":"Insert text immediately BEFORE a symbol's definition (its first byte); padded to the file's own blank-line seam (separator_padded=N). Same refusal contract as replace_symbol_body (not found / ambiguous / stale index / symlink / concurrent write, file unc … [line truncated: 877 more bytes on this line] +{"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] +{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] +{"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 862 more bytes on this line] +{"name":"quality_baseline","description":"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor … [line truncated: 445 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"affected","description":"WHICH TESTS TO RUN for a change — test files that transitively reach the changed files/symbols, ranked by evidence (edited > partner-named > hop distance). files = changed files and/or symbols, comma-separated: each item is tried as an indexed PATH pattern first, … [line truncated: 1238 more bytes on this line] +{"name":"path_between","description":"Does A REACH B, and HOW? — the shortest directed CALL path between two symbols, hop-by-hop. reachable=\"0\" hops=\"0\" is a valid 'not reachable' answer — call edges are name-based, so a missing dynamic/callback edge can hide a real path. Named path_between … [line truncated: 709 more bytes on this line] +{"name":"connect","description":"When a task touches 2..16 named symbols, returns the minimal subgraph RELATING them - terminals, the fewest joining intermediaries (with signatures), and call edges in true direction - finding the shared-caller joins a directed path_between cannot; unrelated symbols … [line truncated: 950 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 2135 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1233 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1475 more bytes on this line] +{"name":"stray_content","description":"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the wo … [line truncated: 1123 more bytes on this line] +{"name":"flags","description":"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is bo … [line truncated: 1434 more bytes on this line] +{"name":"doc_drift","description":"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), … [line truncated: 1167 more bytes on this line] +{"name":"slice","description":"WHERE IS THIS VARIABLE DEFINED AND USED inside one function — NAME-BASED intra-procedural def-use rows of one variable inside ONE uniquely-resolved definition (the ARISE slicer, arXiv:2605.03117). symbol alone lists the sliceable locals to pick from; add var (or spel … [line truncated: 1722 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: + +````` +tools= 33 manifest_bytes= 47445 (~tokens at 4 bytes/token: 11861 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +whereis desc_bytes= 701 schema_bytes= 941 required=['symbol'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] +affected desc_bytes= 790 schema_bytes= 610 required=['files'] +from_trace desc_bytes= 836 schema_bytes= 559 required=['trace'] +find_symbol desc_bytes= 752 schema_bytes= 590 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +rank_by desc_bytes= 758 schema_bytes= 481 required=[] +for desc_bytes= 453 schema_bytes= 777 required=['task'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symbol'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +quality_delta desc_bytes= 742 schema_bytes= 279 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: 862 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 2135 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1233 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The core manifest, summarised (name / description bytes / required args): + +````` +tools= 8 manifest_bytes= 14186 (~tokens at 4 bytes/token: 3546 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +from_trace desc_bytes= 836 schema_bytes= 559 required=['trace'] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +quality_delta desc_bytes= 742 schema_bytes= 279 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: 7.60s** + +````` +{"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: 6566 more bytes on this line] +"_index":"[index: files=4056 symbols=33884 hash=e5a05745]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + ptKindLegend + "graph holds none. {}{}{}{}-->{}", ( defAltsLegend( ptAlts ) + unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + ptKindLegend + splitArmLegend( ptHasSplit ) + pathUnboundLegend( !ptBound.from || !ptBound.to ) - + memberOwnerLegend( ptHasOwner ) + memberOwnerContainedLegend( ptMarks.ownerContained ) + std::string( pathPairsLegend( ptPairs ) ) + pathOrderLegend( ptPairs, ptTwin ) ).c_str(), + + memberOwnerLegend( ptHasOwner ) + memberOwnerContainedLegend( ptMarks.ownerContained ) + std::string( pathPairsLegend( ptPairs ) ) + pathOrderLegend( ptPairs, ptTwin ) + + std::string( pathHopBodiesLegend( !ptHopBodies.empty() ) ) + std::string( pathHopNextLegend( !ptHopNext.empty() ) ) + + std::string( routeBodyHedgeLegend( ptHopBodyEl.hedged || ptRouteEl.hedged ) ) ).c_str(), ( std::string( toValueRefsLegend( ptToValueRefs.count > 0 ) ) + valueRefsDepthLegend( ptToValueRefs.depthCut.files > 0 ) ).c_str(), ( pathGapsLegend( ptGaps.any(), !ptSinkCallers.empty() ) + pathBridgeLegend( ptBridge ) - + nearSinkLegend( ptNear ) + std::string( pathGlobLegend( srcGlob || dstGlob ) ) ).c_str(), + + nearSinkLegend( ptNear ) + std::string( pathHandlerRuleLegend( ptHandler ) ) + std::string( pathRouteBodiesLegend( !ptRouteEl.xml.empty() ) ) + + std::string( pathGlobLegend( srcGlob || dstGlob ) ) ).c_str(), graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rootRelPathsLegend( ptSingleRoot ) ); rw::emitTo( mem, "{}{}{}", ptUnreached.rootAttrs, ptUnreached.rows, pathBridgeXml( ing, g, ptBridge, ptSingleRoot, ptRootPrefix ), - pathNearSinkXml( ing, ptNear, ptSingleRoot, ptRootPrefix ) ); + rw::emitTo( mem, "{}>{}{}{}{}", ptUnreached.rootAttrs, ptUnreached.rows, pathBridgeXml( ing, g, ptBridge, ptSingleRoot, ptRootPrefix ), + pathNearSinkXml( ing, ptNear, ptSingleRoot, ptRootPrefix ), + pth.empty() ? pathHandlerRuleXml( ing, ptBackHops->names(), ptHandler, ptSingleRoot, ptRootPrefix ) : std::string() ); emitPathAnswerRows( mem, ing, g, ptPairs, pth, ptOwners, ptSingleRoot, ptRootPrefix ); // the CLI's own row emitter (pathpairs.h / defattrib.h) + rw::emitRaw( mem, ptHopBodies.c_str() ); // PATH-HOP-BODY, as on the CLI + rw::emitRaw( mem, ptRouteEl.xml.c_str() ); // BODY-ON-ROUTE, as on the CLI + rw::emitRaw( mem, ptAlts.xml.c_str() ); // D2 part 5, as on the CLI rw::emitRaw( mem, "" ); return mcpAnswerText( stream ); // nullopt = the buffer lost bytes (dispatch answers -32603), "" stays not-found } diff --git a/src/model.h b/src/model.h index 61a3a7699..0eccaa7d0 100644 --- a/src/model.h +++ b/src/model.h @@ -505,7 +505,7 @@ inline const char* refRoleTag( RefRole r ) noexcept // Reference-as-value round: the grammar family whose value positions src/ingest_valuerefs.h reads, and whose // visibility rule src/valuerefs.h resolves under — ONE table both sides index, so the capture and the resolver // cannot disagree about which languages are armed. None = not armed (no Value/Through rows are captured). -enum class ValueRefFamily : std::uint8_t { None, C, Js, Py, Go }; +enum class ValueRefFamily : std::uint8_t { None, C, Js, Py, Go, Rust }; // Rust: the slot join's records only (ingest_rustslots.h) inline constexpr std::array kValueRefFamilyOfLang = [] { std::array table {}; // value-initialised: every language None unless armed below @@ -515,6 +515,7 @@ inline constexpr std::array kValueRefFamilyOfLang = table[ static_cast( Lang::TypeScript ) ] = ValueRefFamily::Js; table[ static_cast( Lang::Python ) ] = ValueRefFamily::Py; table[ static_cast( Lang::Go ) ] = ValueRefFamily::Go; + table[ static_cast( Lang::Rust ) ] = ValueRefFamily::Rust; return table; }(); inline ValueRefFamily valueRefFamily( Lang l ) noexcept @@ -579,34 +580,74 @@ inline constexpr char kValueRefSlotFactScope = 't'; // that function at that position (`Reg( Put )`) into the same field. Distinct from every other scope char. inline constexpr char kValueRefSlotParamScope = 'q'; -// IX-1 / IX1-V2: which languages record a slot join, and how its CALL side is found — the one table both the extractor -// (ingest_valuerefs.h) and the join (valuerefindex.h) read. byFieldName: a member call `x.F( … )` (an ordinary Call reference -// with memberCall) is a call through field F when some function value is stored in a field F, joined at index time by the -// field name (the receiver's type is not read: the name tier, via="slot-name"); otherwise the extractor records the field -// calls itself (C family: kValueRefSlotCallScope rows, everyFieldCall = every `x->f( … )` in C, the C++ gate otherwise). +// IX-1 / IX1-V2: which languages record a slot join, and how — the one table both the extractor (ingest_valuerefs.h) and the +// join (valuerefindex.h) read. A language's row supplies the per-language pieces of the language-blind join (SlotIndex): +// callSide — how a call THROUGH a field is found: +// ThroughRows: the extractor records the field calls itself (C family: kValueRefSlotCallScope rows; everyFieldCall = every +// `x->f( … )` in C, the C++ gate otherwise); +// MemberCallByName: an ordinary member call `x.F( … )` (a Call reference with memberCall) is a call through field F when some +// function value is stored in a field F, joined at index time by the field name (the receiver's type is not read: the name +// tier, via="slot-name"); the extractor records only the STORE side (Go); +// ParenFieldCall: `(x.f)( … )`, the only spelling that calls a function-valued field (`x.f( … )` is always a method call), recorded +// by the extractor as a kValueRefSlotCallScope row, typed when x is `self` (Rust: ingest_rustslots.h). +// typeKey — how a slot's declared struct type is keyed (both sides and --for's struct rows key the same way): +// TypedefFacts: the type as written, through the typedef / field-type slot facts (C family); +// PackageDir: the package DIRECTORY the type is declared in, plus its name (Go: valuerefindex.h slot_join::GoTypeKeys); +// ModuleType: the one in-tree struct the name spells — its own file's, else its crate's, else the repo's only one (Rust: +// slot_join::RustTypeKeys; `Self` is resolved to the impl type by the extractor). +// pathSep — a registrar may be an associated function reached by a PATH call (`CallEval::new( … )`): the argument row's callee +// is written `Ownername`, resolved to the owner type's function of that name ("" = bare callees only, never a method). +enum class VrSlotCallSide : std::uint8_t { ThroughRows, MemberCallByName, ParenFieldCall }; +enum class VrSlotTypeKey : std::uint8_t { TypedefFacts, PackageDir, ModuleType }; struct VrSlotLang { - Lang lang; - bool everyFieldCall; - bool byFieldName; + Lang lang; + bool everyFieldCall; + VrSlotCallSide callSide; + VrSlotTypeKey typeKey; + std::string_view pathSep; }; inline constexpr VrSlotLang kVrSlotLangs[] = { - { Lang::C, true, false }, - { Lang::Cpp, false, false }, - { Lang::Go, false, true }, + { Lang::C, true, VrSlotCallSide::ThroughRows, VrSlotTypeKey::TypedefFacts, "" }, + { Lang::Cpp, false, VrSlotCallSide::ThroughRows, VrSlotTypeKey::TypedefFacts, "" }, + { Lang::Go, false, VrSlotCallSide::MemberCallByName, VrSlotTypeKey::PackageDir, "" }, + { Lang::Rust, false, VrSlotCallSide::ParenFieldCall, VrSlotTypeKey::ModuleType, "::" }, }; -inline constexpr std::array kSlotByFieldNameOfLang = [] +inline constexpr std::array kVrSlotLangOf = [] { - std::array byLang {}; + std::array byLang {}; for( const VrSlotLang& row : kVrSlotLangs ) { - byLang[ static_cast( row.lang ) ] = row.byFieldName; + byLang[ static_cast( row.lang ) ] = &row; } return byLang; }(); +// The slot row of language `l`, nullptr when it records no slot join. +inline const VrSlotLang* vrSlotLang( Lang l ) noexcept +{ + return enumTableAt( kVrSlotLangOf, l, static_cast( nullptr ) ); +} inline bool slotJoinsByFieldName( Lang l ) noexcept { - return enumTableAt( kSlotByFieldNameOfLang, l, false ); + const VrSlotLang* row = vrSlotLang( l ); + return row != nullptr && row->callSide == VrSlotCallSide::MemberCallByName; +} +// How language `l` keys a slot type (TypedefFacts for a language with no slot row: it keys nothing of its own). +inline VrSlotTypeKey slotTypeKeyOf( Lang l ) noexcept +{ + const VrSlotLang* row = vrSlotLang( l ); + return row == nullptr ? VrSlotTypeKey::TypedefFacts : row->typeKey; +} +// The path separator of language `l`'s registrar callees ("" = bare callees only). +inline std::string_view slotPathSep( Lang l ) noexcept +{ + const VrSlotLang* row = vrSlotLang( l ); + return row == nullptr ? std::string_view() : row->pathSep; +} +// A language whose slot types key by its own rule (valuerefindex.h slot_join), not through the C family's typedef facts. +inline bool slotKeysOwnTypes( Lang l ) noexcept +{ + return slotTypeKeyOf( l ) != VrSlotTypeKey::TypedefFacts; } // A slot type key written with the `struct` / `union` keyword (`struct thandle`) leads with this mark: a C tag is no typedef // name, so the join never reads it through an alias. A slot fact whose composeRel is kValueRefSlotBodyRel declares that its @@ -1556,6 +1597,12 @@ struct RubyMainMixinRow std::uint32_t line = 0; // the constant's 1-based line }; +// PY-ALIAS-SPLIT-MARKER: a Python def or class statement at MODULE scope that sits inside a compound statement (`if`, `try`, +// `with`, `for`, `while`, `match`: it may not run) files, besides its ordinary VarDecl, a second VarDecl spelled with this prefix +// (`?f`). No identifier starts with it, so no name lookup ever matches the record; graph.h pythonRenamedImportPin reads it as "f may +// be unbound", the case where a module `__getattr__` (PEP 562, consulted only for names the module lacks) answers instead. +inline constexpr std::string_view kPythonCondDefPrefix = "?"; + struct Binding { NodeId fromSymbol = kNoNode; // enclosing function/method (the binding's scope); kNoNode if file-scope diff --git a/src/pathgaps.h b/src/pathgaps.h index 946d50e0c..01da9b3ed 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -38,6 +38,7 @@ #include "filter.h" // pathTierIndexOver — the near-sink rows' tier key within a depth #include "arch.h" // wildcardMatch — the ONE `*` match, which a glob endpoint reads #include "closures.h" // ClosureIndex — a near-sink row written inside another function's body (why=closure) +#include "hopbody.h" // BODY-ON-ROUTE: RouteBody, routeBodiesXml — the one edge-kind-aware body printer #include "backhops.h" // HOP1: BackwardHops (slot + declined hops of a sink), DeclinedCrossRule, builtinSpelledCall #include "infra/sortutil.h" // svLess: the explicit byte order every string_view sort here takes (portablebuildcheck #6) #include "infra/enumcount.h" // enumCountIsExact — the kNearSinkKindCount / kBridgeViaCount sizes follow their enums @@ -45,6 +46,7 @@ #include "infra/namesplit.h" // afterLast — a bound scope's final segment, the base index key (K5 rule 2c) #include +#include #include #include #include @@ -1045,7 +1047,8 @@ inline constexpr std::string_view kNearSinkLegend = inline constexpr std::string_view kNearSinkNameKeyLegend = "ns via=name-key cands=K: the declined site is a NAME-KEYED lookup, not a call: in= looks a method up by a name built from a template " "(getattr(o, 'do_' + c), o['on' + e]) that fits this row, a template that can name K definitions including it; matched, never traced; " - "the bridge crosses a lookup of at most 16, so a wider one is a way in with no bridge row. "; + "the bridge crosses a lookup of at most 16, so a wider one is a way in with no bridge row. Or a posted message: in= passes a construction of a " + "message class whose handler name (the repo's own __init_subclass__ rule) is this row's name, at site=; K the methods of that name. "; inline std::string nearSinkLegend( const PathNearSink& ns ) { const std::size_t shown = std::min( ns.rows.size(), kNearSinkRows ); @@ -1535,4 +1538,326 @@ inline std::string pathBridgeXml( const IngestResult& ing, const Graph& g, const return out + ""; } +// ── ON-NAME-HANDLERS-V2: the message-handler rule on a reachable=0 --path ───────────────────────────────────────────────── +// When from= is a poster (handlerrule.h: a method of a class that reads the rule attribute) called at post sites whose +// message class names a to= handler, the answer says HOW the message gets there by name: the rule (where the repo writes the +// handler name and its spelling), the readers that pass the name on (the dispatch side, by name on the read's own line), +// and the posted classes with the width of each handler name. All of it is a HEDGE: matched by name, never traced, never in +// reachable=/hops=; a handler name with more than kHandlerRuleListMax definitions is a count with next=, never a cut list. +inline constexpr std::size_t kHandlerRuleShown = 3; // rows: the widest posted classes first +inline constexpr std::size_t kHandlerRuleReaders = 4; // rows +inline constexpr std::size_t kHandlerRuleListMax = 16; // the bridge's own width limit: at most this many defs are listed as + +struct PathHandlerRow +{ + NodeId cls = kNoNode; + std::uint32_t site = 0; // the first post site of that class + std::uint32_t posts = 0; + std::uint32_t cands = 0; + std::string handler; +}; +struct PathHandlerRule +{ + const HandlerRule* rule = nullptr; + std::uint32_t posts = 0; + std::uint32_t defs = 0; + std::vector rows; // every posted class, widest first + std::vector listed; // the handler defs when defs <= kHandlerRuleListMax + bool found() const noexcept { return rule != nullptr && !rows.empty(); } +}; + +inline PathHandlerRule pathHandlerRule( const IngestResult& ing, const NameKeyHops& names, std::span srcDefs, std::span dstDefs ) +{ + PathHandlerRule out; + const HandlerRules& hr = names.rules(); + if( !hr.any() ) + { + return out; + } + std::vector fromNames; + for( const NodeId s : srcDefs ) + { + fromNames.push_back( ing.symbols[s].name ); + } + std::vector toNames; + for( const NodeId d : dstDefs ) + { + toNames.push_back( ing.symbols[d].name ); + } + std::ranges::sort( toNames, sortutil::svLess ); + toNames.erase( std::ranges::unique( toNames ).begin(), toNames.end() ); + std::vector hit; // the handler names a counted post site carries that to= names + for( const HandlerPostSite& site : hr.sites() ) + { + const Reference& r = ing.references[ site.ref ]; + if( !std::ranges::contains( fromNames, std::string_view( r.calleeName ) ) ) + { + continue; + } + bool counted = false; + for( std::size_t k = 0; k < site.classes.size(); ++k ) + { + const std::string h = hr.handlerNameOf( site.classes[k] ); + if( !std::ranges::binary_search( toNames, std::string_view( h ), sortutil::svLess ) ) + { + continue; + } + counted = true; + hit.push_back( h ); + const auto row = std::ranges::find( out.rows, site.classes[k], &PathHandlerRow::cls ); + if( row != out.rows.end() ) + { + ++row->posts; + continue; + } + const std::vector t = hr.targets( site.ref ); + out.rows.push_back( PathHandlerRow{ site.classes[k], site.ref, 1, std::uint32_t( std::ranges::count_if( t, [ & ]( NodeId d ) { return ing.symbols[d].name == h; } ) ), h } ); + } + out.posts += counted ? 1 : 0; + } + if( out.rows.empty() ) + { + return out; + } + std::ranges::sort( hit, sortutil::svLess ); + hit.erase( std::ranges::unique( hit ).begin(), hit.end() ); + for( const NodeId d : dstDefs ) + { + if( std::ranges::binary_search( hit, std::string_view( ing.symbols[d].name ), sortutil::svLess ) && hr.classes().ownerOf( d ) != kNoNode ) + { + ++out.defs; + out.listed.push_back( d ); + } + } + if( out.defs > kHandlerRuleListMax ) + { + out.listed.clear(); + } + std::ranges::stable_sort( out.rows, []( const PathHandlerRow& a, const PathHandlerRow& b ) { return a.cands > b.cands; } ); + out.rule = hr.ruleOf( out.rows.front().cls ); // the ONE rule pick (handlerrule.h): the nearest rule base + return out; +} + +// Owner.name for a definition inside a class, else the name. +inline std::string handlerRuleQualified( const IngestResult& ing, const HandlerRules& hr, NodeId d ) +{ + const NodeId owner = hr.classes().ownerOf( d ); + return owner != kNoNode ? ing.symbols[ owner ].name + "." + ing.symbols[d].name : ing.symbols[d].name; +} + +// The line `n` of `text` (1-based), or empty. +inline std::string_view handlerRuleLine( std::string_view text, std::uint32_t n ) +{ + std::size_t at = 0; + for( std::uint32_t k = 1; k < n && at != std::string_view::npos; ++k ) + { + at = text.find( '\n', at ); + at = at == std::string_view::npos ? at : at + 1; + } + if( at == std::string_view::npos || at >= text.size() ) + { + return {}; + } + return text.substr( at, std::min( text.find( '\n', at ), text.size() ) - at ); +} +// Does `line` pass word `w` as a call argument (`(w` or `, w` before a `,` or `)`)? +inline bool handlerRulePassesWord( std::string_view line, std::string_view w ) +{ + for( std::size_t at = line.find( w ); at != std::string_view::npos && !w.empty(); at = line.find( w, at + 1 ) ) + { + std::size_t b = at; + while( b > 0 && line[ b - 1 ] == ' ' ) + { + --b; + } + std::size_t e = at + w.size(); + while( e < line.size() && line[e] == ' ' ) + { + ++e; + } + if( b > 0 && ( line[ b - 1 ] == '(' || line[ b - 1 ] == ',' ) && e < line.size() && ( line[e] == ',' || line[e] == ')' ) ) + { + return true; + } + } + return false; +} +// The definition a reader passes the name to: a call of a method of the reader's own class (same file) on the line of its +// read, or on a later line that passes the local the read was assigned to (`n = m.attr` … `self.f( n, m )`). +inline NodeId handlerRulePassee( const IngestResult& ing, const HandlerRules& hr, const HandlerReader& reader ) +{ + const Symbol& fn = ing.symbols[ reader.fn ]; + const NodeId owner = hr.classes().ownerOf( reader.fn ); + const std::string_view text( hr.text( fn.fileId ) ); + const std::string_view read = handlerRuleLine( text, reader.line ); + std::string_view local; + if( const std::size_t eq = read.find( '=' ); eq != std::string_view::npos && read.find( "==" ) == std::string_view::npos ) + { + local = read.substr( 0, eq ); + while( !local.empty() && ( local.front() == ' ' || local.front() == '\t' ) ) + { + local.remove_prefix( 1 ); + } + while( !local.empty() && local.back() == ' ' ) + { + local.remove_suffix( 1 ); + } + if( !std::ranges::all_of( local, []( char c ) { return std::isalnum( static_cast( c ) ) || c == '_'; } ) ) + { + local = {}; + } + } + for( const Reference& c : ing.references ) + { + if( c.fromSymbol != reader.fn || c.role != RefRole::Call || c.line < reader.line + || ( c.line != reader.line && !handlerRulePassesWord( handlerRuleLine( text, c.line ), local ) ) ) + { + continue; + } + for( const Symbol& s : ing.symbols ) + { + if( s.name == c.calleeName && s.fileId == fn.fileId && hr.classes().ownerOf( s.id ) == owner && s.id != reader.fn ) + { + return s.id; + } + } + } + return kNoNode; +} + +inline std::string pathHandlerRuleXml( const IngestResult& ing, const NameKeyHops& names, const PathHandlerRule& hrule, bool singleRoot, std::string_view rootPrefix ) +{ + if( !hrule.found() || hrule.rule == nullptr ) + { + return {}; + } + const HandlerRules& hr = names.rules(); + const HandlerRule& rule = *hrule.rule; + std::vector esc; + const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; + const auto rel = [ & ]( std::uint32_t fileId ) -> std::string + { + return ex( singleRoot ? sarif::rootRelativeUri( ing.files[ fileId ], rootPrefix ) : std::string_view( ing.files[ fileId ] ) ); + }; + const auto loc = [ & ]( NodeId n ) { return rel( ing.symbols[n].fileId ) + ":" + std::to_string( ing.symbols[n].line ); }; + const std::string spelled = rule.prefix + ( rule.qualified ? "{snake(Owner)_}" : "" ) + "{snake(Class)}" + ( rule.nsKeyword ? " | " + rule.prefix + "{namespace}_{snake(Class)}" : std::string() ); + const std::size_t shown = std::min( hrule.rows.size(), kHandlerRuleShown ); + std::string out = ""; + } + for( std::size_t i = 0; i < shown; ++i ) + { + const PathHandlerRow& row = hrule.rows[i]; + const Reference& site = ing.references[ row.site ]; + out += ""; + } + for( const NodeId d : hrule.listed ) + { + out += ""; + } + return out + ""; +} + +// Present-only: rides exactly when prints. +inline constexpr std::string_view kHandlerRuleLegend = + ": HOW a posted message reaches a to= handler BY NAME, not a call: " + "base='s __init_subclass__ writes attr= (p=) as name= (snake: an underscore between a lower and an upper letter, then lower case; Owner = the class it is " + "nested in), read from this repo's own source; posts= calls of from= whose first argument constructs a subclass of base= that names a to= handler, " + "classes= those classes, handlers= the to= definitions those names match (methods; which one runs depends on the receiver's class). " + ": a function that reads attr= (outside tests) and, in passes=, the method it hands the name to: a call on the line of the read, or a later call passing the local it was read into (the lookup side; no such call found: no passes=). " + ": one posted class (widest first, shown= of classes=), its handler name, its first post site, cands=K definitions of h=. " + ": every handler definition when handlers= is at most 16; above that next= lists one name's definitions. Matched, never traced; a hedge, never a hop. "; +inline std::string_view pathHandlerRuleLegend( const PathHandlerRule& hrule ) noexcept +{ + return hrule.found() ? kHandlerRuleLegend : std::string_view(); +} + + +// ── BODY-ON-ROUTE (lane path-hop-body-fix1, textual-11): the bodies of the route a reachable=0 answer already names ────────── +// A reachable=0 answer names a route it never traced: the bridge rows (poster -> queue consumer -> dispatcher) and the handler_rule +// readers (where the handler name is read and dispatched). The question is usually what those functions DO (textual-11: the post +// puts onto _message_queue, the loop takes from it, _on_message dispatches and bubbles to _parent) and the answer printed only names. +// GAIN GATE (the FOR-ROUTE lesson: 25 KB of rows that gained nothing): only symbols the answer already prints on its route, the +// bridge rows within kRouteBodyMaxHops of from= (never to=) and the readers shown; each body under the per-body guard +// (kHopBodyCapBytes); at most kRouteBodyMax bodies, capped="1" + next= past it. HONESTY: the envelope says proven="0"; a body under a +// crossing carries that crossing's via= cands= (one candidate's body, never the proven callee), a reader's via="name-key"; nothing +// here enters reachable=, hops= or any proven count. +inline constexpr std::size_t kRouteBodyMaxHops = 2; // bridge rows 0..2 (from= and the next two), never the to= row +inline constexpr std::size_t kRouteBodyMax = 4; // spew guard: bodies per answer; past it capped="1" next= the first unshown + +// The route's element (routeBodiesXml, the one printer); "" when the answer names no route body. +inline RouteBodiesXml pathRouteBodies( const IngestResult& ing, const Graph& g, const PathBridge& br, const PathHandlerRule& hrule, std::string_view rootArg ) +{ + std::vector out; + std::vector seen; + const auto add = [ & ]( NodeId n, std::string hedge ) + { + if( n >= ing.symbols.size() || ing.symbols[n].kind == SymKind::ModuleScope || ing.symbols[n].endByte <= ing.symbols[n].sigStartByte + || fold_detail::overlapsServed( ing, n, seen, /*orHolds=*/true ) ) + { + return; + } + seen.push_back( n ); + out.push_back( { n, std::move( hedge ) } ); + }; + if( br.found() && br.hops.size() >= 2 ) + { + const std::size_t last = std::min( kRouteBodyMaxHops, br.hops.size() - 2 ); // the to= row is the last one: never printed + for( std::size_t i = 0; i <= last; ++i ) + { + std::string hedge; + if( i > 0 && br.via[i] != BridgeVia::Edge ) + { + hedge = " via=\"" + std::string( kBridgeViaNames[ std::size_t( br.via[i] ) ] ) + ( bridgeViaIsSlot( br.via[i] ) ? "\" slot_callers=\"" : "\" cands=\"" ) + + std::to_string( br.fan[i] ) + "\"" + ( bridgeViaIsSlot( br.via[i] ) ? " slot_fns=\"" + std::to_string( br.fwd[i] ) + "\"" : std::string() ); // the bridge row's own widths + } + else if( i > 0 ) + { + hedge = chainHopHedge( ing, g, br.hops[i - 1], br.hops[i] ); // an edge row: by name / link / split / impl, as on a reached chain + } + add( br.hops[i], std::move( hedge ) ); + } + } + if( hrule.found() && hrule.rule != nullptr ) + { + std::size_t readers = 0; + for( const HandlerReader& reader : hrule.rule->readers ) + { + if( readers++ == kHandlerRuleReaders ) + { + break; // exactly the rows pathHandlerRuleXml prints + } + add( reader.fn, " via=\"name-key\"" ); + } + } + return routeBodiesXml( ing, out, "route", " proven=\"0\"", kRouteBodyMax, rootArg ); +} +static_assert( kRouteBodyMaxHops == 2 && kRouteBodyMax == 4 && kHopBodyCapBytes == 4000, "the route clause spells the bounds: re-word it with the knobs" ); +inline std::string_view pathRouteBodiesLegend( bool on ) noexcept +{ + return on ? "bodies of=route proven=0: the bodies of the route this answer names but never traced: the bridge rows within 2 hops of " + "from= (never to=), then the hrd readers; at most 4 (capped=1, next= the first unshown), each whole up to 4000 bytes " + "(truncated=1 past it); a hedge, never in reachable= or hops=. " + : ""; +} + } // namespace rw diff --git a/src/quality.h b/src/quality.h index e1f690de1..18d9e5e77 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2441,7 +2441,9 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 46; // MUST equal ingest.cpp's kCacheVersion (gated); 46 = train 40 (js-def-naming JsObjectLiteral; its placeholder 45), 44 = train 37b (rust-degraded FileHealth::useBoundsBlanked; its placeholder 42), 43 = train 36 (#155 trailingLambda ref byte; above main 40, #438's placeholder 41, rust-degraded's 42), 40 = train 34 (fc3-js-rebind JsRebind; above main 39), 39 = train 33 (above main 36 and l6-iface 38), 38 = train 32 lane l6-iface fix round 1: LocalBindKind ImplBlock (train renumbers), 37 = train 32 lane l6-iface: LocalBindKind DispatchType (train renumbers), 36 = train 30 (above call-result-typing's and fcall-forward's 33, name-template-dispatch's 35, main's 34), 34 = train 29 (above lane arity-min's and call-result-typing's 33), 33 = train 29 lane arity-min: def record paramsMin (the train renumbers), 33 = lane call-result-typing: RawBind resultIndex + unwrapDepth, 33 = lane fcall-forward: ref `forwarded` byte, 35 = lane name-template-dispatch: RefRole Pattern 11, 32 = train 28: RefRole Subscript 9 + Inert 10, 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 251; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 259; // MUST equal ingest.cpp's kParserVer (gated) + // 259 = 2026-10-28 (train 41: fn-slot-2 Rust slot records / py-split-marker `?name`, + // placeholders 249, 255; above fn-slot-s2's 257/258) // 251 = 2026-10-27 (train 40: js-def-naming / string-keyed / fn-slot extraction, // placeholders 238, 241, 247; above train 39's 239/240 and fn-slot-2's 249) // 239 = 2026-10-27 (train 39: receiver-fixes typing, placeholder 237/238; above train 38 diff --git a/src/valuerefindex.h b/src/valuerefindex.h index eabe0b3a4..65e3001a7 100644 --- a/src/valuerefindex.h +++ b/src/valuerefindex.h @@ -70,13 +70,13 @@ inline bool vrBindLess( const IngestResult& ing, const Reference& a, const Refer return a.startByte < b.startByte; } -// ── IX1-V2: the byFieldName languages' side of the slot join (model.h kVrSlotLangs) ────────────────────────────────── +// ── IX1-V2: the per-language pieces of the slot join (model.h kVrSlotLangs) ────────────────────────────────────── namespace slot_join { -// fix1 F-2: a byFieldName (Go) slot type's key — the package DIRECTORY the type is declared in (a Go package is a directory: +// fix1 F-2: a PackageDir (Go) slot type's key — the package DIRECTORY the type is declared in (a Go package is a directory: // x/types.Cmd and y/types.Cmd are two types though both packages are named `types`), as the first file of that directory, and the // type name: jsImportKey( dirFile, name ). keyOfSymbol is the ONE spelling the store side (SlotIndex) and --for's struct rows -// (forslots.h, through SlotIndex::goTypeKeyOf) both key by. A store's type (Reference::memberCtor, keyOf): `T` → the writing +// (forslots.h, through SlotIndex::typeKeyOf) both key by. A store's type (Reference::memberCtor, keyOf): `T` → the writing // file's directory; `pkg.T` → the directory of the file's import bound to `pkg`, when exactly one in-tree Go directory is that // import path's tail, else "" (unknown: the name tier — an out-of-tree package, an unmatched or ambiguous path). fix1 F-5: an // in-tree type that is no STRUCT (`type Table map[Op]func()`, an alias) keys kNoSlot: its keyed literal names map keys, not fields. @@ -108,7 +108,7 @@ class GoTypeKeys } for( const Symbol& sym : ing.symbols ) { - if( sym.kind == SymKind::Struct && slotJoinsByFieldName( sym.lang ) ) + if( sym.kind == SymKind::Struct && slotTypeKeyOf( sym.lang ) == VrSlotTypeKey::PackageDir ) { m_structs.try_emplace( keyOfSymbol( sym ), char( 1 ) ); } @@ -159,7 +159,514 @@ class GoTypeKeys HashMap m_structs; // every in-tree Go struct's key HashMap m_dirOf; // import path → its directory's first file (kNoFile none) }; -// The member calls `x.F( … )` of a byFieldName language whose field F holds a stored function somewhere (`holds`), as calls +// FN-SLOT part 2: each file's Rust crate as its path spells it — the root-relative path before the first `src/` directory (a crate's +// sources live under its own src/), else the file's directory; "" for a file of no path-call language (never compared). +inline std::vector cratesOf( const IngestResult& ing ) +{ + std::vector out( ing.files.size() ); + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + const std::string_view rel = rootRelPath( ing, f ); + if( !rel.ends_with( ".rs" ) || rel.starts_with( "src/" ) ) + { + continue; + } + const std::size_t at = rel.find( "/src/" ); + out[f] = at == std::string_view::npos ? includerDir( rel ) : rel.substr( 0, at ); + } + return out; +} +// FN-SLOT-2 fix 1 (review rv-fn-slot-2 F-1/F-2): where a Rust path AS WRITTEN can reach — the module path ingest_rustslots.h +// keeps (a value's memberVia, a path callee's recvVar, a struct type's memberCtor), read against the file's `use` records (the +// L3 Rust import records: ing.includes, the specifiers resolve.h resolveRustImport resolves for the call graph) and the in-tree +// crates (cratesOf). Never a guess: a path whose target is out of the tree, renamed, or not decidable reaches nothing. +// * std / core / alloc, an out-of-tree crate (`tokio::spawn`, `use extcrate::Hook`), a `use … as` rename, an unknown first +// segment (an inline module: a floor): None. +// * `crate::a::x`, `self::…`, `super::…`, `mod a; a::x`, a `use` that spells one: the ONE file resolveRustImport resolves the +// path to (this crate's root), never the writing file's own namesake (F-2); `crate::x` the crate root's. +// * a path led by an in-tree crate's name (`zed::x`, `use zed::{x}`): that crate's one, outside the writing file. +// * a bare name no `use` names: this file's, else its crate's, else the repo's only one — that last tier only when the file +// `use`s the defining crate (F-1 iv); none of the last two while the file has an OUT-OF-TREE glob (`use extcrate::*`: the +// bare name may come from it; a namesake in this file still shadows it). A bare name a `use` names while this file defines a +// namesake too: None (the `use` sits in an inner scope; which one this reference sees is not read). +class RustPaths +{ +public: + enum class Reach : std::uint8_t { Near, File, Crate, InCrate, CrateRoot, None }; + struct At + { + Reach kind = Reach::Near; + bool viaUse = false; // Near only: decided through a `use` (a namesake in this file: None) + std::uint32_t file = kNoFile; // File: the one file + std::string_view crate; // Crate / InCrate / CrateRoot: the crate + }; + void build( const IngestResult& ing ) + { + m_ing = &ing; + m_crates = cratesOf( ing ); + m_uses.assign( ing.files.size(), {} ); + for( const Include& inc : ing.includes ) + { + if( inc.fileId < ing.files.size() && !inc.isSymbolic && rootRelPath( ing, inc.fileId ).ends_with( ".rs" ) ) + { + m_uses[ inc.fileId ].push_back( inc.target ); + } + } + for( const std::string_view c : m_crates ) + { + if( c.empty() ) + { + continue; + } + const auto [ it, fresh ] = m_crateByName.try_emplace( crateName( c ), c ); + if( !fresh && it->second != c ) + { + it->second = std::string_view(); // two crates of one name: which one a path means is unknown + } + } + m_extGlob.assign( ing.files.size(), 0 ); + for( std::uint32_t f = 0; f < m_uses.size(); ++f ) + { + m_extGlob[f] = hasOutOfTreeGlob( f ) ? 1 : 0; + } + } + std::string_view crateOf( std::uint32_t f ) const { return m_crates[f]; } + // Where `name`, written after module path `module` ("" bare) by reference `r`, can reach. + At reach( const Reference& r, std::string_view module, std::string_view name ) const + { + if( !module.empty() ) + { + return moduleReach( r, module, name, 0 ); + } + std::string via; + switch( useOf( r.fileId, name, via ) ) + { + case Use::None: return At{}; + case Use::Opaque: return At{ Reach::None }; + case Use::Path: break; + } + At at = moduleReach( r, via, name, 0 ); + at.viaUse = true; + return at; + } + // The one candidate of `cands` (symbols) `at` reaches from file `fileId`: Near — its own file's, else its crate's, else the + // repo's only one (use-gated); kNoNode when the tier that has any holds two (which one is unknown: never a guess). + NodeId nearestOf( std::span cands, std::uint32_t fileId, const At& at ) const + { + const IngestResult& ing = *m_ing; + NodeId pick[3] = { kNoNode, kNoNode, kNoNode }; + int n[3] = { 0, 0, 0 }; + for( const NodeId id : cands ) + { + const std::uint32_t f = ing.symbols[id].fileId; + const int tier = tierOf( f, fileId, at ); + if( f == fileId && at.viaUse && !( at.kind == Reach::File && at.file == fileId ) ) + { + return kNoNode; // a `use` names it AND this file defines it: the use is in an inner scope (which one is unread) + } + for( int t = tier; t >= 0 && t < 3; ++t ) + { + pick[t] = id; + ++n[t]; + } + } + for( int t = 0; t < 3; ++t ) + { + if( n[t] > 0 ) + { + return n[t] == 1 ? pick[t] : kNoNode; + } + } + return kNoNode; + } + +private: + enum class Use : std::uint8_t { None, Path, Opaque }; + static bool upperLead( std::string_view s ) noexcept { return !s.empty() && s[0] >= 'A' && s[0] <= 'Z'; } + // resolve.h trimWs, newlines included (a `use` brace group spans lines) + static std::string_view trimmed( std::string_view s ) noexcept + { + for( std::size_t was = s.size() + 1; was != s.size(); ) + { + was = s.size(); + s = trimWs( s ); + s = s.starts_with( '\n' ) ? s.substr( 1 ) : s; + s = s.ends_with( '\n' ) ? s.substr( 0, s.size() - 1 ) : s; + } + return s; + } + static std::string_view firstSegment( std::string_view p ) noexcept { return p.substr( 0, std::min( p.find( "::" ), p.size() ) ); } + // A crate's name as a path spells it: its directory's last component, `-` as `_` (`crates/nu-engine` → nu_engine). + static std::string crateName( std::string_view crate ) + { + std::string n( crate.substr( crate.rfind( '/' ) == std::string_view::npos ? 0 : crate.rfind( '/' ) + 1 ) ); + std::ranges::replace( n, '-', '_' ); + return n; + } + // Each name one `use` specifier binds: cb( name, module path, renamed ). `a::{b, c::d as e, self}`, `a::b as c`; a glob reports + // the name "*" (it binds none; no identifier equals it) with the module path it opens. + template + static void forEachUse( std::string_view t, const std::string& prefix, int depth, const Cb& cb ) + { + t = trimmed( t ); + if( t.starts_with( "::" ) ) + { + t.remove_prefix( 2 ); + } + if( const std::size_t br = t.find( '{' ); br != std::string_view::npos ) + { + const std::size_t close = t.rfind( '}' ); + std::string_view head = trimmed( t.substr( 0, br ) ); + if( close == std::string_view::npos || close < br || depth > 4 ) + { + return; + } + if( head.ends_with( "::" ) ) + { + head.remove_suffix( 2 ); + } + const std::string p = head.empty() ? prefix : CallResultTypes::joinModule( prefix, head ); + const std::string_view body = t.substr( br + 1, close - br - 1 ); + int lvl = 0; + std::size_t from = 0; + for( std::size_t i = 0; i <= body.size(); ++i ) + { + const char c = i < body.size() ? body[i] : ','; + lvl += c == '{' ? 1 : c == '}' ? -1 : 0; + if( c == ',' && lvl == 0 ) + { + forEachUse( body.substr( from, i - from ), p, depth + 1, cb ); + from = i + 1; + } + } + return; + } + std::string_view alias; + if( const std::size_t as = t.find( " as " ); as != std::string_view::npos ) + { + alias = trimmed( t.substr( as + 4 ) ); + t = trimmed( t.substr( 0, as ) ); + } + const std::size_t c = t.rfind( "::" ); + std::string_view leaf = c == std::string_view::npos ? t : t.substr( c + 2 ); + std::string module = c == std::string_view::npos ? prefix : CallResultTypes::joinModule( prefix, t.substr( 0, c ) ); + std::string own; // the `self` module's name, copied BEFORE `module` is reassigned (a view into it would dangle) + if( leaf == "self" ) + { + const std::size_t up = module.rfind( "::" ); // `a::b::{self}` binds module b + own = module.substr( up == std::string::npos ? 0 : up + 2 ); + module = up == std::string::npos ? std::string() : module.substr( 0, up ); + if( alias.empty() ) + { + cb( std::string_view( own ), module, false ); + return; + } + leaf = own; + } + if( leaf.empty() ) + { + return; + } + if( leaf == "*" ) + { + cb( leaf, module, false ); // a glob binds no name; its module path is what hasOutOfTreeGlob reads + return; + } + cb( alias.empty() ? leaf : alias, module, !alias.empty() && alias != leaf ); + } + // The module path the file's `use` binds `name` from (`use tokio::spawn` → "tokio"); Opaque: a rename, or two that differ. + Use useOf( std::uint32_t fileId, std::string_view name, std::string& via ) const + { + Use got = Use::None; + for( const std::string_view t : m_uses[ fileId ] ) + { + if( t.starts_with( "mod:" ) || got == Use::Opaque ) + { + continue; + } + forEachUse( t, std::string(), 0, [ & ]( std::string_view bound, const std::string& module, bool renamed ) + { + if( bound != name ) + { + return; + } + if( renamed || module.empty() || ( got == Use::Path && via != module ) ) + { + got = Use::Opaque; + return; + } + got = Use::Path; + via = module; + } ); + } + return got; + } + bool declaresMod( std::uint32_t fileId, std::string_view seg ) const + { + return std::ranges::any_of( m_uses[ fileId ], [ & ]( std::string_view t ) { return t.starts_with( "mod:" ) && t.substr( 4 ) == seg; } ); + } + // The file `use`s a glob of something outside the tree (`use extcrate::*`, `use std::io::prelude::*`): a bare name no `use` + // names may be that glob's, so an in-tree namesake of another file is one guess among two. A glob led by `crate` / `self` / + // `super`, an in-tree crate or a `mod` this file declares is in the tree (its names are in-tree ones); the crate-less `use *` none. + bool hasOutOfTreeGlob( std::uint32_t fileId ) const + { + bool out = false; + for( const std::string_view t : m_uses[ fileId ] ) + { + if( t.starts_with( "mod:" ) || out ) + { + continue; + } + forEachUse( t, std::string(), 0, [ & ]( std::string_view bound, const std::string& module, bool ) + { + const std::string_view seg = trimmed( firstSegment( module ) ); + out = out || ( bound == "*" && !seg.empty() && seg != "crate" && seg != "self" && seg != "super" + && !m_crateByName.contains( std::string( seg ) ) && !declaresMod( fileId, seg ) ); + } ); + } + return out; + } + bool usesCrate( std::uint32_t fileId, std::string_view crate ) const + { + const std::string want = crateName( crate ); + return std::ranges::any_of( m_uses[ fileId ], [ & ]( std::string_view t ) + { + t = trimmed( t.starts_with( "::" ) ? t.substr( 2 ) : t ); + return trimmed( t.substr( 0, std::min( { t.find( "::" ), t.find( '{' ), t.size() } ) ) ) == want; + } ); + } + // The candidate's tier for `at` (-1: out of reach). Near: 0 own file, 1 own crate, 2 another crate the file `use`s — tiers 1 and 2 + // only while the file has no out-of-tree glob (fix 2 L-2: that glob may be where the bare name comes from; a local item shadows it). + int tierOf( std::uint32_t f, std::uint32_t fileId, const At& at ) const + { + switch( at.kind ) + { + case Reach::Near: + return f == fileId ? 0 : m_extGlob[fileId] ? -1 : m_crates[f] == m_crates[fileId] ? 1 : usesCrate( fileId, m_crates[f] ) ? 2 : -1; + case Reach::File: return f == at.file ? 0 : -1; + case Reach::Crate: return f != fileId && m_crates[f] == at.crate ? 0 : -1; + case Reach::InCrate: return m_crates[f] == at.crate ? 0 : -1; + case Reach::CrateRoot: + { + const std::string_view p = rootRelPath( *m_ing, f ); + return m_crates[f] == at.crate && ( p.ends_with( "/lib.rs" ) || p.ends_with( "/main.rs" ) ) && includerDir( p ) == crateRootDir( f ) ? 0 : -1; + } + case Reach::None: return -1; + } + return -1; + } + // The directory a file's crate's `crate::` anchors in: `/src`, a root crate's `src`, else the crate's own directory. + std::string_view crateRootDir( std::uint32_t f ) const + { + const std::string_view rel = rootRelPath( *m_ing, f ); + if( rel.starts_with( "src/" ) ) + { + return rel.substr( 0, 3 ); + } + const std::size_t at = rel.find( "/src/" ); + return at == std::string_view::npos ? includerDir( rel ) : rel.substr( 0, at + 4 ); + } + At fileAt( std::uint32_t fileId, const std::string& target, std::uint32_t rootOf ) const + { + if( m_fileIndex.empty() ) + { + m_fileIndex.reserve( m_ing->files.size() ); + for( std::uint32_t f = 0; f < m_ing->files.size(); ++f ) + { + m_fileIndex.emplace( lexicalNormalize( rootRelPath( *m_ing, f ) ), f ); + } + } + const std::uint32_t f = resolveRustImport( rootRelPath( *m_ing, fileId ), target, m_fileIndex, crateRootDir( rootOf ), true ); + return f == kNoFile ? At{ Reach::None } : At{ Reach::File, false, f }; + } + // `module::name` led by an anchor, an in-tree crate's name, a `mod` this file declares or a module a `use` binds. + At moduleReach( const Reference& r, std::string_view module, std::string_view name, int depth ) const + { + const std::string_view seg = trimmed( firstSegment( module ) ); + const std::string_view rest = module.size() > seg.size() + 2 ? module.substr( seg.size() + 2 ) : std::string_view(); + if( seg == "std" || seg == "core" || seg == "alloc" ) + { + return At{ Reach::None }; + } + if( seg == "crate" || seg == "self" || seg == "super" ) + { + return anchored( r, seg, rest, name ); + } + if( const auto it = m_crateByName.find( std::string( seg ) ); it != m_crateByName.end() ) + { + if( it->second.empty() ) + { + return At{ Reach::None }; + } + if( rest.empty() ) + { + return At{ Reach::Crate, false, kNoFile, it->second }; + } + for( std::uint32_t f = 0; f < m_crates.size(); ++f ) + { + if( m_crates[f] == it->second ) + { + return fileAt( r.fileId, "crate::" + CallResultTypes::joinModule( rest, name ), f ); // that crate's root anchors it + } + } + return At{ Reach::None }; + } + if( depth == 0 && declaresMod( r.fileId, seg ) ) + { + return anchored( r, "self", module, name ); + } + std::string via; + if( depth == 0 && useOf( r.fileId, seg, via ) == Use::Path ) + { + return moduleReach( r, CallResultTypes::joinModule( via, module ), name, 1 ); + } + return At{ Reach::None }; + } + // `crate::…`, `self::…`, `super::…`: resolveRustImport's module-file rule from this file (`rest` the modules after the anchor). + At anchored( const Reference& r, std::string_view anchor, std::string_view rest, std::string_view name ) const + { + const IngestResult& ing = *m_ing; + const std::string_view path = rootRelPath( ing, r.fileId ); + const std::string_view dir = includerDir( path ); + std::string_view stem = path.substr( dir.empty() ? 0 : dir.size() + 1 ); + stem = stem.substr( 0, stem.rfind( '.' ) ); + const bool modFile = stem == "mod" || stem == "lib" || stem == "main"; + if( anchor == "crate" ) + { + return rest.empty() ? At{ Reach::CrateRoot, false, kNoFile, m_crates[ r.fileId ] } : fileAt( r.fileId, "crate::" + CallResultTypes::joinModule( rest, name ), r.fileId ); + } + if( r.fromSymbol < ing.symbols.size() ) + { + const Symbol& from = ing.symbols[ r.fromSymbol ]; + if( from.kind == SymKind::Method && !from.scope.empty() && !upperLead( from.scope ) ) + { + return At{ Reach::None }; // inside an inline `mod m { … }`: `self` / `super` are not this file's module + } + } + if( anchor == "self" ) + { + if( rest.empty() ) + { + return At{ Reach::File, false, r.fileId }; + } + return fileAt( r.fileId, modFile ? "self::" + CallResultTypes::joinModule( rest, name ) : "self::" + CallResultTypes::joinModule( CallResultTypes::joinModule( stem, rest ), name ), r.fileId ); + } + if( modFile ) // super of a mod.rs: its directory's parent module (a crate root has none) + { + return stem != "mod" || rest.empty() ? At{ Reach::None } : fileAt( r.fileId, "super::" + CallResultTypes::joinModule( rest, name ), r.fileId ); + } + if( rest.empty() ) // `super::x` in d/s.rs: x in module d (d.rs or d/mod.rs) + { + const std::string_view up = dir.substr( dir.rfind( '/' ) == std::string_view::npos ? 0 : dir.rfind( '/' ) + 1 ); + return up.empty() ? At{ Reach::None } : fileAt( r.fileId, "super::" + CallResultTypes::joinModule( up, name ), r.fileId ); + } + return fileAt( r.fileId, "self::" + CallResultTypes::joinModule( rest, name ), r.fileId ); // super::a::x in d/s.rs: d/a… + } + + const IngestResult* m_ing = nullptr; + std::vector m_crates; // cratesOf + std::vector> m_uses; // per file: its Rust `use` specifiers and `mod:x` records + HashMap m_crateByName; // a crate's path name → the crate ("" two crates share it) + std::vector m_extGlob; // per file: 1 when it `use`s an out-of-tree glob (hasOutOfTreeGlob) + mutable HashMap m_fileIndex; // root-relative normalised path → fileId, built on first anchored path +}; +// FN-SLOT part 2: a ModuleType (Rust) slot type's key — the in-tree STRUCT symbol the written path reaches (RustPaths: a bare +// name its own file's, else its crate's, else the repo's only one; a module path the file it resolves to), as jsImportKey( symbolId, name ); "" when no struct or several of one tier carry the +// name (an out-of-tree type, or which one is unknown: the name tier); kNoSlot when the name is a path through a TYPE (`E::V`, a +// variant: no field slot of a struct). +class RustTypeKeys +{ +public: + void build( const IngestResult& ing ) + { + for( NodeId id = 0; id < ing.symbols.size(); ++id ) + { + const Symbol& sym = ing.symbols[id]; + if( ( sym.kind == SymKind::Struct || sym.kind == SymKind::Class ) && slotTypeKeyOf( sym.lang ) == VrSlotTypeKey::ModuleType ) // a Rust struct is a class row + { + m_structs[ sym.name ].push_back( id ); + } + } + if( !m_structs.empty() ) + { + m_paths.build( ing ); + } + } + static std::string keyOfSymbol( const Symbol& s, NodeId id ) { return jsImportKey( id, s.name ); } + std::string keyOf( const IngestResult& ing, const Reference& r ) const + { + const std::string_view t = r.memberCtor; + if( t.empty() || m_structs.empty() ) + { + return {}; + } + const std::size_t at = t.rfind( "::" ); + const std::string_view name = at == std::string_view::npos ? t : t.substr( at + 2 ); + const std::string_view module = at == std::string_view::npos ? std::string_view() : t.substr( 0, at ); + const std::size_t up = module.rfind( "::" ); + const std::string_view last = up == std::string_view::npos ? module : module.substr( up + 2 ); + if( !last.empty() && last[0] >= 'A' && last[0] <= 'Z' ) + { + return std::string( kNoSlot ); // `E::V`: a variant, no struct's field + } + const RustPaths::At reach = m_paths.reach( r, module, name ); + if( reach.kind == RustPaths::Reach::None ) + { + return std::string( kNoSlot ); // an out-of-tree or undecidable type (`extcrate::Hook`, `std::…`): no in-tree slot + } + const auto it = m_structs.find( std::string( name ) ); + if( it == m_structs.end() ) + { + return {}; + } + const NodeId id = m_paths.nearestOf( it->second, r.fileId, reach ); + return id == kNoNode ? std::string() : keyOfSymbol( ing.symbols[id], id ); + } + +private: + HashMap> m_structs; // struct name → the in-tree Rust structs of that name + RustPaths m_paths; // built when any Rust struct exists +}; +// The type key of every language that keys its own types (model.h slotKeysOwnTypes), by its row's VrSlotTypeKey. +class TypeKeys +{ +public: + void build( const IngestResult& ing ) + { + m_ing = &ing; + m_go.build( ing ); + m_rust.build( ing ); + } + // A switch with no default (rv-fn-slot-2 F-8): a new VrSlotTypeKey fails -Wswitch here instead of keying as a Go directory. + // TypedefFacts keys nothing here (its slots key through SlotIndex's typedef facts; endOf and forslots never ask). + std::string keyOf( const Reference& r ) + { + switch( slotTypeKeyOf( r.lang ) ) + { + case VrSlotTypeKey::PackageDir: return m_go.keyOf( r ); + case VrSlotTypeKey::ModuleType: return m_rust.keyOf( *m_ing, r ); + case VrSlotTypeKey::TypedefFacts: return {}; + } + return {}; + } + std::string keyOfSymbol( NodeId id ) const + { + const Symbol& s = m_ing->symbols[id]; + switch( slotTypeKeyOf( s.lang ) ) + { + case VrSlotTypeKey::PackageDir: return m_go.keyOfSymbol( s ); + case VrSlotTypeKey::ModuleType: return RustTypeKeys::keyOfSymbol( s, id ); + case VrSlotTypeKey::TypedefFacts: return {}; + } + return {}; + } + +private: + const IngestResult* m_ing = nullptr; + GoTypeKeys m_go; + RustTypeKeys m_rust; +}; +// The member calls `x.F( … )` of a MemberCallByName language whose field F holds a stored function somewhere (`holds`), as calls // through that slot, in reference order. Not when that language defines a METHOD named F anywhere in the tree: the call may // then be that method, and the receiver's type is not read here (a named floor, never a guess). template @@ -200,15 +707,16 @@ class ParamSlots { public: void take( std::uint32_t refIdx ) { m_refs.push_back( refIdx ); } - void build( const IngestResult& ing, GoTypeKeys& keys ) + void build( const IngestResult& ing, TypeKeys& keys ) { for( const std::uint32_t q : m_refs ) { const Reference& r = ing.references[q]; - if( r.fromSymbol >= ing.symbols.size() || ing.symbols[ r.fromSymbol ].kind != SymKind::Function - || ing.symbols[ r.fromSymbol ].name != r.recvVar ) + const bool pathCalls = !slotPathSep( r.lang ).empty(); // a path call `T::f( … )` reaches an associated function + if( r.fromSymbol >= ing.symbols.size() || ing.symbols[ r.fromSymbol ].name != r.recvVar + || !( ing.symbols[ r.fromSymbol ].kind == SymKind::Function || ( pathCalls && ing.symbols[ r.fromSymbol ].kind == SymKind::Method ) ) ) { - continue; // a method registrar (no bare call reaches it), or a fact outside its own function's definition + continue; // a method registrar no call of its language reaches by name, or a fact outside its own function's definition } const std::string to = keys.keyOf( r ) + "#" + r.memberPath; std::string& have = m_slot[ jsImportKey( r.fromSymbol, std::to_string( r.argCount ) ) ]; @@ -243,7 +751,7 @@ class ParamSlots private: static constexpr std::string_view kConflict = "\x01"; - std::vector m_refs; // the facts, keyed in build() (the type key by its file, GoTypeKeys) + std::vector m_refs; // the facts, keyed in build() (the type key by its file, TypeKeys) HashMap m_slot; // "registrarId#k" → "TypeKey#field" (kConflict: two different slots) }; // `extra` (ascending) merged into the slot calls and their per-function lists, all kept in reference order (join() and callsIn() @@ -310,8 +818,8 @@ class SlotIndex // After every take: keys the slot values (`values`, ascending, with their resolved `targets`) and the calls by field. void build( const IngestResult& ing, std::span values, std::span> targets ) { - m_goKeys.build( ing ); - m_params.build( ing, m_goKeys ); + m_keys.build( ing ); + m_params.build( ing, m_keys ); for( std::size_t k = 0; k < values.size(); ++k ) { if( targets[k].empty() ) @@ -333,7 +841,7 @@ class SlotIndex } m_valuesByField[ m_values.back().field ].push_back( static_cast( m_values.size() - 1 ) ); } - slot_join::addCalls( ing, slot_join::memberCalls( ing, m_valuesByField ), m_calls, m_callsBySym ); // IX1-V2: a byFieldName slot's calls + slot_join::addCalls( ing, slot_join::memberCalls( ing, m_valuesByField ), m_calls, m_callsBySym ); // IX1-V2: a MemberCallByName slot's calls if( m_calls.empty() ) { return; // no call through a struct field anywhere: no value needs a slot key @@ -350,8 +858,9 @@ class SlotIndex std::vector callsFor( std::uint32_t valueIdx ) const { return join( m_values, valueIdx, m_callsByField, m_callEnds ); } // The slot value references (with a resolved function) the slot call `callIdx` may call through. std::vector valuesFor( std::uint32_t callIdx ) const { return join( m_callEnds, callIdx, m_valuesByField, m_values ); } - // fix1 F-2: a byFieldName struct symbol's type key, as valuesOfType reads it (the store side keys the same way) - std::string goTypeKeyOf( const Symbol& s ) const { return m_goKeys.keyOfSymbol( s ); } + // fix1 F-2: the slot type key of a struct symbol of a language that keys its own types (model.h slotKeysOwnTypes), as + // valuesOfType reads it (the store side keys the same way) + std::string typeKeyOf( NodeId id ) const { return m_keys.keyOfSymbol( id ); } // IX1-V2: the slot value references stored into a field of struct type `type` (the resolved type name), with the field, // in reference order — what --for's slot_fields= lists on a struct row. std::vector> valuesOfType( std::string_view type ) const @@ -384,7 +893,10 @@ class SlotIndex { return "." + e->field; } - const std::string_view t = std::string_view( e->type ).substr( 0, e->type.starts_with( '@' ) ? e->type.find( '#' ) : std::string::npos ); + // an anonymous struct's "@LINE#file" prints "@LINE"; an own-keyed type's "id#Name" (TypeKeys) prints its name + const std::size_t hash = e->type.find( '#' ); + const std::string_view t = e->type.starts_with( '@' ) ? std::string_view( e->type ).substr( 0, hash ) + : hash == std::string::npos ? std::string_view( e->type ) : std::string_view( e->type ).substr( hash + 1 ); return std::string( t ) + "." + e->field; } @@ -471,11 +983,11 @@ class SlotIndex End e; e.ref = refIdx; e.fam = valueRefFamily( r.lang ); - if( r.role == RefRole::Call || slotJoinsByFieldName( r.lang ) ) + if( r.role == RefRole::Call || slotKeysOwnTypes( r.lang ) ) { - // IX1-V2: a member call `x.F( … )` (slot_join::memberCalls, its receiver's type unread: the name tier), or a byFieldName - // store (package-qualified; no typedef aliases or field-type facts on this side) - e.type = r.role == RefRole::Call ? std::string() : m_goKeys.keyOf( r ); + // IX1-V2: a member call `x.F( … )` (slot_join::memberCalls, its receiver's type unread: the name tier), or a store of a + // language that keys its own types (Go: package-qualified; no typedef aliases or field-type facts on this side) + e.type = r.role == RefRole::Call ? std::string() : m_keys.keyOf( r ); e.field = r.role == RefRole::Call ? r.calleeName : r.memberPath; return e; } @@ -531,7 +1043,7 @@ class SlotIndex HashMap m_aliasInFile; // "fileId#name" → struct type, the file's own typedef HashMap m_body; // struct type → its body's digest (kConflict: files differ) slot_join::ParamSlots m_params; // IX1-V2: registrars' parameters stored into field slots - slot_join::GoTypeKeys m_goKeys; // fix1 F-2: Go type keys by package directory + slot_join::TypeKeys m_keys; // fix1 F-2 / part 2: Go and Rust type keys std::function( const Reference& )> m_ownersOf; // fix1 F-1: a bare callee's definitions std::vector m_calls; // slot Through references, ascending HashMap> m_callsBySym; // calling function → its slot Through references @@ -555,6 +1067,31 @@ class ValueRefIndex const SlotIndex& slots() const noexcept { return m_slots; } // IX-1: the struct-field slot join + // The files that the JS/TS/Python imports of file `fileId` binding `name` resolve to, in import order (none: no such import, or + // a package outside the tree). The import's module is resolved by the call graph's own Step-A (resolve.h resolvePreciseInclude: + // a relative JS/TS specifier against the importer, a Python module relative-to-file then root, then the whole-component suffix + // for an absolute Python spec). A bare package specifier resolves to no file: no row — a stem match across directories bound + // `import { test } from "./a"` to every a.js. resolveName and the class-base check of handlerrule.h read this one rule. + std::vector importedFilesBinding( std::uint32_t fileId, std::string_view name ) const + { + std::vector out; + const auto imp = m_importsByFile.find( fileId ); + if( imp == m_importsByFile.end() ) + { + return out; + } + for( const Binding* b : imp->second ) + { + const bool named = b->kind == LocalBindKind::JsImport ? ( b->var == name && b->importedName == name ) + : ( b->var == name && b->importedName.empty() ); + if( const std::uint32_t moduleFile = named ? importedModuleFile( *b ) : kNoFile; moduleFile != kNoFile ) + { + out.push_back( moduleFile ); + } + } + return out; + } + // Value references (indices into ing.references) whose resolved target is any of `defs`, in served order. std::vector valueRefsTo( std::span defs ) const { @@ -815,6 +1352,7 @@ class ValueRefIndex m_fnByName[ s.name ].push_back( id ); } } + indexOwners( keep ); markPythonMembers(); for( std::uint32_t i = 0; i < ing.references.size(); ++i ) { @@ -837,10 +1375,33 @@ class ValueRefIndex m_valuesByTarget[ t ].push_back( static_cast( k ) ); } } - m_slots.resolveCalleesBy( [ this ]( const Reference& v ) { return resolveName( v, v.recvVar ); } ); + m_slots.resolveCalleesBy( [ this ]( const Reference& v ) { return resolveCallee( v ); } ); m_slots.build( ing, m_values, m_targets ); m_slots.resolveCalleesBy( nullptr ); // it captured `this`: never outlives the build } + // part 2: a path language's functions by owner type (`Editor::backspace`, ownedBy's "family#Owner::name"), and every file's + // crate (slot_join::cratesOf) when such a language is indexed. + void indexOwners( const KeepSets* keep ) + { + bool pathLang = false; + for( NodeId id = 0; id < m_ing.symbols.size(); ++id ) + { + const Symbol& s = m_ing.symbols[id]; + if( slotPathSep( s.lang ).empty() ) + { + continue; + } + pathLang = true; + if( ( s.kind == SymKind::Function || s.kind == SymKind::Method ) && !s.scope.empty() && ( keep == nullptr || keep->names.contains( s.name ) ) ) + { + m_fnByOwner[ jsImportKey( static_cast( valueRefFamily( s.lang ) ), s.scope + "::" + s.name ) ].push_back( id ); + } + } + if( pathLang ) + { + m_paths.build( m_ing ); + } + } // Reference `i` into its table (`keep` non-null: a narrow index, which skips what the splice never reads). void indexReference( std::uint32_t i, const KeepSets* keep ) { @@ -913,8 +1474,11 @@ class ValueRefIndex { continue; } - const bool named = std::ranges::binary_search( names, std::string_view( r.calleeName ), rw::sortutil::svLess ); - if( !named && r.memberPath.empty() && !( sc == 'a' && registrars.contains( r.recvVar ) ) ) + const bool named = std::ranges::binary_search( names, std::string_view( r.calleeName ), rw::sortutil::svLess ); + const std::string_view sep = slotPathSep( r.lang ); + const std::size_t at = sep.empty() ? std::string::npos : r.recvVar.rfind( sep ); + const std::string leaf = at == std::string::npos ? r.recvVar : r.recvVar.substr( at + sep.size() ); // resolveCallee's name + if( !named && r.memberPath.empty() && !( sc == 'a' && registrars.contains( leaf ) ) ) { continue; } @@ -922,7 +1486,7 @@ class ValueRefIndex k.names.try_emplace( r.calleeName, char( 1 ) ); if( sc == 'a' ) { - k.names.try_emplace( r.recvVar, char( 1 ) ); + k.names.try_emplace( leaf, char( 1 ) ); } else if( sc == 'p' ) { @@ -1064,6 +1628,96 @@ class ValueRefIndex return fam == VrLangFamily::C && fIsFn && f.scope == m.scope; // a C++ member function of the same class } + // The method-value owner rule (rv-fn-slot part-2 piece 3): `fns` (one name's functions) owned by type `owner`, as reference + // `r`'s language means it. Go (a method value `b.opPlan` of the enclosing method's receiver, ingest_valuerefs.h methodValueAt): + // that type's METHOD in r's own package directory. Rust (`Editor::backspace`, `Self::f` with Self resolved, `module` the path + // written before the type): the owner type's function of that name in the crate the type's path reaches (slot_join::RustPaths; + // a bare type: nearest r — its own file, else its crate, else the repo's only one), else nothing. + std::vector ownedBy( const Reference& r, std::string_view owner, std::span fns, std::string_view module = {} ) const + { + const VrLangFamily fam = valueRefFamily( r.lang ); + if( !slotPathSep( r.lang ).empty() ) + { + const auto it = fns.empty() ? m_fnByOwner.end() : m_fnByOwner.find( jsImportKey( static_cast( fam ), std::string( owner ) + "::" + m_ing.symbols[ fns.front() ].name ) ); + if( it == m_fnByOwner.end() ) + { + return {}; + } + using Reach = slot_join::RustPaths::Reach; + slot_join::RustPaths::At at = m_paths.reach( r, module, owner ); + if( at.kind == Reach::File || at.kind == Reach::Crate || at.kind == Reach::CrateRoot ) + { + // the type's own crate (an impl lives in its type's crate, not always its file) + at = { Reach::InCrate, false, kNoFile, at.kind == Reach::File ? m_paths.crateOf( at.file ) : at.crate }; + } + const NodeId one = m_paths.nearestOf( it->second, r.fileId, at ); + return one == kNoNode ? std::vector{} : std::vector{ one }; + } + std::vector out; + for( const NodeId id : fns ) + { + const Symbol& s = m_ing.symbols[id]; + if( s.kind == SymKind::Method && valueRefFamily( s.lang ) == fam && s.scope == owner + && includerDir( m_ing.files[ s.fileId ] ) == includerDir( m_ing.files[ r.fileId ] ) ) + { + out.push_back( id ); + } + } + return out; + } + // fix1 F-1 / part-2 piece 4: the definitions an argument row's callee (recvVar) resolves to — a bare callee by resolveName; + // a path callee `Owner::name` (a language whose slot row has a pathSep) by ownedBy, never by its bare last segment. + std::vector resolveCallee( const Reference& v ) const + { + const std::string_view sep = slotPathSep( v.lang ); + const std::size_t at = sep.empty() ? std::string::npos : v.recvVar.rfind( sep ); + if( at == std::string::npos ) + { + return valueRefFamily( v.lang ) == VrLangFamily::Rust ? resolveIn( v, v.recvVar, {} ) : resolveName( v, v.recvVar ); + } + const std::string_view path = std::string_view( v.recvVar ).substr( 0, at ); // `crate::other` / `a::Owner` / `Owner` + const std::size_t up = path.rfind( sep ); + const std::string_view owner = up == std::string_view::npos ? path : path.substr( up + sep.size() ); + if( owner.empty() || !( owner[0] >= 'A' && owner[0] <= 'Z' ) ) + { + return resolveIn( v, std::string_view( v.recvVar ).substr( at + sep.size() ), path ); // a module path: no owner type + } + const auto it = m_fnByName.find( v.recvVar.substr( at + sep.size() ) ); + return it == m_fnByName.end() ? std::vector{} : ownedBy( v, owner, it->second, up == std::string_view::npos ? std::string_view() : path.substr( 0, up ) ); + } + + // FN-SLOT-2 fix 1: Rust function `name` written after module path `module` ("" bare) by reference `r` — the one + // slot_join::RustPaths reaches (a bare name with no `use`: every one in r's own file, else its crate's one, else the repo's + // only one in a crate the file `use`s); a class member (an impl's or an inline module's function) is never in bare scope. + std::vector resolveIn( const Reference& r, std::string_view name, std::string_view module ) const + { + const auto it = m_fnByName.find( std::string( name ) ); + if( it == m_fnByName.end() ) + { + return {}; + } + std::vector cands, sameFile; + for( const NodeId id : it->second ) + { + const Symbol& s = m_ing.symbols[id]; + if( valueRefFamily( s.lang ) != VrLangFamily::Rust || ( isClassMember( s ) && !memberVisibleFrom( r, s, VrLangFamily::Rust ) ) ) + { + continue; + } + cands.push_back( id ); + if( s.fileId == r.fileId ) + { + sameFile.push_back( id ); + } + } + const slot_join::RustPaths::At at = m_paths.reach( r, module, name ); + if( at.kind == slot_join::RustPaths::Reach::Near && !sameFile.empty() ) + { + return sameFile; // the file's own (`#[cfg]` twins all of them, as every family's same-file rule) + } + const NodeId one = m_paths.nearestOf( cands, r.fileId, at ); + return one == kNoNode ? std::vector{} : std::vector{ one }; + } // `name` as seen from reference `r`'s file, under the visibility rules in the header comment. std::vector resolveName( const Reference& r, std::string_view name ) const { @@ -1075,19 +1729,11 @@ class ValueRefIndex const VrLangFamily fam = valueRefFamily( r.lang ); if( r.role == RefRole::Value && !r.memberRoot.empty() ) { - // IX1-V2: a Go method value `b.opPlan` of the enclosing method's receiver (ingest_valuerefs.h methodValueAt): that - // receiver type's method of this name in this package, else nothing - std::vector out; - for( const NodeId id : it->second ) - { - const Symbol& s = m_ing.symbols[id]; - if( s.kind == SymKind::Method && valueRefFamily( s.lang ) == fam && s.scope == r.memberRoot - && includerDir( m_ing.files[ s.fileId ] ) == includerDir( m_ing.files[ r.fileId ] ) ) - { - out.push_back( id ); - } - } - return out; + return ownedBy( r, r.memberRoot, it->second, r.memberVia ); + } + if( valueRefFamily( r.lang ) == VrLangFamily::Rust ) + { + return resolveIn( r, name, r.role == RefRole::Value ? std::string_view( r.memberVia ) : std::string_view() ); } // A C/C++ PROTOTYPE (a bodyless declaration) names the function its definition lives in: it is the target only // when no definition with a body is visible — a prototype in this file never hides the extern definition. @@ -1149,27 +1795,12 @@ class ValueRefIndex } break; } + case VrLangFamily::Rust: break; // resolveIn above case VrLangFamily::Js: case VrLangFamily::Py: { - const auto imp = m_importsByFile.find( r.fileId ); - if( imp == m_importsByFile.end() ) - { - break; - } - for( const Binding* b : imp->second ) + for( const std::uint32_t moduleFile : importedFilesBinding( r.fileId, name ) ) { - const bool named = b->kind == LocalBindKind::JsImport ? ( b->var == name && b->importedName == name ) - : ( b->var == name && b->importedName.empty() ); - if( !named ) - { - continue; - } - // The import's module, resolved by the call graph's own Step-A (resolve.h resolvePreciseInclude: a - // relative JS/TS specifier against the importer, a Python module relative-to-file then root, then the - // whole-component suffix for an absolute Python spec). A bare package specifier resolves to no file: - // no row — a stem match across directories bound `import { test } from "./a"` to every a.js. - const std::uint32_t moduleFile = importedModuleFile( *b ); for( const NodeId id : other ) { if( m_ing.symbols[id].fileId == moduleFile ) @@ -1189,6 +1820,8 @@ class ValueRefIndex const IngestResult& m_ing; HashMap> m_fnByName; + HashMap> m_fnByOwner; // part 2: "family#Owner::name" → a path language's owned functions + slot_join::RustPaths m_paths; // part 2 fix 1: where a Rust path reaches, when a path language is indexed std::vector m_pyMember; // per symbol: 1 = a Python def directly in its class (empty: no Python) std::vector m_values; // Value reference indices, ascending std::vector m_depthCuts; // the files' depth-cut records (no row), ascending @@ -1247,6 +1880,16 @@ class ClassFamilies } return isAncestor( member, site ) || isAncestor( site, member ); } + // Calls `visit( base )` for each class definition the written base names of class `c` bind to, in reference order. + template + void eachBase( NodeId c, Visit&& visit ) const + { + buildBases(); + if( const auto it = m_basesOf.find( c ); it != m_basesOf.end() ) + { + std::ranges::for_each( it->second, visit ); + } + } private: struct ClassSpan diff --git a/src/verbs_for.h b/src/verbs_for.h index a260c2600..4333c2b9d 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -578,6 +578,8 @@ struct ForLensHeaderParts bool bodyFoldPresent = false; // body-fold: a compact-route answer in the default regime has fold candidates // (forfold.h forBodyFoldCandidates), so a rides and its clause defines it; // set after construction, cleared with the section + bool foldFollowPresent = false; // WRAPPER-FOLLOW (forfold.h renderFollows): the fold carries a (set after + // the fold renders), so its clause defines it bool memberNamesPresent = false; // member-names (formembers.h): a shown owner row carries mem= (set after the // splice), so its clause defines it bool initSlotsPresent = false; // FOR-SLOTS (forslots.h): a shown initializer row carries slots= (set after the @@ -588,6 +590,7 @@ struct ForLensHeaderParts bool callerHopPresent = false; // mf1-nx: a name-exact answer whose body candidates are ONE function in the default // regime (forcallerhop.h forCallerHopTargets), so a rides and its clause // defines it; set after construction, cleared with the section + bool callerHopRanked = false; // UPWARD-2HOP: that is the ranked seed's (by="rank"), so ITS clause rides instead bool epPresent = false; // ep-manifest (epmanifest.h): a manifest entry-point group registers a shown row in // the default regime, so an rides and its clause defines it; cleared with it }; @@ -790,6 +793,13 @@ inline constexpr std::string_view presentOnlyClause( bool present, bool compact, { return !present ? std::string_view() : ( compact ? compactClause : fullClause ); } + +// mf1-nx / UPWARD-2HOP: the caller hop's present-only clause — the named-route one, or the ranked seed's (by="rank") +inline constexpr std::string_view callerHopClause( const ForLensHeaderParts& p, bool compact ) noexcept +{ + return p.callerHopRanked ? presentOnlyClause( p.callerHopPresent, compact, rw::kForCompactRankedCallerHopLegend, rw::kForRankedCallerHopLegend ) + : presentOnlyClause( p.callerHopPresent, compact, rw::kForCompactCallerHopLegend, rw::kForCallerHopLegend ); +} // FOR-SLOTS (slots=) and OWNER-CTOR-HOP (ctor_l=/ctor_calls=): the two -row splices' clauses, then ep-manifest's clause (epmanifest.h), each present-only, in that order inline std::string peerRowClauses( const ForLensHeaderParts& p, bool compact ) { @@ -805,9 +815,11 @@ inline std::string_view hopsCallsNextLegend( const ForLensHeaderParts& p, bool c return presentOnlyClause( p.hopsCallsNextPresent, compact, kForCompactHopsCallsNextClause, kForHopsCallsNextClause ); } // body-fold: the fold's clause in this dialect, present-only (bodyFoldPresent), right after the slot clause -inline std::string_view bodyFoldLegend( const ForLensHeaderParts& p, bool compact ) noexcept +// WRAPPER-FOLLOW: the follow_of= clause rides right after it, present-only (foldFollowPresent) +inline std::string bodyFoldLegend( const ForLensHeaderParts& p, bool compact ) { - return presentOnlyClause( p.bodyFoldPresent, compact, rw::kForCompactBodyFoldLegend, rw::kForBodyFoldLegend ); + return std::string( presentOnlyClause( p.bodyFoldPresent, compact, rw::kForCompactBodyFoldLegend, rw::kForBodyFoldLegend ) ) + + std::string( presentOnlyClause( p.foldFollowPresent, compact, rw::kForCompactFoldFollowLegend, rw::kForFoldFollowLegend ) ); } inline constexpr std::string_view kForCompactLegendTail = "; t p= file outside sigs (weaker), t sym= its top symbols (a pointer, not the answer), r= rank (gap = trimmed)"; @@ -922,7 +934,7 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, h += bodyFoldLegend( p, /*compact=*/true ); // body-fold: present-only, after the slot clause h += presentOnlyClause( p.memberNamesPresent, /*compact=*/true, rw::kForCompactMemberNamesLegend, rw::kForMemberNamesLegend ); // member-names: after the fold clause h += peerRowClauses( p, /*compact=*/true ); // FOR-SLOTS + OWNER-CTOR-HOP: present-only, after mem= - h += presentOnlyClause( p.callerHopPresent, /*compact=*/true, rw::kForCompactCallerHopLegend, rw::kForCallerHopLegend ); // mf1-nx: present-only, after the fold clause + h += callerHopClause( p, /*compact=*/true ); // mf1-nx: present-only, after the fold 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 @@ -1070,7 +1082,7 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout h.append( bodyFoldLegend( p, /*compact=*/false ) ); // body-fold: present-only, after the slot clause h.append( presentOnlyClause( p.memberNamesPresent, /*compact=*/false, rw::kForCompactMemberNamesLegend, rw::kForMemberNamesLegend ) ); // member-names h.append( peerRowClauses( p, /*compact=*/false ) ); // FOR-SLOTS + OWNER-CTOR-HOP - h.append( presentOnlyClause( p.callerHopPresent, /*compact=*/false, rw::kForCompactCallerHopLegend, rw::kForCallerHopLegend ) ); // mf1-nx: present-only, after the fold clause + h.append( callerHopClause( p, /*compact=*/false ) ); // mf1-nx: present-only, after the fold clause if( p.tailLegend ) { h.append( rw::kForFileTailLegend ); // deep-tail: defines r= and the element (sigs-charge-exempt, serialize.h) @@ -2558,6 +2570,31 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR return out; } +// UPWARD-2HOP (forcallerhop.h forRankedCallerHopTargets): the ranked caller hop is a LATE splice, on the late splices' posture (the +// compact auto bundle, XML, the default regime, the whole answer measured, no hops cut marker), never beside the name-exact hop, +// only on an error-shaped task; it reads the rows EMITTED (r= order), so its clause's "r=1..3" holds. `held` is the +// named hop's render, returned unchanged when that hop rode. +struct RankedHopPosture +{ + bool bundleCompact = false; // the auto bundle on the compact route + bool sigsRendered = false; + bool surfaceOff = false; + bool hopsCutMarker = false; + bool namedHop = false; // the name-exact hop already rode + const char* routeTag = ""; +}; +inline rw::CallerHopRender rankedCallerHop( const RankedHopPosture& p, const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, + const std::vector& shownSigIds, std::string_view rootArg, rw::CallerHopRender held ) +{ + if( p.namedHop || !p.bundleCompact || !p.sigsRendered || p.surfaceOff || p.hopsCutMarker || rw::isNameExactRouteTag( p.routeTag ) || cfg.json + || cfg.tokenBudget > 0 || !rw::forCallerHopOn() || !rw::forRankedCallerHopTask( cfg.forTask ) ) + { + return held; + } + const std::vector ids = rw::forRankedCallerHopTargets( ing, g, shownSigIds, cfg.forTask ); + return ids.empty() ? held : rw::renderForCallerHop( ing, g, ids, rootArg, /*ranked=*/true ); +} + // mf1-nx (forcallerhop.h): the ONE function whose callers ride — `nameExactBodies` is the posture runForLensPass knows (the auto // bundle on the name-exact route); this adds the regime (XML, no explicit --token-budget, the A/B switch) and the target rule. inline std::vector forCallerHopIds( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, const std::vector& lensSurfaceIds, @@ -2652,6 +2689,7 @@ struct ForLensPassMode }; // the verb sections (verbs_navigate.h) come later in this translation unit; the --for block calls their CLI entry points std::optional runPath( const MainDispatch& d ); +std::optional runPathServed( const MainDispatch& d, std::span served, bool inForBlock ); std::optional runCallHierarchy( const MainDispatch& d ); std::optional runConnect( const MainDispatch& d ); @@ -2659,7 +2697,7 @@ std::optional runConnect( const MainDispatch& d ); // gets that verb's DEFAULT answer inside , just before , on the graph this run built // (no re-index). Every byte before it is the ordinary answer; nothing resolves → nothing is printed. XML only (the // --json / columnar dialects keep their single document; the verb is one call away there). -inline void appendSubjectGraphVerb( const MainDispatch& d ) +inline void appendSubjectGraphVerbBlock( const MainDispatch& d, std::span served ) { const rw::Config& cfg = d.cfg; if( cfg.json || cfg.columnar || cfg.candidates || d.multiRoot ) @@ -2694,7 +2732,7 @@ inline void appendSubjectGraphVerb( const MainDispatch& d ) { switch( args->form ) { - case rw::GraphSubjectForm::Path: (void)runPath( sd ); break; + case rw::GraphSubjectForm::Path: (void)runPathServed( sd, served, /*inForBlock=*/true ); break; case rw::GraphSubjectForm::Callers: (void)runCallHierarchy( sd ); break; case rw::GraphSubjectForm::Common: (void)runConnect( sd ); break; } @@ -2711,7 +2749,7 @@ inline void appendSubjectGraphVerb( const MainDispatch& d ) if( sub.legend == "compact" ) { DISCLOSE( Diagnostics::answerUnchanged, "the full legend is a correct superset of the compact one: only the cost grows", - "appendSubjectGraphVerb: no capture buffer — the graph verb's FULL legend is emitted" ); + "appendSubjectGraphVerbBlock: no capture buffer — the graph verb's FULL legend is emitted" ); } if( args->form == rw::GraphSubjectForm::Path ) // CR 4396503866: the open tag cannot read the rows, so a by-name hop into a test file goes unmarked { @@ -3572,7 +3610,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co const std::size_t bodyFoldLegendEmitted = bodyFoldLegend( headerParts, compactLegendOn ).size(); // mf1-nx: the caller hop's clause on the same contract — the hop rides on top of the complete answer (, the tail and // the bodies byte-identical with RIPWIRE_NO_CALLER_HOP=1), so its reading never costs a signature row - const std::size_t callerHopLegendEmitted = presentOnlyClause( headerParts.callerHopPresent, compactLegendOn, rw::kForCompactCallerHopLegend, rw::kForCallerHopLegend ).size() + const std::size_t callerHopLegendEmitted = callerHopClause( headerParts, compactLegendOn ).size() + presentOnlyClause( headerParts.epPresent, compactLegendOn, rw::kForCompactEpManifestLegend, rw::kForEpManifestLegend ).size(); // ep-manifest: same contract // attrib-multidef Q1: the prov="split" clause is a disclosure on the same contract — exempt, as e= is, so (rows and // its continuation's budget) is byte-identical with and without it (fordisclosurecheck #2: auto vs --signatures-only) @@ -3610,7 +3648,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co + ownerLegendExemptBytes( headerParts, /*compact=*/false ) + bodySlotLegend( headerParts, /*compact=*/false ).size() + bodyFoldLegend( headerParts, /*compact=*/false ).size() // body-fold - + presentOnlyClause( headerParts.callerHopPresent, /*compact=*/false, rw::kForCompactCallerHopLegend, rw::kForCallerHopLegend ).size() // mf1-nx + + callerHopClause( headerParts, /*compact=*/false ).size() // mf1-nx + presentOnlyClause( headerParts.epPresent, /*compact=*/false, rw::kForCompactEpManifestLegend, rw::kForEpManifestLegend ).size() // ep-manifest + ( headerParts.splitPresent ? rw::kForSplitClause.size() : 0u ); // attrib-multidef Q1 std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; @@ -4071,7 +4109,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { if( sigsPreRendered && !enrich.surfaceOff && !enrich.hopsCutMarker ) { - fold = rw::renderForBodyFold( rw::BodyFoldInputs{ ing, cfg.forTask, slot.served ? slotOwner.id : rw::kNoNode, lensRank, flRootArg, redactPtr, cfg.compress }, foldIds ); + fold = rw::renderForBodyFold( rw::BodyFoldInputs{ ing, cfg.forTask, slot.served ? slotOwner.id : rw::kNoNode, lensRank, flRootArg, redactPtr, cfg.compress, &g }, foldIds ); + if( fold.follows > 0 && !fold.xml.empty() ) + { + headerParts.foldFollowPresent = true; // WRAPPER-FOLLOW: the clause rides beside the attribute it defines + headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + } } if( fold.xml.empty() ) { @@ -4286,6 +4329,18 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); } } + // ── UPWARD-2HOP (forcallerhop.h forRankedCallerHopTargets): an error-shaped task's ranked caller hop, a late splice on the + // rows EMITTED (rankedCallerHop states the posture); its clause rides beside it + callerHop = rankedCallerHop( RankedHopPosture{ .bundleCompact = autoBundleMode && plan.compact, .sigsRendered = sigsPreRendered, + .surfaceOff = enrich.surfaceOff, .hopsCutMarker = enrich.hopsCutMarker, + .namedHop = headerParts.callerHopPresent, .routeTag = lr.routeTag }, + cfg, ing, g, shownSigIds, flRootArg, std::move( callerHop ) ); + if( !callerHop.xml.empty() && !headerParts.callerHopPresent ) + { + headerParts.callerHopPresent = true; // the ranked clause rides beside the element it defines + headerParts.callerHopRanked = true; + headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + } // 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). const ForLensRootFinish rootFinish{ @@ -4413,7 +4468,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { rw::emitChargedSection( stdout, graphSection, [ & ]{ packGraphBlock( stdout, ing, lensRank, g.outOff, g.outTargets ); } ); } - appendSubjectGraphVerb( d ); // FOR-ROUTE-DECLINED: "" unless the task is spelled as a graph question that resolves + appendSubjectGraphVerbBlock( d, servedAnswerBodies().bodies ); // FOR-ROUTE-DECLINED: "" unless the task is spelled as a graph question that resolves; the bodies this answer carries print no second copy in its path hop bodies rw::emitRaw( stdout, "" ); reportRedactions( stderr, redactCounts ); diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 82d5c644a..616d45647 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -4,6 +4,7 @@ #endif #include "infra/emit.h" // rw::emitTo — THE emitter (std::print, or std::format+fputs where is absent) +#include "hopbody.h" // PATH-HOP-BODY: the middle hops' bodies of a reached --path #include "typeuse.h" // L7: --impact of a type seeds its walk from the symbols that name it #include // std::string_view — the %.*s (precision, pointer) pair collapses to one view @@ -2476,7 +2477,182 @@ std::optional runExternalSurface( const MainDispatch& d ) return std::nullopt; } -std::optional runPath( const MainDispatch& d ) +// --defs=NAME[,NAME…] (lane D2 DEFS-LOOKUP): which definitions each name binds — src/defslookup.h lookupDefs, the one lookup +// --path's inline alternatives read too. One index load for up to kDefsNamesMax names; a single name that binds nothing gets +// the shared refusal (selectorrefuse.h), a missing name among several a defs="0" row whose next= is that refusal. +std::optional runDefs( const MainDispatch& d ) +{ + using namespace rw; + const Config& cfg = d.cfg; + const IngestResult& ing = d.ing; + if( cfg.defsSpec.empty() ) + { + return std::nullopt; + } + std::vector names; + for( std::string_view rest = cfg.defsSpec; ; ) + { + const std::size_t comma = rest.find( ',' ); + names.push_back( rest.substr( 0, comma ) ); + if( comma == std::string_view::npos ) + { + break; + } + rest.remove_prefix( comma + 1 ); + } + for( std::size_t i = 0; i < names.size(); ++i ) + { + if( names[i].empty() ) + { + rw::emitTo( stderr, "{}\n", rw::emptyListItemMessage( "--defs", i + 1, "--defs=parseArgs" ).c_str() ); + return 1; + } + } + if( names.size() > kDefsNamesMax ) + { + rw::refuseFlagValue( "--defs", "up to 16 symbol names, A[,B…]", cfg.defsSpec, "--defs=parseArgs,rankGraph" ); + return 1; + } + if( names.size() > 1 && cfg.pageOffset > 0 ) + { + rw::refuseFlagValue( "--offset", "a page of ONE --defs name (several names each print their first page)", std::to_string( cfg.pageOffset ), "--defs=parseArgs --offset=16" ); + return 1; + } + // the page: kDefRowsCap rows unless --limit names another size; --offset pages one name only + const std::size_t pageRows = cfg.pageLimit > 0 ? std::size_t( cfg.pageLimit ) : kDefRowsCap; + std::vector looks; + looks.reserve( names.size() ); + for( const std::string_view n : names ) + { + looks.push_back( lookupDefs( ing, d.g, n, names.size() == 1 ? std::size_t( std::max( cfg.pageOffset, 0 ) ) : 0, pageRows ) ); + } + if( names.size() == 1 && looks[0].matches.empty() && looks[0].dataDefs == 0 ) + { + return refuseNotFoundWithAnswer( ing, cfg, rw::NotFoundAnswer{ "defs", { { "of", std::string( names[0] ) } }, {}, + rw::notFoundNear( ing, names[0], notFoundGitRoot( ing, cfg ) ) }, + "ripwire: --defs symbol not found: ", names[0], "--defs=" ); // the shared refusal + } + const std::size_t offset = names.size() == 1 ? std::size_t( std::max( cfg.pageOffset, 0 ) ) : 0; // --offset pages one name's rows + const MemberOwners owners( ing ); + const bool singleRoot = ing.realPaths.empty() && cfg.roots.size() == 1; + std::vector esc; + bool anyProvenMiss = false, anyCut = false, anyMissing = false, anyBodyless = false, anyUnproven = false, anyData = false; + std::string body; + for( const DefLookup& L : looks ) + { + const std::size_t begin = std::min( offset, L.defs() ); + const std::size_t end = std::min( begin + pageRows, L.defs() ); + if( cfg.json ) + { + body += std::string( body.empty() ? "" : "," ) + "{\"q\":\"" + jsonStr( L.spec ) + "\",\"defs\":" + std::to_string( L.defs() ); + } + else + { + body += " 0 ) + { + anyUnproven = true; + body += cfg.json ? ",\"unproven_defs\":" + std::to_string( L.unprovenDefs ) : rw::unprovenDefsAttrXml( L.unprovenDefs ); + } + if( L.dataDefs > 0 ) + { + anyData = true; + body += cfg.json ? ",\"data_defs\":" + std::to_string( L.dataDefs ) : " data_defs=\"" + std::to_string( L.dataDefs ) + "\""; + } + std::string next; + if( L.matches.empty() && L.dataDefs == 0 ) + { + anyMissing = true; + next = nextFlag( "--defs=", L.spec ); // the single-name call answers with the shared refusal and its near= + } + else if( end < L.defs() || begin > 0 ) + { + anyCut = true; + body += cfg.json ? ",\"shown\":" + std::to_string( end - begin ) + ",\"offset\":" + std::to_string( begin ) + : " shown=\"" + std::to_string( end - begin ) + "\" offset=\"" + std::to_string( begin ) + "\""; + if( end < L.defs() ) + { + next = nextFlag( "--defs=", L.spec ) + " --offset=" + std::to_string( end ) + ( cfg.pageLimit > 0 ? " --limit=" + std::to_string( cfg.pageLimit ) : std::string() ); + } + } + if( cfg.json ) + { + body += next.empty() ? std::string() : ",\"next\":\"" + jsonStr( next ) + "\""; + body += ",\"rows\":["; + } + else + { + body += nextAttrXml( next ) + ">"; + } + for( std::size_t i = begin; i < end; ++i ) + { + const DefRow& r = L.rows[i]; + anyProvenMiss = anyProvenMiss || !r.selProven; + anyBodyless = anyBodyless || L.groups.bodyless[ r.group ]; + if( cfg.json ) + { + if( i > begin ) + { + body += ","; + } + defRowJson( body, ing, L, r, owners ); + } + else + { + defRowXml( body, ing, L, r, owners, /*compact=*/false ); + } + } + body += cfg.json ? "]}" : ""; + } + std::string rootAttr; + if( singleRoot ) + { + rootAttr = cfg.json ? ",\"root\":\"" + jsonStr( cfg.roots[0] ) + "\"" : " root=\"" + std::string( escapeXml( cfg.roots[0], esc ) ) + "\""; + } + if( cfg.json ) + { + rw::emitTo( stdout, "{{\"schema\":\"ripwire.defs/v1\"{},\"names\":[{}]}}\n", rootAttr, body ); + return 0; + } + // present-only legend: every clause rides only when an attribute it defines is on the page + std::string legend = "{}{}", legend, rw::rootRelPathsLegend( singleRoot ), rootAttr, body ); + return 0; +} + +// served: bodies the enclosing answer already carries (the --for graph-verb block passes its fold's and slot's); a middle hop among them +// prints no second copy (hopbody.h). The bare --path passes none. +std::optional runPathServed( const MainDispatch& d, std::span served, bool inForBlock ) { using namespace rw; const Config& cfg = d.cfg; @@ -2578,6 +2754,8 @@ std::optional runPath( const MainDispatch& d ) // ix2-slice: the sink-side frontier beside the forward gap rows (src/pathgaps.h pathNearSink), reachable="0" only; its name-key // ways in come from the same provider const rw::PathNearSink pthNear = path.empty() ? rw::pathNearSink( ing, g, dstDefs, pthGaps.cone, *pthValueIdx, pthBackHops->names() ) : rw::PathNearSink{}; + // ON-NAME-HANDLERS-V2: a poster from= whose posted message classes name to= handlers (src/pathgaps.h pathHandlerRule) + const rw::PathHandlerRule pthHandler = path.empty() ? rw::pathHandlerRule( ing, pthBackHops->names(), srcDefs, dstDefs ) : rw::PathHandlerRule{}; // TRACE-EP (src/pathpairs.h): a reached answer whose names have several definitions lists EVERY reaching definition pair // (up to kPathPairChains chains, the rest alt=), never one silently; a glob endpoint keeps its any-of-these meaning const rw::PathPairs pthPairs = path.empty() || srcGlob || dstGlob ? rw::PathPairs{} @@ -2613,21 +2791,43 @@ std::optional runPath( const MainDispatch& d ) const rw::PathTestTwin pthTwin = srcGlob || dstGlob ? rw::PathTestTwin{} : rw::pathTestTwins( ing, g, { srcDefs, srcN, pthBound.from }, { dstDefs, dstN, pthBound.to }, pthShown ); const std::vector pthOwners = rw::pathHopOwners( ing, path ); // PATH-OWNERS: n="Owner.name" per hop const bool pthHasOwner = pthMarks.owner; + // PATH-HOP-BODY (src/hopbody.h): a reached chain of 2 hops prints its middle hop's body (hedged when that hop is no proven + // call); a longer one names the first in next=. BODY-ON-ROUTE (src/pathgaps.h pathRouteBodies): a reachable=0 answer prints + // the bodies of the route it names (bridge rows, handler_rule readers), every one a hedge. ONE printer for both. + const std::string_view pthBodyRoot = pthSingleRoot ? std::string_view( cfg.roots[0] ) : std::string_view(); + const bool pthBodiesOn = !path.empty() && rw::hopBodiesOn(); + const rw::RouteBodiesXml pthHopBodyEl = pthBodiesOn ? rw::pathHopBodiesXml( ing, rw::pathMiddleHops( ing, g, pthShown, served ), pthBodyRoot ) : rw::RouteBodiesXml{}; + // GAIN GATE (fix1 fire table): never inside a --for graph-verb block — the --for answer serves its own bodies, and the one + // in-sample fire there (LARGE-DEV vscode-03, +7.7 KB) named nothing its question asked for + const rw::RouteBodiesXml pthRouteEl = path.empty() && !inForBlock && rw::hopBodiesOn() + ? rw::pathRouteBodies( ing, g, pthBridge, pthHandler, pthBodyRoot ) : rw::RouteBodiesXml{}; + const std::string& pthHopBodies = pthHopBodyEl.xml; + // D2 part 5 (defslookup.h): the other definitions of an endpoint whose pick is not provably unique, inline — after the bodies, so a + // definition a element printed is not listed again + std::vector pthBodyIds = pthHopBodyEl.ids; + pthBodyIds.insert( pthBodyIds.end(), pthRouteEl.ids.begin(), pthRouteEl.ids.end() ); + const rw::PathAlts pthAlts = rw::pathAltsOf( ing, g, srcN, dstN, srcGlob, dstGlob, pthBound.from ? srcUsed : rw::kNoNode, pthBound.to ? dstUsed : rw::kNoNode, pthPairs, pthBodyIds ); + std::string pthRootAttrs = rw::pathTestTwinRootAttrs( ing, rw::pathPairsRootAttrs( ing, pthPairs, pthSingleRoot, pthRootPrefix ).rootAttrs, pthTwin, pthSingleRoot, pthRootPrefix ); + const std::string pthHopNext = pthBodiesOn && pthRootAttrs.find( " next=\"" ) == std::string::npos ? rw::pathMiddleHopNext( ing, pthShown, pthBodyRoot ) : std::string(); + pthRootAttrs += pthHopNext.empty() ? std::string() : rw::nextAttrXml( pthHopNext ); rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + "graph holds none. {}{}{}{}-->{}", ( rw::defAltsLegend( pthAlts ) + rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + rw::viaLinkLegend( pthHasLink ) + rw::anchorMissLegend( pthHasMiss ) + rw::splitArmLegend( pthHasSplit ) + rw::pathUnboundLegend( !pthBound.from || !pthBound.to ) - + rw::memberOwnerLegend( pthHasOwner ) + rw::memberOwnerContainedLegend( pthMarks.ownerContained ) + std::string( rw::pathPairsLegend( pthPairs ) ) + rw::pathOrderLegend( pthPairs, pthTwin ) ).c_str(), + + rw::memberOwnerLegend( pthHasOwner ) + rw::memberOwnerContainedLegend( pthMarks.ownerContained ) + std::string( rw::pathPairsLegend( pthPairs ) ) + rw::pathOrderLegend( pthPairs, pthTwin ) + + std::string( rw::pathHopBodiesLegend( !pthHopBodies.empty() ) ) + std::string( rw::pathHopNextLegend( !pthHopNext.empty() ) ) + + std::string( rw::routeBodyHedgeLegend( pthHopBodyEl.hedged || pthRouteEl.hedged ) ) ).c_str(), ( std::string( rw::toValueRefsLegend( pthToValueRefs.count > 0 ) ) + rw::valueRefsDepthLegend( pthToValueRefs.depthCut.files > 0 ) ).c_str(), ( rw::pathGapsLegend( pthGaps.any(), !pthSinkCallers.empty() ) + rw::pathBridgeLegend( pthBridge ) - + rw::nearSinkLegend( pthNear ) + std::string( rw::pathGlobLegend( srcGlob || dstGlob ) ) ).c_str(), + + rw::nearSinkLegend( pthNear ) + std::string( rw::pathHandlerRuleLegend( pthHandler ) ) + std::string( rw::pathRouteBodiesLegend( !pthRouteEl.xml.empty() ) ) + + std::string( rw::pathGlobLegend( srcGlob || dstGlob ) ) ).c_str(), rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( pthSingleRoot ) ); rw::emitTo( stdout, " runPath( const MainDispatch& d ) ( srcDefs.size() > 1 || dstDefs.size() > 1 ) ? "; several defs share these names — qualify as file:name to pick one" : "" }, pthSinkCallers ) : rw::PathGapsXml{}; - rw::emitTo( stdout, "{}>{}{}{}", pthUnreached.rootAttrs, pthUnreached.rows, rw::pathBridgeXml( ing, g, pthBridge, pthSingleRoot, pthRootPrefix ), - rw::pathNearSinkXml( ing, pthNear, pthSingleRoot, pthRootPrefix ) ); + rw::emitTo( stdout, "{}>{}{}{}{}", pthUnreached.rootAttrs, pthUnreached.rows, rw::pathBridgeXml( ing, g, pthBridge, pthSingleRoot, pthRootPrefix ), + rw::pathNearSinkXml( ing, pthNear, pthSingleRoot, pthRootPrefix ), + path.empty() ? rw::pathHandlerRuleXml( ing, pthBackHops->names(), pthHandler, pthSingleRoot, pthRootPrefix ) : std::string() ); // FE-B via="name", #409 via="link"/anchor="missing", attrib-multidef prov="split", PATH-OWNERS n="Owner.name": one emitter, shared with MCP path_between rw::emitPathAnswerRows( stdout, ing, g, pthPairs, path, pthOwners, pthSingleRoot, pthRootPrefix ); // TRACE-EP: pairs or today's rows, shared with MCP + rw::emitRaw( stdout, pthHopBodies.c_str() ); // PATH-HOP-BODY: after the hop rows they explain ("" when none) + rw::emitRaw( stdout, pthRouteEl.xml.c_str() ); // BODY-ON-ROUTE: after the route rows they explain (reachable=0 only) + rw::emitRaw( stdout, pthAlts.xml.c_str() ); // D2 part 5: empty when both picks are provably unique rw::emitTo( stdout, "" ); return 0; } return std::nullopt; } +std::optional runPath( const MainDispatch& d ) +{ + return runPathServed( d, {}, /*inForBlock=*/false ); +} + std::optional runConnect( const MainDispatch& d ) { using namespace rw; diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index d9fb4cd98..cf572375d 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -739,7 +739,11 @@ ripwire.pack-top-n/v1 830 813 ripwire.expand/v1 280 265 ripwire.expand-file/v1 510 508 ripwire.for/v1 1100 1099 +ripwire.defs/v1 490 472 ' +# PINNED 2026-10-27 (lane/defs-lookup): ripwire.defs/v1 on this gate's --defs=zzqq9 probe. First pin 340 on 316 B, measured BEFORE +# the not-found readings existed; RE-PINNED 2026-10-28 to 490 on 472 B = the verb's purpose line (~290 B) plus the shared not-found +# answer's found=/of=/near= readings (compactlegendcheck UG required them). Present-only: none of the defs row readings ride here. pinFor() { printf '%s\n' "$PIN_TABLE" | awk -v schema="$1" '$1 == schema { printf "%s", $2; exit }' diff --git a/test/cppqualcheck.sh b/test/cppqualcheck.sh index 356a367aa..d3a3f4748 100755 --- a/test/cppqualcheck.sh +++ b/test/cppqualcheck.sh @@ -347,10 +347,13 @@ rrun US --uses=selectBaseline \ # source through the canonical helper to read its namespace and class chain — one new call site, same helper. # 37 -> 38 (lane/testonly-dead, D1): --dead-code's test-only name vetting (verbs_quality.h dropTestOnlySpelledInLibrary) reads each # non-test code file once through the canonical helper — one new call site, same helper, no new fopen/fread. +# 38 -> 39 2026-10-28 (train 41, lane/upward-2hop): the ranked caller hop's seed choice (forcallerhop.h ForRankedSources::of) +# reads a candidate's file once through the canonical helper to find its message literal — one new call site, same helper +# (measured: the one new row at src/forcallerhop.h:322 on the composed tree; main 12056c16 answers 38). rrun RW --uses=readWholeFile \ -&& { [ "$( cnt "$RW" )" = 38 ] \ - && ok "repo: --uses=readWholeFile count=38 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ - || no "repo: --uses=readWholeFile expected 38, got '$( cnt "$RW" )'"; } +&& { [ "$( cnt "$RW" )" = 39 ] \ + && ok "repo: --uses=readWholeFile count=39 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ + || no "repo: --uses=readWholeFile expected 39, got '$( cnt "$RW" )'"; } rrun WC --callers=writeTally \ && { [ "$( cnt "$WC" )" = 1 ] \ && ok "repo: --callers=writeTally count=1 (was 0 — both template call sites are in writeDocDriftPage)" \ diff --git a/test/defslookupcheck.sh b/test/defslookupcheck.sh new file mode 100755 index 000000000..d61475f25 --- /dev/null +++ b/test/defslookupcheck.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# defslookupcheck.sh — `--defs=NAME[,NAME…]` (src/defslookup.h rw::lookupDefs) and --path's inline (lane D2 DEFS-LOOKUP). +# +# RED-FIRST (recorded 2026-10-28 against the train-40 base 1318069a): every arm below fails there — the base has no --defs +# flag (18 FAIL) and its --path answer carries no . Three NEGATIVES pass vacuously there (no row is a JSON key, every +# sel= proven, P2 no ); each was proven able to fail by a mutant on f966224b: the data-Section filter dropped (6 FAIL), +# picksAlone always false (4 FAIL), defPickProvablyUnique false at one group (P2 FAIL). +# FIX ROUND 1 (2026-10-28, review F15): the picksAlone all_of -> any_of mutant ("binds this group AND MAYBE OTHERS" counts as +# proven) survived all 21 arms because no fixture held two same-named definitions in ONE file, so file:name always picked alone. +# Arm B2 (src/pair.ts: P.go, Q.go) fails that mutant, as do the (C) re-resolution of its sels. Mutants of the fix round are recorded +# at the end of this header. +# Arms: namesakes across files with a test copy and a JSON key (A), a C prototype folded with its definition (B), every +# printed sel= re-resolved by another verb to one definition (C), file:name / Owner.name spellings (D), zero found (E), over +# the 16-row cap with --offset paging (F), several names in one call (G), the JSON twin spelling every row as the XML does (J), +# --path alternatives present only when the pick is not provably unique (P). +# +# Usage: test/defslookupcheck.sh [BIN] (BIN falls back to RIPWIRE_BIN, then build/ripwire). Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/defslookupfix" +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; } +echo "defslookupcheck: BIN=$BIN FIX=$FIX" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +run(){ ( cd "$FIX" && "$BIN" "$FIX" --no-cache "$@" ); } +# the answer without its legend comments (the compact legend itself spells `` and ``) +body(){ perl -0pe 's///gs' "$1"; } + +echo "-- (A) namesakes: two source methods, one test copy, one JSON key" +run --defs=run --legend=full >"$TMP/a.xml" 2>"$TMP/a.err"; rc=$? +name="$( body "$TMP/a.xml" | grep -o ']*>' )" +if [ "$rc" -eq 0 ] && printf '%s' "$name" | grep -q 'defs="3"'; then ok "defs=3 (two classes + the test copy)"; else no "defs= not 3 (rc=$rc): $name"; fi +if printf '%s' "$name" | grep -q 'data_defs="1"'; then ok "the JSON key is data_defs=1"; else no "no data_defs=1: $name"; fi +grep -q 'lang="json"' "$TMP/a.xml" && no "a JSON key is listed as a definition" || ok "no row is a JSON key" +classes="$( grep -o 'class="[a-z]*"' "$TMP/a.xml" | tr '\n' ' ' )" +if [ "$classes" = 'class="source" class="source" class="test" ' ]; then ok "source rows before the test row"; else no "row order by class wrong: $classes"; fi +if xmllint --noout "$TMP/a.xml" 2>/dev/null; then ok "--legend=full answer is well-formed XML"; else no "--legend=full answer is not well-formed"; fi +grep -q 'sel_proven=' "$TMP/a.xml" && no "a sel= was not proven: $( grep -o ']*>' "$TMP/a.xml" )" || ok "every sel= proven" + +echo "-- (B) a C prototype and its definition are one group" +run --defs=work >"$TMP/b.xml" 2>/dev/null +if grep -q '' "$TMP/b.xml" && grep -q 'p="src/work.c:2"' "$TMP/b.xml"; then ok "work: one group, the bodied definition"; else no "work not one group: $( grep -o '"$TMP/b2.xml" 2>/dev/null +selsb2="$( grep -o 'sel="[^"]*"' "$TMP/b2.xml" | sed 's/sel="//; s/"$//' )" +if grep -q '' "$TMP/b2.xml" && [ "$( printf '%s\n' "$selsb2" | sort -u | wc -l | tr -d ' ' )" = 2 ] && ! printf '%s\n' "$selsb2" | grep -qx 'src/pair.ts:go' && ! grep -q 'sel_proven=' "$TMP/b2.xml"; then ok "go: 2 rows, 2 DIFFERENT proven sels (Owner-qualified), never the bare file:name that binds both"; else no "go: $( grep -o ']*>' "$TMP/b2.xml" )"; fi +run --callers=src/pair.ts:go >"$TMP/b2c.xml" 2>/dev/null +if grep -q '/dev/null )" + { printf '%s' "$out" | grep -q ']*>' | head -c 200 )"; } +done +if [ "$n" -ge 6 ] && [ "$bad" -eq 0 ]; then ok "$n sel= spellings each bind one definition group"; else no "$bad of $n sel= spellings did not bind one definition group"; fi + +echo "-- (D) spellings the shared resolver accepts" +run --defs=src/b.ts:run >"$TMP/d1.xml" 2>/dev/null +if grep -q 'defs="1"' "$TMP/d1.xml" && grep -q 'sel="src/b.ts:run"' "$TMP/d1.xml"; then ok "file:name -> one row, sel= as given"; else no "file:name: $( grep -o '"$TMP/d2.xml" 2>/dev/null +if grep -q 'defs="1"' "$TMP/d2.xml" && grep -q 'n="A.run"' "$TMP/d2.xml"; then ok "Owner.name -> one row"; else no "Owner.name: $( grep -o '"$TMP/e.xml" 2>"$TMP/e.err"; rc=$? +if [ "$rc" -eq 1 ] && grep -q 'found="0"' "$TMP/e.xml" && grep -q 'symbol not found' "$TMP/e.err"; then ok "rc=1, found=0, stderr refusal"; else no "zero found: rc=$rc $( head -c 300 "$TMP/e.xml" )"; fi + +echo "-- (F) over the cap: 16 rows, defs= the true count, --offset pages" +run --defs=many >"$TMP/f1.xml" 2>/dev/null +rows="$( body "$TMP/f1.xml" | grep -o ']*>' "$TMP/f1.xml" )"; fi +run --defs=many --offset=16 >"$TMP/f2.xml" 2>/dev/null +rows2="$( body "$TMP/f2.xml" | grep -o '"$TMP/f3.xml" 2>/dev/null +if grep -q 'defs="18" shown="5" offset="5" next="--defs=many --offset=10 --limit=5"' "$TMP/f3.xml" && [ "$( body "$TMP/f3.xml" | grep -o ']*>' "$TMP/f3.xml" | tail -1 )"; fi +run --defs=many,run --offset=3 >/dev/null 2>"$TMP/f4.err"; rc=$? +if [ "$rc" -eq 1 ] && grep -q -- '--offset' "$TMP/f4.err"; then ok "--offset beside several names refuses (never ignored)"; else no "--offset with several names: rc=$rc"; fi + +echo "-- (G) several names, one call" +run --defs=run,work,zzqdefsnone >"$TMP/g.xml" 2>/dev/null; rc=$? +if [ "$rc" -eq 0 ] && [ "$( body "$TMP/g.xml" | grep -o '' "$TMP/g.xml"; then ok "3 rows; the missing one defs=0 with its refusal as next="; else no "multi-name: rc=$rc $( grep -o ']*>' "$TMP/g.xml" | tr '\n' ' ' )"; fi + +echo "-- (J) the JSON twin spells every row as the XML does (checklist 41)" +run --defs=run --json >"$TMP/j.json" 2>/dev/null +python3 - "$TMP/j.json" "$TMP/a.xml" <<'PY' +import json, re, sys +j = json.load( open( sys.argv[1] ) )["names"][0]["rows"] +x = re.findall( r'"$TMP/p1.xml" 2>/dev/null +alts="$( grep -o '' "$TMP/p1.xml" )" +if printf '%s' "$alts" | grep -q '^' && [ "$( printf '%s' "$alts" | grep -o ' lists the 2 others (B.run, the test copy), not the bound one"; else no "P1: $alts"; fi +run --path=A.run,helperA >"$TMP/p2.xml" 2>/dev/null +if grep -q 'reachable="1"' "$TMP/p2.xml" && ! grep -q ' (0 bytes)"; else no "P2: $( head -c 400 "$TMP/p2.xml" )"; fi +run --path=many,helperA --legend=full >"$TMP/p3.xml" 2>/dev/null +if grep -q '' "$TMP/p3.xml" && xmllint --noout "$TMP/p3.xml" 2>/dev/null; then ok "P3: 17 others past the cap: alts_shown=16 defs=18 next=--defs=many --offset=16|17, well-formed"; else no "P3: $( grep -o ']*>' "$TMP/p3.xml" )"; fi +# F8: the continuation starts AFTER the rows already shown (it used to replay page 1): the 16 shown + the bound + the offset page = 18 distinct, no repeat +off="$( grep -o ']*next="--defs=many --offset=[0-9]*"' "$TMP/p3.xml" | sed 's/.*--offset=//; s/"$//' )" +run --defs=many --offset="${off:-0}" >"$TMP/p3n.xml" 2>/dev/null +shownp="$( grep -o '' "$TMP/p3.xml" | grep -o 'p="[^"]*"' | sort )" +boundp="$( grep -o ']*from_p="[^"]*"' "$TMP/p3.xml" | sed 's/.*from_p=/p=/' )" +nextp="$( body "$TMP/p3n.xml" | grep -o ']*' | grep -o 'p="[^"]*"' | sort )" +rep="$( printf '%s\n%s\n' "$shownp" "$nextp" | sort | uniq -d | grep -c . )" +cover="$( printf '%s\n%s\n%s\n' "$shownp" "$nextp" "$boundp" | sort -u | grep -c . )" +if [ -n "$off" ] && [ "$rep" = 0 ] && [ "$cover" = 18 ]; then ok "P3 next=: --defs=many --offset=$off adds only rows the did not show (shown 16 + bound + the offset page = 18 distinct, 0 repeats)"; else no "P3 next=: off=$off repeats=$rep distinct=$cover"; fi + +# CR 4408459903 (harvest 463): sel= is computed for the first 17 sorted rows only. wd/: 18 TS `wide` + a C definition + an untied header +# prototype (row 0, skipped) + the bound w05 (skipped): the 16th alternative used to come from row 17 with sel="" sel_proven="0". +run --path=wide,helperA >"$TMP/p3b.xml" 2>/dev/null +altsw="$( grep -o '' "$TMP/p3b.xml" )" +if printf '%s' "$altsw" | grep -q 'alts_shown=' && ! printf '%s' "$altsw" | grep -q 'sel=""' && ! printf '%s' "$altsw" | grep -q 'sel_proven' && [ "$( printf '%s' "$altsw" | grep -o ' row carries a sel= (none empty, none sel_proven=0); the rest pages via next="; else no "P3b: $( printf '%s' "$altsw" | grep -o ']*>\|]*sel=\"\"[^>]*>' | head -c 400 )"; fi +offw="$( printf '%s' "$altsw" | grep -o 'next="--defs=wide --offset=[0-9]*"' | sed 's/.*--offset=//; s/"$//' )" +run --defs=wide --offset="${offw:-0}" >"$TMP/p3bn.xml" 2>/dev/null +shownw="$( printf '%s' "$altsw" | grep -o 'p="[^"]*"' | sort )" +nextw="$( body "$TMP/p3bn.xml" | grep -o ']*' | grep -o 'p="[^"]*"' | sort )" +repw="$( printf '%s\n%s\n' "$shownw" "$nextw" | sort | uniq -d | grep -c . )" +if [ -n "$offw" ] && [ "$repw" = 0 ] && printf '%s' "$nextw" | grep -q 'wd/w17.ts'; then ok "P3b next=: --defs=wide --offset=$offw starts at the first unprinted row (w17), repeating none"; else no "P3b next=: off=$offw repeats=$repw"; fi + +echo "-- (P4) an untied C prototype is no second candidate: --path prints no (endpointDefinitionCount's rule)" +run --path=orphan,helperA >"$TMP/p4.xml" 2>/dev/null +if grep -q 'from_defs="2"' "$TMP/p4.xml" && ! grep -q ' from_defs=2 yet no (0 bytes)"; else no "P4: $( grep -o '' "$TMP/p4.xml" | head -c 300 ) $( grep -o 'from_defs="[0-9]*"' "$TMP/p4.xml" )"; fi +run --defs=orphan >"$TMP/p4d.xml" 2>/dev/null +if grep -q '' "$TMP/p4d.xml" && grep -q 'decl_only="1"' "$TMP/p4d.xml"; then ok "P4 control: --defs=orphan still lists both groups (the verb answering 'which definitions' is complete), the header decl_only"; else no "P4 control: $( grep -o '"$TMP/p4b.xml" 2>/dev/null +altsb="$( grep -o '' "$TMP/p4b.xml" )" +if grep -q 'from_defs="3"' "$TMP/p4b.xml" && [ "$( printf '%s' "$altsb" | grep -o ' lists the 2 definitions, never the prototype"; else no "P4b: $altsb"; fi + +echo "-- (P5) never re-lists a definition the answer prints as a pair endpoint; a tied contract still lists, decl_only rides" +run --path=ping,helperA --legend=full >"$TMP/p5.xml" 2>/dev/null +altsp="$( perl -0ne 'print "$1\n" while /]*>(.*?)<\/alts>/gs' "$TMP/p5.xml" )" +pairp="$( body "$TMP/p5.xml" | grep -o '"; else no "P5: pairs=[$pairp] dup=$dup alts=$altsp"; fi +if [ "$( printf '%s' "$altsp" | grep -o '/dev/null; then ok "P5: --legend=full reads decl_only=1 when a row carries it; well-formed"; else no "P5 legend / well-formedness"; fi +run --path=solo,helperA >"$TMP/p5d.xml" 2>/dev/null +if grep -q 'pairs_reaching="2"' "$TMP/p5d.xml" && ! grep -q ' nothing left to tell, no empty (0 bytes)"; else no "P5 solo: $( grep -o '"$TMP/p5b.xml" 2>/dev/null +if grep -q ' with no decl_only row has no decl_only clause (present-only legend)"; else no "P5 control: decl_only text on an answer with no such row"; fi +run --path=ping,helperA >"$TMP/p5c.xml" 2>/dev/null +if grep -q 'alts d decl_only=1: no body' "$TMP/p5c.xml"; then ok "P5: the default (compact) legend reads decl_only=1 on --path"; else no "P5: compact legend lacks the path decl_only reading: $( grep -o '' "$TMP/p5c.xml" | head -c 200 )"; fi + +echo "-- (P6) an row whose sel= is NOT proven says so: the legend never claims it binds one alone (F7)" +run --path=inner,helperA --legend=full >"$TMP/p6.xml" 2>/dev/null +unp="$( grep -o '' "$TMP/p6.xml" )" +if printf '%s' "$unp" | grep -q 'sel_proven="0"' && grep -q 'except on a sel_proven=0 row' "$TMP/p6.xml" && xmllint --noout "$TMP/p6.xml" 2>/dev/null; then ok "P6: a sel_proven=0 alts row -> --legend=full carries the 'except on a sel_proven=0 row' reading; well-formed"; else no "P6: alts=$unp"; fi +run --path=inner,helperA >"$TMP/p6c.xml" 2>/dev/null +if grep -q 'alts d sel_proven=0: no spelling tried' "$TMP/p6c.xml"; then ok "P6: the default (compact) legend reads sel_proven=0 on --path"; else no "P6: compact legend lacks the path sel_proven reading"; fi +if ! grep -q 'sel_proven' "$TMP/p5.xml" && ! grep -q 'sel_proven' "$TMP/p5c.xml"; then ok "P6 control: an whose sels are all proven carries no sel_proven text (present-only, both legends)"; else no "P6 control: sel_proven text on an all-proven "; fi + +[ "$fail" -eq 0 ] && echo "defslookupcheck: OK" || echo "defslookupcheck: FAILURES above" +exit "$fail" diff --git a/test/defslookupfix/config.json b/test/defslookupfix/config.json new file mode 100644 index 000000000..443040900 --- /dev/null +++ b/test/defslookupfix/config.json @@ -0,0 +1 @@ +{ "run": 1 } diff --git a/test/defslookupfix/gen/m01.ts b/test/defslookupfix/gen/m01.ts new file mode 100644 index 000000000..1a74764fb --- /dev/null +++ b/test/defslookupfix/gen/m01.ts @@ -0,0 +1 @@ +export function many(): number { return 1 } diff --git a/test/defslookupfix/gen/m02.ts b/test/defslookupfix/gen/m02.ts new file mode 100644 index 000000000..eee05c983 --- /dev/null +++ b/test/defslookupfix/gen/m02.ts @@ -0,0 +1 @@ +export function many(): number { return 2 } diff --git a/test/defslookupfix/gen/m03.ts b/test/defslookupfix/gen/m03.ts new file mode 100644 index 000000000..d8637c8ed --- /dev/null +++ b/test/defslookupfix/gen/m03.ts @@ -0,0 +1 @@ +export function many(): number { return 3 } diff --git a/test/defslookupfix/gen/m04.ts b/test/defslookupfix/gen/m04.ts new file mode 100644 index 000000000..ff55ed99f --- /dev/null +++ b/test/defslookupfix/gen/m04.ts @@ -0,0 +1 @@ +export function many(): number { return 4 } diff --git a/test/defslookupfix/gen/m05.ts b/test/defslookupfix/gen/m05.ts new file mode 100644 index 000000000..38f4bb42e --- /dev/null +++ b/test/defslookupfix/gen/m05.ts @@ -0,0 +1 @@ +export function many(): number { return 5 } diff --git a/test/defslookupfix/gen/m06.ts b/test/defslookupfix/gen/m06.ts new file mode 100644 index 000000000..0bbb5a9e2 --- /dev/null +++ b/test/defslookupfix/gen/m06.ts @@ -0,0 +1 @@ +export function many(): number { return 6 } diff --git a/test/defslookupfix/gen/m07.ts b/test/defslookupfix/gen/m07.ts new file mode 100644 index 000000000..7d13ad717 --- /dev/null +++ b/test/defslookupfix/gen/m07.ts @@ -0,0 +1 @@ +export function many(): number { return 7 } diff --git a/test/defslookupfix/gen/m08.ts b/test/defslookupfix/gen/m08.ts new file mode 100644 index 000000000..17433c2f3 --- /dev/null +++ b/test/defslookupfix/gen/m08.ts @@ -0,0 +1 @@ +export function many(): number { return 8 } diff --git a/test/defslookupfix/gen/m09.ts b/test/defslookupfix/gen/m09.ts new file mode 100644 index 000000000..b0cf7c184 --- /dev/null +++ b/test/defslookupfix/gen/m09.ts @@ -0,0 +1 @@ +export function many(): number { return 9 } diff --git a/test/defslookupfix/gen/m10.ts b/test/defslookupfix/gen/m10.ts new file mode 100644 index 000000000..fa80a9d31 --- /dev/null +++ b/test/defslookupfix/gen/m10.ts @@ -0,0 +1 @@ +export function many(): number { return 10 } diff --git a/test/defslookupfix/gen/m11.ts b/test/defslookupfix/gen/m11.ts new file mode 100644 index 000000000..e99dcf4e0 --- /dev/null +++ b/test/defslookupfix/gen/m11.ts @@ -0,0 +1 @@ +export function many(): number { return 11 } diff --git a/test/defslookupfix/gen/m12.ts b/test/defslookupfix/gen/m12.ts new file mode 100644 index 000000000..98b3f60c3 --- /dev/null +++ b/test/defslookupfix/gen/m12.ts @@ -0,0 +1 @@ +export function many(): number { return 12 } diff --git a/test/defslookupfix/gen/m13.ts b/test/defslookupfix/gen/m13.ts new file mode 100644 index 000000000..513f33b68 --- /dev/null +++ b/test/defslookupfix/gen/m13.ts @@ -0,0 +1 @@ +export function many(): number { return 13 } diff --git a/test/defslookupfix/gen/m14.ts b/test/defslookupfix/gen/m14.ts new file mode 100644 index 000000000..4c3c0d613 --- /dev/null +++ b/test/defslookupfix/gen/m14.ts @@ -0,0 +1 @@ +export function many(): number { return 14 } diff --git a/test/defslookupfix/gen/m15.ts b/test/defslookupfix/gen/m15.ts new file mode 100644 index 000000000..35872c983 --- /dev/null +++ b/test/defslookupfix/gen/m15.ts @@ -0,0 +1 @@ +export function many(): number { return 15 } diff --git a/test/defslookupfix/gen/m16.ts b/test/defslookupfix/gen/m16.ts new file mode 100644 index 000000000..ac4450ce4 --- /dev/null +++ b/test/defslookupfix/gen/m16.ts @@ -0,0 +1 @@ +export function many(): number { return 16 } diff --git a/test/defslookupfix/gen/m17.ts b/test/defslookupfix/gen/m17.ts new file mode 100644 index 000000000..aca02c730 --- /dev/null +++ b/test/defslookupfix/gen/m17.ts @@ -0,0 +1 @@ +export function many(): number { return 17 } diff --git a/test/defslookupfix/gen/m18.ts b/test/defslookupfix/gen/m18.ts new file mode 100644 index 000000000..006c53c08 --- /dev/null +++ b/test/defslookupfix/gen/m18.ts @@ -0,0 +1 @@ +export function many(): number { return 18 } diff --git a/test/defslookupfix/proto/orphan.c b/test/defslookupfix/proto/orphan.c new file mode 100644 index 000000000..82397506d --- /dev/null +++ b/test/defslookupfix/proto/orphan.c @@ -0,0 +1 @@ +void orphan( int x ) { x = 1; } diff --git a/test/defslookupfix/proto/orphan.h b/test/defslookupfix/proto/orphan.h new file mode 100644 index 000000000..0cde946a8 --- /dev/null +++ b/test/defslookupfix/proto/orphan.h @@ -0,0 +1 @@ +void orphan( int x ); diff --git a/test/defslookupfix/proto/twin.h b/test/defslookupfix/proto/twin.h new file mode 100644 index 000000000..262f9edbb --- /dev/null +++ b/test/defslookupfix/proto/twin.h @@ -0,0 +1 @@ +void twin( int x ); diff --git a/test/defslookupfix/proto/twin_a.c b/test/defslookupfix/proto/twin_a.c new file mode 100644 index 000000000..5d2ee93f3 --- /dev/null +++ b/test/defslookupfix/proto/twin_a.c @@ -0,0 +1 @@ +void twin( int x ) { x = 1; } diff --git a/test/defslookupfix/proto/twin_b.c b/test/defslookupfix/proto/twin_b.c new file mode 100644 index 000000000..666f2b907 --- /dev/null +++ b/test/defslookupfix/proto/twin_b.c @@ -0,0 +1 @@ +void twin( int x ) { x = 2; } diff --git a/test/defslookupfix/src/a.ts b/test/defslookupfix/src/a.ts new file mode 100644 index 000000000..155594fae --- /dev/null +++ b/test/defslookupfix/src/a.ts @@ -0,0 +1,5 @@ +export class A { + run(): number { return helperA() } +} +export function helperA(): number { return 1 } +export function callsA(a: A): number { return a.run() } diff --git a/test/defslookupfix/src/b.ts b/test/defslookupfix/src/b.ts new file mode 100644 index 000000000..a64209691 --- /dev/null +++ b/test/defslookupfix/src/b.ts @@ -0,0 +1,3 @@ +export class B { + run(): number { return 2 } +} diff --git a/test/defslookupfix/src/inner.js b/test/defslookupfix/src/inner.js new file mode 100644 index 000000000..06db3430b --- /dev/null +++ b/test/defslookupfix/src/inner.js @@ -0,0 +1 @@ +function one() { function inner() { return 1 } return inner() } function two() { function inner() { return 2 } return inner() } diff --git a/test/defslookupfix/src/pair.ts b/test/defslookupfix/src/pair.ts new file mode 100644 index 000000000..3e2891e96 --- /dev/null +++ b/test/defslookupfix/src/pair.ts @@ -0,0 +1,6 @@ +export class P { + go(): number { return 1 } +} +export class Q { + go(): number { return 2 } +} diff --git a/test/defslookupfix/src/ping1.ts b/test/defslookupfix/src/ping1.ts new file mode 100644 index 000000000..738f2f48c --- /dev/null +++ b/test/defslookupfix/src/ping1.ts @@ -0,0 +1,4 @@ +import { helperA } from "./a" +export class PingOne { + ping(): number { return helperA() } +} diff --git a/test/defslookupfix/src/ping2.ts b/test/defslookupfix/src/ping2.ts new file mode 100644 index 000000000..5f06e8534 --- /dev/null +++ b/test/defslookupfix/src/ping2.ts @@ -0,0 +1,4 @@ +import { helperA } from "./a" +export class PingTwo { + ping(): number { return helperA() + 1 } +} diff --git a/test/defslookupfix/src/ping3.ts b/test/defslookupfix/src/ping3.ts new file mode 100644 index 000000000..ff79dad34 --- /dev/null +++ b/test/defslookupfix/src/ping3.ts @@ -0,0 +1,4 @@ +export interface Pinger { ping(): number } +export class PingThree { + ping(): number { return 3 } +} diff --git a/test/defslookupfix/src/solo1.ts b/test/defslookupfix/src/solo1.ts new file mode 100644 index 000000000..c2f0d8e3a --- /dev/null +++ b/test/defslookupfix/src/solo1.ts @@ -0,0 +1,4 @@ +import { helperA } from "./a" +export class SoloOne { + solo(): number { return helperA() } +} diff --git a/test/defslookupfix/src/solo2.ts b/test/defslookupfix/src/solo2.ts new file mode 100644 index 000000000..bd4174223 --- /dev/null +++ b/test/defslookupfix/src/solo2.ts @@ -0,0 +1,4 @@ +import { helperA } from "./a" +export class SoloTwo { + solo(): number { return helperA() + 2 } +} diff --git a/test/defslookupfix/src/work.c b/test/defslookupfix/src/work.c new file mode 100644 index 000000000..0f93fe2f4 --- /dev/null +++ b/test/defslookupfix/src/work.c @@ -0,0 +1,2 @@ +#include "work.h" +void work( int x ) { (void)x; } diff --git a/test/defslookupfix/src/work.h b/test/defslookupfix/src/work.h new file mode 100644 index 000000000..39bcd9166 --- /dev/null +++ b/test/defslookupfix/src/work.h @@ -0,0 +1 @@ +void work( int x ); diff --git a/test/defslookupfix/test/run.test.ts b/test/defslookupfix/test/run.test.ts new file mode 100644 index 000000000..7efb53c94 --- /dev/null +++ b/test/defslookupfix/test/run.test.ts @@ -0,0 +1 @@ +export function run(): number { return 3 } diff --git a/test/defslookupfix/wd/a_wide.h b/test/defslookupfix/wd/a_wide.h new file mode 100644 index 000000000..c9a849951 --- /dev/null +++ b/test/defslookupfix/wd/a_wide.h @@ -0,0 +1 @@ +void wide( int x ); diff --git a/test/defslookupfix/wd/w01.ts b/test/defslookupfix/wd/w01.ts new file mode 100644 index 000000000..8d4aa1429 --- /dev/null +++ b/test/defslookupfix/wd/w01.ts @@ -0,0 +1 @@ +export function wide(): number { return 1 } diff --git a/test/defslookupfix/wd/w02.ts b/test/defslookupfix/wd/w02.ts new file mode 100644 index 000000000..ea86ef385 --- /dev/null +++ b/test/defslookupfix/wd/w02.ts @@ -0,0 +1 @@ +export function wide(): number { return 2 } diff --git a/test/defslookupfix/wd/w03.ts b/test/defslookupfix/wd/w03.ts new file mode 100644 index 000000000..848994ada --- /dev/null +++ b/test/defslookupfix/wd/w03.ts @@ -0,0 +1 @@ +export function wide(): number { return 3 } diff --git a/test/defslookupfix/wd/w04.ts b/test/defslookupfix/wd/w04.ts new file mode 100644 index 000000000..ed9e16b3f --- /dev/null +++ b/test/defslookupfix/wd/w04.ts @@ -0,0 +1 @@ +export function wide(): number { return 4 } diff --git a/test/defslookupfix/wd/w05.ts b/test/defslookupfix/wd/w05.ts new file mode 100644 index 000000000..c21e8c9d2 --- /dev/null +++ b/test/defslookupfix/wd/w05.ts @@ -0,0 +1,2 @@ +import { helperA } from '../src/a' +export function wide(): number { return helperA() } diff --git a/test/defslookupfix/wd/w06.ts b/test/defslookupfix/wd/w06.ts new file mode 100644 index 000000000..b827616b7 --- /dev/null +++ b/test/defslookupfix/wd/w06.ts @@ -0,0 +1 @@ +export function wide(): number { return 6 } diff --git a/test/defslookupfix/wd/w07.ts b/test/defslookupfix/wd/w07.ts new file mode 100644 index 000000000..1c2aeff9f --- /dev/null +++ b/test/defslookupfix/wd/w07.ts @@ -0,0 +1 @@ +export function wide(): number { return 7 } diff --git a/test/defslookupfix/wd/w08.ts b/test/defslookupfix/wd/w08.ts new file mode 100644 index 000000000..411c77c53 --- /dev/null +++ b/test/defslookupfix/wd/w08.ts @@ -0,0 +1 @@ +export function wide(): number { return 8 } diff --git a/test/defslookupfix/wd/w09.ts b/test/defslookupfix/wd/w09.ts new file mode 100644 index 000000000..3e491b391 --- /dev/null +++ b/test/defslookupfix/wd/w09.ts @@ -0,0 +1 @@ +export function wide(): number { return 9 } diff --git a/test/defslookupfix/wd/w10.ts b/test/defslookupfix/wd/w10.ts new file mode 100644 index 000000000..f96d953f3 --- /dev/null +++ b/test/defslookupfix/wd/w10.ts @@ -0,0 +1 @@ +export function wide(): number { return 10 } diff --git a/test/defslookupfix/wd/w11.ts b/test/defslookupfix/wd/w11.ts new file mode 100644 index 000000000..7d3cf8b67 --- /dev/null +++ b/test/defslookupfix/wd/w11.ts @@ -0,0 +1 @@ +export function wide(): number { return 11 } diff --git a/test/defslookupfix/wd/w12.ts b/test/defslookupfix/wd/w12.ts new file mode 100644 index 000000000..c6381c088 --- /dev/null +++ b/test/defslookupfix/wd/w12.ts @@ -0,0 +1 @@ +export function wide(): number { return 12 } diff --git a/test/defslookupfix/wd/w13.ts b/test/defslookupfix/wd/w13.ts new file mode 100644 index 000000000..5721b9cf8 --- /dev/null +++ b/test/defslookupfix/wd/w13.ts @@ -0,0 +1 @@ +export function wide(): number { return 13 } diff --git a/test/defslookupfix/wd/w14.ts b/test/defslookupfix/wd/w14.ts new file mode 100644 index 000000000..c22ad8de1 --- /dev/null +++ b/test/defslookupfix/wd/w14.ts @@ -0,0 +1 @@ +export function wide(): number { return 14 } diff --git a/test/defslookupfix/wd/w15.ts b/test/defslookupfix/wd/w15.ts new file mode 100644 index 000000000..0c53a1b5a --- /dev/null +++ b/test/defslookupfix/wd/w15.ts @@ -0,0 +1 @@ +export function wide(): number { return 15 } diff --git a/test/defslookupfix/wd/w16.ts b/test/defslookupfix/wd/w16.ts new file mode 100644 index 000000000..1dc74abb5 --- /dev/null +++ b/test/defslookupfix/wd/w16.ts @@ -0,0 +1 @@ +export function wide(): number { return 16 } diff --git a/test/defslookupfix/wd/w17.ts b/test/defslookupfix/wd/w17.ts new file mode 100644 index 000000000..41aa556e0 --- /dev/null +++ b/test/defslookupfix/wd/w17.ts @@ -0,0 +1 @@ +export function wide(): number { return 17 } diff --git a/test/defslookupfix/wd/w18.ts b/test/defslookupfix/wd/w18.ts new file mode 100644 index 000000000..5f9aba332 --- /dev/null +++ b/test/defslookupfix/wd/w18.ts @@ -0,0 +1 @@ +export function wide(): number { return 18 } diff --git a/test/defslookupfix/wd/wide.c b/test/defslookupfix/wd/wide.c new file mode 100644 index 000000000..ecb7427a6 --- /dev/null +++ b/test/defslookupfix/wd/wide.c @@ -0,0 +1 @@ +void wide( int x ) { x = 1; } diff --git a/test/fnslotcheck.sh b/test/fnslotcheck.sh index 66bdb2de9..0da333f51 100755 --- a/test/fnslotcheck.sh +++ b/test/fnslotcheck.sh @@ -31,6 +31,51 @@ # T1 [P] a named MAP type's keyed literal `Table{OpPut: putTab}` is no field slot (F-5); T1b [G] a struct's still is # N3b [G] an unnamed map literal `map[Op]func(){OpGet: getMap}` is no field slot (F-6; pins goValueSlot's ty.empty() guard) # Q1 [P] a slot_by= whose only store is in test code (`assertMatch( t, MatchV1 )`) is not printed on a non-test row (F-7) +# part 2 (lane fn-slot-2, Rust; fixture test/fnslotrust/, its own root; model.h kVrSlotLangs Rust row: ParenFieldCall, ModuleType): +# R1 [P] a registrar path call `CallEval::new( 1, eval_block )` → `Self { stack, eval }` → `(self.eval)( … )` in CallEval::run: +# called_by="run" via="slot" (typed: `Self` is the impl type on both sides), cands=4 +# R2 [P] a turbofish value `eval_block_generic::` is the function; R3 [P] a struct expression `CallEval { eval: direct_eval }` +# R4 [P] `Box::new( hook_target )` in `Hooks { eval: … }` lands in Hooks.eval (no Box::new argument row); R4b [G] no row into Box::new +# R5 [P] --callees: `(self.eval)( … )` prints the typed slot and its 4 targets +# R6 [P] a path call to a METHOD `Reg::set( r, path_reg_target )` (self = position 0) feeds `self.h = h`, called in Reg::go +# N8 [G] a local closure of a stored name (`let shadowed = |x| …; CallEval::new( 3, shadowed )`) is no function value: no row +# N9 [P] a same-named struct of ANOTHER crate (crates/twin CallEval) is another slot: eval_block's cands stays 4, twin_run targets 1 +# N10 [G] (N10a [P]) an enum variant `Msg::Fire { eval: variant_target }` is no struct field slot: no called_by= +# N11 [G] a method-call registrar `r.set( method_reg_target )` is a floor: no row +# N12 [P] `x.cb = loose_target` and `(y.cb)( … )` (types not read) join by the field name only: via="slot-name", --for `@~` +# N13 [P] a store in another file of the crate keys the crate's own CallEval (crate tier), typed: via="slot" +# R7 [G] a registrar's owner type decides it: `Hooks::new` (stores into Hooks.eval) never feeds CallEval.eval, nor the reverse +# X2 [G] a Go field of the same name (gox Box.eval) never joins a Rust slot (family guard), nor the reverse +# F3 [P] --for: slot_fields= per struct (eval=4@CallEval::run, the twin's eval=1@CallEval::twin_run), slot_by= on eval_block +# fix round 1 (review rv-fn-slot-2 F-1..F-4; fixture test/fnslotrust2/, its own root: crates app, lib, qual): +# E1 [P] `use extcrate::Hook; Hook { cb: f }` is an out-of-tree struct: never the in-tree Hook.cb (F-1) +# E2 [P] `use tokio::spawn; spawn( f )` is an out-of-tree registrar: never the in-tree `spawn` that stores into Hook.cb (F-1) +# E3 [P] `std::thread::spawn( f )` is std's: never the in-tree `spawn` (F-1); E4 [G] the in-tree Hook keeps its own store +# Q1 [P] `crate::a::Cfg { cb: f }` written in c.rs (which declares its own Cfg) is a.rs's Cfg: called_by="go" (F-2) +# Q1b [G] the twin: c.rs's bare `Cfg { cb: f }` is its own Cfg (called_by="local_go") +# Q2 [P] `crate::other::register( f )` beside a local storing `register` is other.rs's (stores nothing): no called_by= (F-2) +# Q3 [P] `S::new( crate::other::target )` is other.rs's target, never here.rs's namesake (F-2); Q3b [G] the bare twin +# `register( local_t )` still lands in S.f +# Q4 [P] `crate::p::Amb { f }` with two Amb in the crate is p.rs's (the path names the module file): called_by="go_p", typed +# N14 [G] a BARE `Amb { f }` with two Amb in the crate (neither in the writing file) is never typed: via="slot-name" — kills +# the source mutant "nearestOf guesses within an ambiguous tier" (review MG1) +# N15 [G] `(self.inner.f)( … )` is never typed as Self's field: in_t keeps its caller, out_t (Outer.f) is never via="slot" — +# kills the source mutant "slotCallAt types any self.… chain" (review MG2) +# N16 [G] a bare CALLEE the enclosing function binds (`fn run( spawn: fn( fn() ) ) { spawn( worker ) }`, `let spawn = |f| f(); spawn( worker )`) +# is never the in-tree registrar of that name: the argument row prints, no called_by= (CR 4408459928, harvest 463; RED on af8fc25a) +# W1 [P] `std::sync::Arc::new( f )` is the wrapper by its LAST path segment: the value lands in Dyn.g (F-4) +# fix round 2 (second lens rv-fn-slot-2-lens L-1, L-2; fixture crates selfmod, ga, gx): +# S1 [P] `use crate::workspace_settings_module::{self, Other}` (module path past SSO) binds the module: `workspace_settings_module::Ws { … }` +# is that file's Ws, typed (L-1: the `self` branch kept a view into the string it then reassigned) +# S2 [P] `use crate::{a::{self}}` binds `a`: `a::Sa { … }` is a.rs's Sa, never the writing file's own Sa (L-1 short module) +# X1 [P] a bare `Gh { … }` in a file with an OUT-OF-TREE glob (`use extcrate::*`) is a guess among crate namesakes: via="slot-name", +# never typed (L-2 tier 1) +# X2 [P] a bare `Gw { … }` in a file that `use`s the defining crate by name AND has an out-of-tree glob: the same hedge (L-2 tier 2) +# X3 [G] a local `Gm` shadows the glob (tier 0: typed); X4 [G] no glob: the crate namesake stays typed; X5 [G] an in-tree glob +# (`use crate::hk::*`) and X6 [G] a glob led by an in-tree crate (`use ga::*`) are not out-of-tree: typed +# X7 [G] a glob led by a `mod` the file declares, X8 [G] `use super::*`, X9 [G] `use self::Kind::*` are in-tree: typed; +# X10 [G] tier 2 without a glob (`use ga::Gother` only) stays typed; X11 [P] a glob inside a brace group +# (`use extcrate::{Alpha, prelude::*}`) is an out-of-tree glob too: hedged # # Usage: bash test/fnslotcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) set -u @@ -139,5 +184,124 @@ o="$( run '--for=how does tfire call tdrawImpl through tdraw' )" has "T2a the operator~ caller is printed" "$( row "$o" tdrawImpl tilde/t.cpp )" 'operator~' hasnt "T2 a typed caller named operator~ never raises the field-name-only clause" "$o" 'matches the field name only' hasnt "T2b nor the long form of it" "$o" 'a ~ before a slot_by= name' + +FIX="$ROOT/test/fnslotrust" +[ -d "$FIX" ] || { echo " FAIL fixture dir missing: $FIX"; exit 2; } +o="$( run --callers=eval_block )" +has "R1 a registrar path call lands in the typed slot its parameter is stored in" "$o" 'into="CallEval::new#arg1" called_by="run" called_at="src/engine/eval.rs:14" via="slot" cands="4"' +o="$( run --callers=eval_block_generic )" +has "R2 a turbofish value is the function" "$o" 'into="CallEval::new#arg1" called_by="run"' +o="$( run --callers=direct_eval )" +has "R3 a struct expression's field value" "$o" 'into="CallEval.eval" called_by="run" called_at="src/engine/eval.rs:14" via="slot"' +o="$( run --callers=hook_target )" +has "R4 Box::new( f ) lands in the field" "$o" 'into="Hooks.eval" called_by="fire" called_at="src/other/hooks.rs:11" via="slot"' +hasnt "R4b no argument row into the Box::new wrapper" "$o" 'into="Box::new' +o="$( run --callees=CallEval::run )" +has "R5 the (self.eval)( … ) call prints its typed slot and targets" "$o" 'through="self.eval" slot="CallEval.eval" via="slot" targets="4"' +o="$( run --callers=path_reg_target )" +has "R6 a path call to a method counts self as position 0" "$o" 'into="Reg::set#arg1" called_by="go" called_at="crates/twin/src/lib.rs:60" via="slot"' +o="$( run --callers=shadowed )" +hasnt "N8 a local closure of a stored name is no function value" "$o" '' +hasnt "Q3c never the writing file's own namesake" "$o" '' +has "Q3b the bare registrar twin still lands in S.f" "$o" '' +o="$( run --callers=amb_t )" +has "Q4 crate::p::Amb is p.rs's Amb, typed" "$o" 'into="Amb.f" called_by="go_p" called_at="crates/qual/src/p.rs:2" via="slot"' +o="$( run --callers=amb_bare_t )" +has "N14a the bare store joins by the field name" "$o" 'via="slot-name"' +hasnt "N14 an ambiguous crate tier is never typed" "$o" 'via="slot"' +o="$( run --callers=in_t )" +has "N15a (self.inner.f)( … ) keeps Inner.f's caller" "$o" 'into="Inner.f" called_by="go_inner"' +o="$( run --callers=out_t )" +hasnt "N15 (self.inner.f)( … ) is never typed as Self's field" "$o" 'via="slot"' +o="$( run --callers=arc_t )" +has "W1 std::sync::Arc::new( f ) hands the value to the field" "$o" 'into="Dyn.g" called_by="go" called_at="crates/qual/src/wrap.rs:2" via="slot"' +hasnt "W1b no argument row into the Arc::new wrapper" "$o" 'into="Arc::new' +o="$( run --callers=self_long_t )" +has "S1 {self} of a long module path binds the module" "$o" 'into="Ws.scb" called_by="fire_ws" called_at="crates/selfmod/src/workspace_settings_module.rs:2" via="slot"' +hasnt "S1b never the writing file's own Ws" "$o" 'fire_ws_local' +o="$( run --callers=self_short_t )" +has "S2 {self} of a short module path binds the module" "$o" 'into="Sa.acb" called_by="fire_sa" called_at="crates/selfmod/src/a.rs:2" via="slot"' +hasnt "S2b never the writing file's own Sa" "$o" 'fire_sa_local' +o="$( run --callers=glob_t1 )" +has "X1a the store still prints, joined by the field name" "$o" 'into="Gh.hcb" called_by="fire_gh"' +has "X1 an out-of-tree glob hedges the crate tier: slot-name" "$o" 'via="slot-name"' +hasnt "X1b never typed" "$o" 'via="slot"' +o="$( run --callers=glob_t2 )" +has "X2a the store still prints, joined by the field name" "$o" 'into="Gw.wcb" called_by="fire_ga"' +has "X2 an out-of-tree glob hedges the used-crate tier: slot-name" "$o" 'via="slot-name"' +hasnt "X2b never typed" "$o" 'via="slot"' +o="$( run --callers=glob_loc_t )" +has "X3 a local struct shadows the glob: typed" "$o" 'into="Gm.mcb" called_by="fire_gm_local" called_at="crates/gx/src/g3.rs:3" via="slot"' +o="$( run --callers=plain_t )" +has "X4 no glob: the crate namesake stays typed" "$o" 'into="Gh.hcb" called_by="fire_gh" called_at="crates/gx/src/hk.rs:2" via="slot"' +o="$( run --callers=intree_glob_t )" +has "X5 an in-tree glob is no out-of-tree glob: typed" "$o" 'into="Gh.hcb" called_by="fire_gh" called_at="crates/gx/src/hk.rs:2" via="slot"' +o="$( run --callers=crate_glob_t )" +has "X6 a glob led by an in-tree crate: typed" "$o" 'into="Gw.wcb" called_by="fire_ga" called_at="crates/ga/src/lib.rs:2" via="slot"' +o="$( run --callers=mod_glob_t )" +has "X7 a glob led by a mod the file declares: typed" "$o" 'into="Gh.hcb" called_by="fire_gh" called_at="crates/gx/src/hk.rs:2" via="slot"' +o="$( run --callers=super_glob_t )" +has "X8 use super::*: typed" "$o" 'into="Gh.hcb" called_by="fire_gh" called_at="crates/gx/src/hk.rs:2" via="slot"' +o="$( run --callers=self_glob_t )" +has "X9 use self::Kind::*: typed" "$o" 'into="Gh.hcb" called_by="fire_gh" called_at="crates/gx/src/hk.rs:2" via="slot"' +o="$( run --callers=used_crate_t )" +has "X10 tier 2 without a glob stays typed" "$o" 'into="Gw.wcb" called_by="fire_ga" called_at="crates/ga/src/lib.rs:2" via="slot"' +o="$( run --callers=nested_glob_t )" +has "X11 a glob inside a brace group hedges: slot-name" "$o" 'into="Gh.hcb" called_by="fire_gh" called_at="crates/gx/src/hk.rs:2" via="slot-name"' +hasnt "X11b never typed" "$o" 'via="slot"' +o="$( run --callers=worker_param_callee )" +has "N16a a parameter called with a function still prints the argument row" "$o" 'into="spawn#arg0"' +hasnt "N16 a parameter named like an in-tree registrar is never that registrar" "$o" 'called_by=' +o="$( run --callers=worker_local_callee )" +has "N16c a local closure called with a function still prints the argument row" "$o" 'into="spawn#arg0"' +hasnt "N16b a local closure named like an in-tree registrar is never that registrar" "$o" 'called_by=' if [ "$fail" -eq 0 ]; then echo "fnslotcheck: PASS"; else echo "fnslotcheck: FAIL"; fi exit "$fail" diff --git a/test/fnslotrust/Cargo.toml b/test/fnslotrust/Cargo.toml new file mode 100644 index 000000000..82f6cd1e2 --- /dev/null +++ b/test/fnslotrust/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "fnslotrust" +version = "0.1.0" +edition = "2021" diff --git a/test/fnslotrust/crates/twin/src/lib.rs b/test/fnslotrust/crates/twin/src/lib.rs new file mode 100644 index 000000000..d6146a443 --- /dev/null +++ b/test/fnslotrust/crates/twin/src/lib.rs @@ -0,0 +1,78 @@ +// a second crate with a same-named struct: its slot is another type (part-2 type key, crate tier) +pub struct CallEval { + eval: fn(&str) -> usize, +} + +impl CallEval { + pub fn new(eval: fn(&str) -> usize) -> Self { + CallEval { eval: eval } + } + pub fn twin_run(&self, s: &str) -> usize { + (self.eval)(s) + } +} + +pub fn twin_target(s: &str) -> usize { + s.len() + 3 +} + +pub fn twin_make() -> CallEval { + CallEval::new(twin_target) +} + +pub enum Msg { + Fire { eval: fn(&str) -> usize }, +} + +pub fn variant_target(s: &str) -> usize { + s.len() + 4 +} + +pub fn make_variant() -> Msg { + Msg::Fire { eval: variant_target } +} + +pub struct Loose { + pub cb: fn(&str) -> usize, +} + +pub fn loose_target(s: &str) -> usize { + s.len() + 5 +} + +pub fn set_loose(x: &mut Loose) { + x.cb = loose_target; +} + +pub fn call_loose(y: &Loose) -> usize { + (y.cb)("z") +} + +pub struct Reg { + pub h: fn(&str) -> usize, +} + +impl Reg { + pub fn set(&mut self, h: fn(&str) -> usize) { + self.h = h; + } + pub fn go(&self) -> usize { + (self.h)("q") + } +} + +pub fn method_reg_target(s: &str) -> usize { + s.len() + 6 +} + +pub fn use_method_reg(r: &mut Reg) { + r.set(method_reg_target); +} + +pub fn path_reg_target(s: &str) -> usize { + s.len() + 8 +} + +pub fn use_path_reg(r: &mut Reg) { + Reg::set(r, path_reg_target); +} diff --git a/test/fnslotrust/gox/g.go b/test/fnslotrust/gox/g.go new file mode 100644 index 000000000..f4bea4493 --- /dev/null +++ b/test/fnslotrust/gox/g.go @@ -0,0 +1,11 @@ +package gox + +type Box struct { + eval func(string) int +} + +func goTarget(s string) int { return len(s) } + +func makeBox() Box { return Box{eval: goTarget} } + +func (b Box) fire() int { return b.eval("x") } diff --git a/test/fnslotrust/src/engine/closure.rs b/test/fnslotrust/src/engine/closure.rs new file mode 100644 index 000000000..c561726c6 --- /dev/null +++ b/test/fnslotrust/src/engine/closure.rs @@ -0,0 +1,20 @@ +use super::eval::{eval_block, eval_block_generic, CallEval}; + +pub fn closure_eval(block: &str) -> usize { + let mut ce = CallEval::new(1, eval_block); + ce.run(block) +} + +pub fn closure_eval_generic(block: &str) -> usize { + let mut ce = CallEval::new(2, eval_block_generic::); + ce.run(block) +} + +pub fn crate_tier_target(block: &str) -> usize { + block.len() + 11 +} + +// a store in ANOTHER file of the crate: CallEval is this crate's (crates/twin has its own), keyed by the crate tier +pub fn closure_direct() -> CallEval { + CallEval { stack: 9, eval: crate_tier_target } +} diff --git a/test/fnslotrust/src/engine/eval.rs b/test/fnslotrust/src/engine/eval.rs new file mode 100644 index 000000000..739b4b1c7 --- /dev/null +++ b/test/fnslotrust/src/engine/eval.rs @@ -0,0 +1,32 @@ +pub type EvalFn = fn(&str) -> usize; + +pub struct CallEval { + stack: usize, + eval: EvalFn, +} + +impl CallEval { + pub fn new(stack: usize, eval: EvalFn) -> Self { + Self { stack, eval } + } + + pub fn run(&mut self, block: &str) -> usize { + (self.eval)(block) + self.stack + } +} + +pub fn eval_block(block: &str) -> usize { + block.len() +} + +pub fn eval_block_generic(block: &str) -> usize { + block.len() * 2 +} + +pub fn direct_eval(block: &str) -> usize { + block.len() + 1 +} + +pub fn make_direct() -> CallEval { + CallEval { stack: 0, eval: direct_eval } +} diff --git a/test/fnslotrust/src/main.rs b/test/fnslotrust/src/main.rs new file mode 100644 index 000000000..a128b5344 --- /dev/null +++ b/test/fnslotrust/src/main.rs @@ -0,0 +1,5 @@ +mod engine; +mod other; +fn main() { + println!("{}", engine::closure::closure_eval("x")); +} diff --git a/test/fnslotrust/src/other/hooks.rs b/test/fnslotrust/src/other/hooks.rs new file mode 100644 index 000000000..37de1e518 --- /dev/null +++ b/test/fnslotrust/src/other/hooks.rs @@ -0,0 +1,31 @@ +pub struct Hooks { + eval: Box usize>, +} + +impl Hooks { + pub fn new(eval: fn(&str) -> usize) -> Self { + Hooks { eval: Box::new(eval) } + } + + pub fn fire(&self, s: &str) -> usize { + (self.eval)(s) + } +} + +pub fn hook_target(s: &str) -> usize { + s.len() + 7 +} + +pub fn make_hooks() -> Hooks { + Hooks { eval: Box::new(hook_target) } +} + +pub fn shadowed(s: &str) -> usize { + s.len() + 9 +} + +pub fn uses_local(s: &str) -> usize { + let shadowed = |x: &str| x.len(); + let ce = crate::engine::eval::CallEval::new(3, shadowed); + 0 +} diff --git a/test/fnslotrust2/Cargo.toml b/test/fnslotrust2/Cargo.toml new file mode 100644 index 000000000..ab8562c5e --- /dev/null +++ b/test/fnslotrust2/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/app", "crates/lib", "crates/qual", "crates/selfmod", "crates/ga", "crates/gx"] diff --git a/test/fnslotrust2/crates/app/src/main.rs b/test/fnslotrust2/crates/app/src/main.rs new file mode 100644 index 000000000..c496c2e9b --- /dev/null +++ b/test/fnslotrust2/crates/app/src/main.rs @@ -0,0 +1,10 @@ +use extcrate::Hook; +use tokio::spawn; +fn worker_ext_struct() {} +fn worker_bare_ext() {} +fn worker_path_ext() {} +fn main() { + let _h = Hook { cb: worker_ext_struct }; + spawn(worker_bare_ext); + std::thread::spawn(worker_path_ext); +} diff --git a/test/fnslotrust2/crates/ga/src/lib.rs b/test/fnslotrust2/crates/ga/src/lib.rs new file mode 100644 index 000000000..ac7694fb5 --- /dev/null +++ b/test/fnslotrust2/crates/ga/src/lib.rs @@ -0,0 +1,3 @@ +pub struct Gw { pub wcb: fn() } +impl Gw { pub fn fire_ga(&self) { (self.wcb)() } } +pub struct Gother; diff --git a/test/fnslotrust2/crates/gx/src/g1.rs b/test/fnslotrust2/crates/gx/src/g1.rs new file mode 100644 index 000000000..27a29ddff --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g1.rs @@ -0,0 +1,3 @@ +use extcrate::*; +pub fn glob_t1() {} +pub fn mk() { let _a = Gh { hcb: glob_t1 }; } diff --git a/test/fnslotrust2/crates/gx/src/g10.rs b/test/fnslotrust2/crates/gx/src/g10.rs new file mode 100644 index 000000000..59cbd24bc --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g10.rs @@ -0,0 +1,3 @@ +use ga::Gother; +pub fn used_crate_t( _o: Gother ) {} +pub fn mk() { let _a = Gw { wcb: used_crate_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g11.rs b/test/fnslotrust2/crates/gx/src/g11.rs new file mode 100644 index 000000000..59295dcc1 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g11.rs @@ -0,0 +1,3 @@ +use extcrate::{Alpha, prelude::*}; +pub fn nested_glob_t() {} +pub fn mk() { let _a = Gh { hcb: nested_glob_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g2.rs b/test/fnslotrust2/crates/gx/src/g2.rs new file mode 100644 index 000000000..b6f1a7529 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g2.rs @@ -0,0 +1,4 @@ +use ga::Gother; +use extx::*; +pub fn glob_t2( _o: Gother ) {} +pub fn mk() { let _a = Gw { wcb: glob_t2 }; } diff --git a/test/fnslotrust2/crates/gx/src/g3.rs b/test/fnslotrust2/crates/gx/src/g3.rs new file mode 100644 index 000000000..bf4e2e489 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g3.rs @@ -0,0 +1,5 @@ +use extcrate::*; +pub struct Gm { pub mcb: fn() } +impl Gm { pub fn fire_gm_local(&self) { (self.mcb)() } } +pub fn glob_loc_t() {} +pub fn mk() { let _a = Gm { mcb: glob_loc_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g4.rs b/test/fnslotrust2/crates/gx/src/g4.rs new file mode 100644 index 000000000..de566e266 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g4.rs @@ -0,0 +1,2 @@ +pub fn plain_t() {} +pub fn mk() { let _a = Gh { hcb: plain_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g5.rs b/test/fnslotrust2/crates/gx/src/g5.rs new file mode 100644 index 000000000..1341d88c0 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g5.rs @@ -0,0 +1,3 @@ +use crate::hk::*; +pub fn intree_glob_t() {} +pub fn mk() { let _a = Gh { hcb: intree_glob_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g6.rs b/test/fnslotrust2/crates/gx/src/g6.rs new file mode 100644 index 000000000..b7ff53f57 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g6.rs @@ -0,0 +1,3 @@ +use ga::*; +pub fn crate_glob_t() {} +pub fn mk() { let _a = Gw { wcb: crate_glob_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g7.rs b/test/fnslotrust2/crates/gx/src/g7.rs new file mode 100644 index 000000000..173d6225e --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g7.rs @@ -0,0 +1,4 @@ +mod g7_inner; +use g7_inner::*; +pub fn mod_glob_t() {} +pub fn mk() { let _a = Gh { hcb: mod_glob_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g7/g7_inner.rs b/test/fnslotrust2/crates/gx/src/g7/g7_inner.rs new file mode 100644 index 000000000..144ecc662 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g7/g7_inner.rs @@ -0,0 +1 @@ +pub use crate::hk::Gh; diff --git a/test/fnslotrust2/crates/gx/src/g8.rs b/test/fnslotrust2/crates/gx/src/g8.rs new file mode 100644 index 000000000..76a9eb600 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g8.rs @@ -0,0 +1,3 @@ +use super::*; +pub fn super_glob_t() {} +pub fn mk() { let _a = Gh { hcb: super_glob_t }; } diff --git a/test/fnslotrust2/crates/gx/src/g9.rs b/test/fnslotrust2/crates/gx/src/g9.rs new file mode 100644 index 000000000..899c089c6 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/g9.rs @@ -0,0 +1,4 @@ +use self::Kind::*; +pub enum Kind { A, B } +pub fn self_glob_t() {} +pub fn mk() { let _k = A; let _a = Gh { hcb: self_glob_t }; } diff --git a/test/fnslotrust2/crates/gx/src/hk.rs b/test/fnslotrust2/crates/gx/src/hk.rs new file mode 100644 index 000000000..982bb197b --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/hk.rs @@ -0,0 +1,2 @@ +pub struct Gh { pub hcb: fn() } +impl Gh { pub fn fire_gh(&self) { (self.hcb)() } } diff --git a/test/fnslotrust2/crates/gx/src/hm.rs b/test/fnslotrust2/crates/gx/src/hm.rs new file mode 100644 index 000000000..cc4fd9c50 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/hm.rs @@ -0,0 +1,2 @@ +pub struct Gm { pub mcb: fn() } +impl Gm { pub fn fire_gm(&self) { (self.mcb)() } } diff --git a/test/fnslotrust2/crates/gx/src/lib.rs b/test/fnslotrust2/crates/gx/src/lib.rs new file mode 100644 index 000000000..6e599e585 --- /dev/null +++ b/test/fnslotrust2/crates/gx/src/lib.rs @@ -0,0 +1 @@ +pub mod hk; pub mod hm; pub mod g1; pub mod g2; pub mod g3; pub mod g4; pub mod g5; pub mod g6; pub mod g7; pub mod g8; pub mod g9; pub mod g10; pub mod g11; diff --git a/test/fnslotrust2/crates/lib/src/lib.rs b/test/fnslotrust2/crates/lib/src/lib.rs new file mode 100644 index 000000000..24b33996a --- /dev/null +++ b/test/fnslotrust2/crates/lib/src/lib.rs @@ -0,0 +1,12 @@ +pub struct Hook { cb: fn() } +impl Hook { + pub fn fire(&self) { (self.cb)() } +} +pub fn spawn(f: fn()) -> Hook { Hook { cb: f } } +pub fn lib_target() {} +pub fn mk() -> Hook { Hook { cb: lib_target } } +// CR 4408459928 (harvest 463): a callee the function itself binds (a parameter, a local closure) is never the in-tree registrar of that name +pub fn worker_param_callee() {} +pub fn run_param(spawn: fn(fn())) { spawn(worker_param_callee) } +pub fn worker_local_callee() {} +pub fn run_local() { let spawn = |f: fn()| f(); spawn(worker_local_callee) } diff --git a/test/fnslotrust2/crates/qual/src/a.rs b/test/fnslotrust2/crates/qual/src/a.rs new file mode 100644 index 000000000..0e56f85c5 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/a.rs @@ -0,0 +1,6 @@ +pub struct Cfg { pub cb: fn() } +impl Cfg { + pub fn go(&self) { (self.cb)() } +} +pub fn fa() {} +pub fn mka() -> Cfg { Cfg { cb: fa } } diff --git a/test/fnslotrust2/crates/qual/src/c.rs b/test/fnslotrust2/crates/qual/src/c.rs new file mode 100644 index 000000000..255fece60 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/c.rs @@ -0,0 +1,8 @@ +pub struct Cfg { pub cb: fn() } +impl Cfg { + pub fn local_go(&self) { (self.cb)() } +} +pub fn fc_local() {} +pub fn fc_qualified() {} +pub fn mk_local() -> Cfg { Cfg { cb: fc_local } } +pub fn mk_qualified() -> crate::a::Cfg { crate::a::Cfg { cb: fc_qualified } } diff --git a/test/fnslotrust2/crates/qual/src/here.rs b/test/fnslotrust2/crates/qual/src/here.rs new file mode 100644 index 000000000..7dcc7713a --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/here.rs @@ -0,0 +1,11 @@ +pub struct S { f: fn() } +impl S { pub fn new(f: fn()) -> Self { S { f } } pub fn go(&self) { (self.f)() } } +pub fn target() {} +pub fn register(f: fn()) -> S { S { f } } +pub fn local_t() {} +pub fn qual_t() {} +pub fn uses() { + let _a = S::new(crate::other::target); + let _b = crate::other::register(qual_t); + let _c = register(local_t); +} diff --git a/test/fnslotrust2/crates/qual/src/lib.rs b/test/fnslotrust2/crates/qual/src/lib.rs new file mode 100644 index 000000000..2d58ef2a6 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/lib.rs @@ -0,0 +1 @@ +pub mod a; pub mod c; pub mod other; pub mod here; pub mod p; pub mod q; pub mod r; pub mod nest; pub mod wrap; diff --git a/test/fnslotrust2/crates/qual/src/nest.rs b/test/fnslotrust2/crates/qual/src/nest.rs new file mode 100644 index 000000000..02bb0a7a2 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/nest.rs @@ -0,0 +1,8 @@ +pub struct Inner { pub f: fn() } +pub struct Outer { pub inner: Inner, pub f: fn() } +impl Outer { + pub fn go_inner(&self) { (self.inner.f)() } +} +pub fn in_t() {} +pub fn out_t() {} +pub fn mk() -> Outer { Outer { inner: Inner { f: in_t }, f: out_t } } diff --git a/test/fnslotrust2/crates/qual/src/other.rs b/test/fnslotrust2/crates/qual/src/other.rs new file mode 100644 index 000000000..99753fe7c --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/other.rs @@ -0,0 +1,2 @@ +pub fn target() {} +pub fn register(f: fn()) { f() } diff --git a/test/fnslotrust2/crates/qual/src/p.rs b/test/fnslotrust2/crates/qual/src/p.rs new file mode 100644 index 000000000..05b42db87 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/p.rs @@ -0,0 +1,2 @@ +pub struct Amb { pub f: fn() } +impl Amb { pub fn go_p(&self) { (self.f)() } } diff --git a/test/fnslotrust2/crates/qual/src/q.rs b/test/fnslotrust2/crates/qual/src/q.rs new file mode 100644 index 000000000..c0da79080 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/q.rs @@ -0,0 +1,2 @@ +pub struct Amb { pub f: fn() } +impl Amb { pub fn go_q(&self) { (self.f)() } } diff --git a/test/fnslotrust2/crates/qual/src/r.rs b/test/fnslotrust2/crates/qual/src/r.rs new file mode 100644 index 000000000..86a0a3126 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/r.rs @@ -0,0 +1,4 @@ +pub fn amb_t() {} +pub fn amb_bare_t() {} +pub fn mk() -> crate::p::Amb { crate::p::Amb { f: amb_t } } +pub fn mk_bare() { let _x = Amb { f: amb_bare_t }; } diff --git a/test/fnslotrust2/crates/qual/src/wrap.rs b/test/fnslotrust2/crates/qual/src/wrap.rs new file mode 100644 index 000000000..c2e8eedd0 --- /dev/null +++ b/test/fnslotrust2/crates/qual/src/wrap.rs @@ -0,0 +1,4 @@ +pub struct Dyn { g: std::sync::Arc } +impl Dyn { pub fn go(&self) { (self.g)() } } +pub fn arc_t() {} +pub fn mk() -> Dyn { Dyn { g: std::sync::Arc::new(arc_t) } } diff --git a/test/fnslotrust2/crates/selfmod/src/a.rs b/test/fnslotrust2/crates/selfmod/src/a.rs new file mode 100644 index 000000000..c107f7cce --- /dev/null +++ b/test/fnslotrust2/crates/selfmod/src/a.rs @@ -0,0 +1,2 @@ +pub struct Sa { pub acb: fn() } +impl Sa { pub fn fire_sa(&self) { (self.acb)() } } diff --git a/test/fnslotrust2/crates/selfmod/src/lib.rs b/test/fnslotrust2/crates/selfmod/src/lib.rs new file mode 100644 index 000000000..2e1f78ba8 --- /dev/null +++ b/test/fnslotrust2/crates/selfmod/src/lib.rs @@ -0,0 +1 @@ +pub mod workspace_settings_module; pub mod u_long; pub mod a; pub mod u_short; diff --git a/test/fnslotrust2/crates/selfmod/src/u_long.rs b/test/fnslotrust2/crates/selfmod/src/u_long.rs new file mode 100644 index 000000000..baa1287db --- /dev/null +++ b/test/fnslotrust2/crates/selfmod/src/u_long.rs @@ -0,0 +1,5 @@ +use crate::workspace_settings_module::{self, Other}; +pub struct Ws { pub scb: fn() } +impl Ws { pub fn fire_ws_local(&self) { (self.scb)() } } +pub fn self_long_t() {} +pub fn mk( _o: Other ) { let _y = workspace_settings_module::Ws { scb: self_long_t }; } diff --git a/test/fnslotrust2/crates/selfmod/src/u_short.rs b/test/fnslotrust2/crates/selfmod/src/u_short.rs new file mode 100644 index 000000000..37a2906f4 --- /dev/null +++ b/test/fnslotrust2/crates/selfmod/src/u_short.rs @@ -0,0 +1,5 @@ +use crate::{a::{self}}; +pub struct Sa { pub acb: fn() } +impl Sa { pub fn fire_sa_local(&self) { (self.acb)() } } +pub fn self_short_t() {} +pub fn mk() { let _y = a::Sa { acb: self_short_t }; } diff --git a/test/fnslotrust2/crates/selfmod/src/workspace_settings_module.rs b/test/fnslotrust2/crates/selfmod/src/workspace_settings_module.rs new file mode 100644 index 000000000..98a0fef9d --- /dev/null +++ b/test/fnslotrust2/crates/selfmod/src/workspace_settings_module.rs @@ -0,0 +1,3 @@ +pub struct Ws { pub scb: fn() } +impl Ws { pub fn fire_ws(&self) { (self.scb)() } } +pub struct Other; diff --git a/test/forcallerhopcheck.sh b/test/forcallerhopcheck.sh index 3321a93bb..ed28ed322 100755 --- a/test/forcallerhopcheck.sh +++ b/test/forcallerhopcheck.sh @@ -28,6 +28,20 @@ # B3 [P] a d="2" row carries via="name" when its own callers row does (Python driver(c): c.submit(None)) # Q1 [P] train 33 (MF1-NX x QUALNAME-1): every first-hop row's n= and p= equal the callers verb's own row; a member caller # folds its owner into n= (n="Client.submit", no sc=) +# R1 [P] UPWARD-2HOP: an error-shaped task ("dependency cycle error") on the ranked route whose r=1 row is a narrow-funnel function +# (verify_dag, 1 caller) carries : recompute_node, then recompute_dirty / recompute_edges d="2" +# R2 [P] the ranked clause rides in both legends (the named-route clause does not) and defines every attribute the element carries +# R3 [P] a 4-caller r=1 function is skipped and the next r=1..3 narrow funnel is the seed (the cockroach-01 shape: r=3) +# R4 [G] every first-hop caller a test path: none R5 [G] the same head, no error word: none R6 [G] the narrow funnel at r>3: none +# R7 [G] prefix identity vs RIPWIRE_NO_CALLER_HOP=1 (both legends) R8 [P] est_tokens prices it +# R9 [G] --json / --signatures-only / --token-budget=3000: byte-identical to the hop-off answer R10 [P] first-hop rows = --callers rows +# R11 [P] fix round 1 (F4): each error word of the language-neutral vocabulary (panicked, Traceback, raises, throws, segfault, crashed, +# failing, ... and an error TYPE as one identifier: DependencyCycleError, dependency_cycle_error, ValueError) fires the hop +# R12 [G] WHOLE WORD: failover, throwaway, terror, ErrorKind, raiser, crashpad, failsafe, errorless carry none (kills a substring match) +# R13 [P] an explanation question (How does ... / Explain ... / Describe ...) carries none; [G] a where-question with a message-bearing row does +# R14 [P] F5: the seed is the narrow funnel whose own body holds a string literal with >= 2 task words (rankword: lookup_word r=3, not +# command_not_found_hook r=1; hook_user r=2 holds a ONE-word literal, below the bar); R15 [P] a question with no message-bearing narrow funnel carries none (a statement keeps R3's fallback) +# R16 [P] F2: a seed whose only first-hop caller is a by-name match (Go interface, Python duck typing) carries none (rankgo, rankpy) # X1 [G] determinism X2 [G] xmllint well-formed X3 [G] no "--" inside a legend comment # # Usage: bash test/forcallerhopcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forcallerhopcheck.sh @@ -243,8 +257,141 @@ for q in "b3:test/forcallerhopfix/d2via:Ledger::post_entry" "p1:$FC:start_server done printf '%s' "$H3b" | grep -q ''; verdict $? "(Q1b) a member caller folds its owner into n= and prints no sc=" "(Q1b) the Client.submit hop row is not n=\"Client.submit\" without sc=: [$H3b]" +# ── R: UPWARD-2HOP, the ranked seed (forcallerhop.h forRankedCallerHopTargets; [P] arms RED on the train-40 binary 5163ffbdb) ── +FR="test/forcallerhopfix/rank" +[ -f "$FR/graph.c" ] || { no "(R-pre) fixture $FR missing"; } +run "$FR" --for="dependency cycle error" >"$TMP/r1" +run "$FR" --for="dependency cycle error" --legend=full >"$TMP/r1f" +R1="$( hop "$TMP/r1" )" +if grep -q 'route="subtoken+body' "$TMP/r1" && grep -q ']*n="verify_dag"[^>]*r="1"' "$TMP/r1"; then ok "(R1pre) the error task routes subtoken+body with verify_dag at r=1"; else no "(R1pre) the fixture does not rank verify_dag r=1 on a ranked route — the arm proves nothing"; fi +printf '%s' "$R1" | python3 -c ' +import re, sys +x = sys.stdin.read() +ok = x.startswith("]* d=\"2\"", x) and re.search(r"n=\"recompute_edges\"[^>]* d=\"2\"", x) and "next=\"--impact=verify_dag\"" in x +sys.exit(0 if ok else 1)' +verdict $? "(R1) an error task's r=1 narrow-funnel function carries : recompute_node, then recompute_dirty / recompute_edges at d=2" "(R1) the ranked hop is missing or wrong: [$R1]" +for f in r1 r1f; do + python3 - "$TMP/$f" <<'PY' +import re, sys +t = open(sys.argv[1], encoding="utf-8", errors="replace").read() +m = re.search(r']*>.*?', t, re.S) +if not m: sys.exit(1) +legend = "".join(re.findall(r'', t, re.S)) +i = legend.find("; callers by=rank of=:") +if i < 0 or "; callers of=: the route named" in legend: sys.exit(2) +clause = legend[i:] +names = set(re.findall(r'\s([a-z_0-9]+)="', m.group(0))) +miss = [n for n in names if not re.search(r'(?"$TMP/r3" +R3="$( hop "$TMP/r3" )" +if grep -q ']*n="parse_manifest"[^>]*in="4"[^>]*r="1"' "$TMP/r3" && printf '%s' "$R3" | grep -q '^"$TMP/r4" +if [ -s "$TMP/r4" ] && grep -q ']*n="release_lockfile"[^>]*r="1"' "$TMP/r4" && ! grep -q '"$TMP/r5" +if [ -s "$TMP/r5" ] && grep -q ']*n="verify_dag"[^>]*r="1"' "$TMP/r5" && ! grep -q '"$TMP/r6" +if [ -s "$TMP/r6" ] && grep -q ']*n="poll_socket_once"[^>]*in="1"[^>]*r="[4-9]"' "$TMP/r6" && ! grep -q '"$TMP/r7on.$lg"; strip "$TMP/r7on.$lg" >"$TMP/r7on.s.$lg" + python3 - "$TMP/r7on.s.$lg" <<'PY' +import re, sys +p = sys.argv[1]; t = open(p, encoding="utf-8").read() +t = re.sub(r'; callers by=rank of=:.*?next=(?: lists)? the (?:whole caller )?radius', '', t, flags=re.S) +open(p, "w", encoding="utf-8").write(t) +PY + RIPWIRE_NO_CALLER_HOP=1 run "$FR" --for="dependency cycle error" --legend=$lg >"$TMP/r7off.$lg"; strip "$TMP/r7off.$lg" >"$TMP/r7off.s.$lg" + if [ -s "$TMP/r7off.s.$lg" ] && cmp -s "$TMP/r7on.s.$lg" "$TMP/r7off.s.$lg" && ! grep -q ' $r8on)"; else no "(R8) est_tokens did not grow with the ranked hop (on=[$r8on] off=[$r8off])"; fi +for extra in --json --signatures-only --token-budget=3000; do + run "$FR" --for="dependency cycle error" "$extra" >"$TMP/r9.on" + RIPWIRE_NO_CALLER_HOP=1 run "$FR" --for="dependency cycle error" "$extra" >"$TMP/r9.off" + if [ -s "$TMP/r9.on" ] && cmp -s "$TMP/r9.on" "$TMP/r9.off"; then ok "(R9) [G] $extra: no ranked hop, byte-identical to the hop-off answer"; else no "(R9) $extra: the ranked hop changed the answer"; fi +done +run "$FR" --callers=verify_dag >"$TMP/r10" +python3 - "$TMP/r1" "$TMP/r10" <<'PY' +import re, sys +hop = re.search(r']*>(.*?)', open(sys.argv[1]).read(), re.S) +cal = open(sys.argv[2]).read() +if not hop: sys.exit(1) +key = lambda r: (re.search(r' n="([^"]*)"', r).group(1), re.search(r' p="([^"]*)"', r).group(1)) +first = [key(r) for r in re.findall(r']*/>', hop.group(1)) if ' d="2"' not in r] +verb = {key(r) for r in re.findall(r']*/>', cal) if ' n="' in r and ' p="' in r} +sys.exit(0 if first and all(k in verb for k in first) else 1) +PY +verdict $? "(R10) every first-hop row of the ranked hop is --callers=verify_dag's own row" "(R10) a ranked first-hop row differs from --callers=verify_dag" + +# ── R11-R16: UPWARD-2HOP fix round 1 (rv-upward-2hop F1-F3, F5; the error-task matcher F4) ────────────────────────────────────── +# R11-R13 pin the task matcher: a WHOLE WORD of the language-neutral error vocabulary (or an error TYPE written as one identifier), +# not a substring, and not an EXPLANATION question. Each task rides the same fixture whose r=1 narrow funnel is verify_dag, so a +# missing hop is the matcher's verdict, never the route's (the premise is re-checked per task). +rk_hop(){ # $1 task -> prints "R" when verify_dag is r=1 on a ranked route, then "H" when the ranked hop rode + run "$FR" --for="$1" >"$TMP/rk" + grep -q 'route="subtoken+body' "$TMP/rk" && grep -q ']*n="verify_dag"[^>]*r="1"' "$TMP/rk" && printf R + grep -q '= 2 task words (lookup_word: "command %d not +# found"), not the r=1 funnel that merely carries the words in its name (command_not_found_hook). Statement and question alike. +FW="test/forcallerhopfix/rankword" +for q in "command not found error" "Where is the command not found error raised?"; do + run "$FW" --for="$q" >"$TMP/r14" + if grep -q ']*n="\(command_not_found_hook\|hook_user\)"[^>]*r="[12]"' "$TMP/r14" && grep -q ']*n="lookup_word"[^>]*r="3"' "$TMP/r14"; then + grep -q '"$TMP/r15" +if [ -s "$TMP/r15" ] && grep -q ']*n="load_one"[^>]*r="2"' "$TMP/r15" && ! grep -q '"$TMP/r15b" + if [ -s "$TMP/r15b" ] && grep -q ']*n="load_one"[^>]*r="2"' "$TMP/r15b"; then + ! grep -q '"$TMP/r16" + run "test/forcallerhopfix/$d" --callers="$of" >"$TMP/r16c" + if [ -s "$TMP/r16" ] && grep -q 'route="subtoken+body' "$TMP/r16" && grep -q ' via="name"' "$TMP/r16c"; then + ! grep -q '', open(p).read(), re.S): @@ -254,7 +401,7 @@ verdict $? "(X3) no -- inside a legend comment" "(X3) a legend comment holds --" run "$FC" --for=start_server >"$TMP/x2" cmp -s "$TMP/p1" "$TMP/x2"; verdict $? "(X1) deterministic" "(X1) two runs differ" if command -v xmllint >/dev/null 2>&1; then - for f in p1 p2 p3 p4f p8 b3 f1.on dp.f; do xmllint --noout "$TMP/$f" 2>/dev/null; verdict $? "(X2) well-formed: $f" "(X2) xmllint rejects $f"; done + for f in p1 p2 p3 p4f p8 b3 f1.on dp.f r1 r1f r3; do xmllint --noout "$TMP/$f" 2>/dev/null; verdict $? "(X2) well-formed: $f" "(X2) xmllint rejects $f"; done else printf ' SKIP (X2) xmllint not installed\n' fi diff --git a/test/forcallerhopfix/rank/graph.c b/test/forcallerhopfix/rank/graph.c new file mode 100644 index 000000000..56208b4d5 --- /dev/null +++ b/test/forcallerhopfix/rank/graph.c @@ -0,0 +1,32 @@ +/* forcallerhopfix/rank — UPWARD-2HOP fixture: an error-shaped task ranks a narrow-funnel function (verify_dag, one caller). */ +#include + +void report_problem( const char* what ) +{ + fprintf( stderr, "%s\n", what ); +} + +int verify_dag( int node ) +{ + if( node < 0 ) + { + report_problem( "dependency cycle detected" ); + return 0; + } + return 1; +} + +int recompute_node( int node ) +{ + return verify_dag( node ); +} + +int recompute_dirty( int node ) +{ + return recompute_node( node ); +} + +int recompute_edges( int node ) +{ + return recompute_node( node + 1 ); +} diff --git a/test/forcallerhopfix/rank/lock.c b/test/forcallerhopfix/rank/lock.c new file mode 100644 index 000000000..bf45a58e3 --- /dev/null +++ b/test/forcallerhopfix/rank/lock.c @@ -0,0 +1,9 @@ +/* every caller of release_lockfile is a test: the chain already ends there, so no ranked hop */ +int release_lockfile( int fd ) +{ + if( fd < 0 ) + { + return -1; /* lockfile release stale */ + } + return 0; +} diff --git a/test/forcallerhopfix/rank/main.c b/test/forcallerhopfix/rank/main.c new file mode 100644 index 000000000..c1012c671 --- /dev/null +++ b/test/forcallerhopfix/rank/main.c @@ -0,0 +1,7 @@ +int recompute_dirty( int node ); +int recompute_edges( int node ); +int load_one( void ); +int main( void ) +{ + return recompute_dirty( 1 ) + recompute_edges( 2 ) + load_one(); +} diff --git a/test/forcallerhopfix/rank/sock.c b/test/forcallerhopfix/rank/sock.c new file mode 100644 index 000000000..27af76d94 --- /dev/null +++ b/test/forcallerhopfix/rank/sock.c @@ -0,0 +1,14 @@ +/* r=4: three socket timeout types rank above the one narrow function, so the ranked hop (r=1..3 only) carries none */ +struct socket_timeout_state { int socket_timeout; }; +struct socket_timeout_limits { int socket_timeout_max; }; +struct socket_timeout_stats { int socket_timeout_count; }; + +int poll_socket_once( int fd ) +{ + return fd > 0 ? 0 : -1; /* socket timeout */ +} + +int poll_loop( int fd ) +{ + return poll_socket_once( fd ); +} diff --git a/test/forcallerhopfix/rank/tests/lock_test.c b/test/forcallerhopfix/rank/tests/lock_test.c new file mode 100644 index 000000000..eff7b680f --- /dev/null +++ b/test/forcallerhopfix/rank/tests/lock_test.c @@ -0,0 +1,2 @@ +int release_lockfile( int fd ); +int test_release_lockfile( void ) { return release_lockfile( 3 ); } diff --git a/test/forcallerhopfix/rank/wide.c b/test/forcallerhopfix/rank/wide.c new file mode 100644 index 000000000..2091acac5 --- /dev/null +++ b/test/forcallerhopfix/rank/wide.c @@ -0,0 +1,14 @@ +/* the wide funnel: parse_manifest has four callers, so a task about its error carries no ranked hop */ +int parse_manifest( const char* text ) +{ + if( text == 0 ) + { + return -1; /* manifest syntax malformed */ + } + return 0; +} + +int load_one( void ) { return parse_manifest( "a" ); } +int load_two( void ) { return parse_manifest( "b" ); } +int load_three( void ) { return parse_manifest( "c" ); } +int load_four( void ) { return parse_manifest( "d" ); } diff --git a/test/forcallerhopfix/rankgo/store.go b/test/forcallerhopfix/rankgo/store.go new file mode 100644 index 000000000..a7a8da113 --- /dev/null +++ b/test/forcallerhopfix/rankgo/store.go @@ -0,0 +1,39 @@ +// forcallerhopfix/rankgo — UPWARD-2HOP F2: the seed method (memStore.Put) is reached through an interface, so its only first-hop caller is a by-name match +package store + +import "errors" + +type Store interface { + Put(key string) error +} + +type memStore struct{ keys []string } + +func (m *memStore) Put(key string) error { + for _, k := range m.keys { + if k == key { + return errors.New("duplicate key error") + } + } + m.keys = append(m.keys, key) + return nil +} + +type diskStore struct{} + +func (d *diskStore) Put(key string) error { return nil } + +func saveAll(s Store, keys []string) (err error) { + for i := 0; i < len(keys) && err == nil; i++ { + err = s.Put(keys[i]) + } + return err +} + +func Import(keys []string) error { + return saveAll(&memStore{}, keys) +} + +func Sync(keys []string) error { + return saveAll(&diskStore{}, keys) +} diff --git a/test/forcallerhopfix/rankpy/parsers.py b/test/forcallerhopfix/rankpy/parsers.py new file mode 100644 index 000000000..7d92b4549 --- /dev/null +++ b/test/forcallerhopfix/rankpy/parsers.py @@ -0,0 +1,25 @@ +# forcallerhopfix/rankpy — UPWARD-2HOP F2: the seed method (HeaderParser.parse_header) is reached through a duck-typed parameter, so its only first-hop caller is a by-name match +class HeaderParser: + def parse_header(self, data): + if not data.startswith(b"HDR"): + raise ValueError("bad header error") + return data[3:] + + +class LenientParser: + def parse_header(self, data): + return data + + +def load_blob(parser, data): + return parser.parse_header(data) + + +def import_file(path): + with open(path, "rb") as f: + return load_blob(HeaderParser(), f.read()) + + +def import_lenient(path): + with open(path, "rb") as f: + return load_blob(LenientParser(), f.read()) diff --git a/test/forcallerhopfix/rankword/cmd.c b/test/forcallerhopfix/rankword/cmd.c new file mode 100644 index 000000000..b89ed76a5 --- /dev/null +++ b/test/forcallerhopfix/rankword/cmd.c @@ -0,0 +1,34 @@ +/* forcallerhopfix/rankword — UPWARD-2HOP seed fixture: the r=1 narrow funnel names the task's words but holds no message; + the function that raises the message ("command %d not found") is the next narrow funnel. */ +#include + +int command_not_found_hook( int w ) +{ + return w + 1; +} + +int hook_user( int w ) +{ + fputs( "error command\n", stderr ); /* ONE task word plus "error" (never a task word): below the two-word bar */ + return command_not_found_hook( w ); +} + +int lookup_word( int w ) +{ + if( w < 0 ) + { + fprintf( stderr, "command %d not found\n", w ); + return -1; + } + return 0; +} + +int run_word( int w ) +{ + return lookup_word( w ); +} + +int main( void ) +{ + return hook_user( 1 ) + run_word( 2 ); +} diff --git a/test/forheadrrcheck.sh b/test/forheadrrcheck.sh index eead9604b..ebfd8b1ec 100755 --- a/test/forheadrrcheck.sh +++ b/test/forheadrrcheck.sh @@ -22,6 +22,7 @@ # rows, more distinct files in the head-8 (a file the OFF head-8 left out enters it), r=1 unchanged, files_first= == the rows whose rank changed, reading present # H2 [P] a C typedef/struct pair (pair): the same, and the fixture exercises the name rule (the model without it differs) # H3 [G] the fold serves the score top three: the first three rows of the RIPWIRE_NO_HEAD_RR=1 answer (mono, pair) +# H3b [P fix1] the loop aggregator dispatchQueuedJobs (mono) is never followed (review F6; red on 5351fbbb) # H4 [P] --json: the rows in the XML ON order, "files_first" present; H4m [P] MCP `for`: the same order, files_first= present # H5 [G] exempt, byte-identical to RIPWIRE_NO_HEAD_RR=1: the name-exact route, --no-route, a question naming src/scheduler.ts, # --token-budget=1200 @@ -106,7 +107,16 @@ if cmd == "rr": # ON OFF [names] — ON is the round-robin of OFF; fi elif cmd == "fold": # ON OFF — the fold's rows are the score top three (OFF's first three rows), and ON's fold is OFF's t = load(args[0]); m = re.search(r']*>(.*?)', t, re.S) if not m: fail("no ") - got = [attrs(x.group(1)).get("n") for x in re.finditer(r'<(?:b|skip|slice) ([^>]*)>', m.group(1))] + # WRAPPER-FOLLOW (src/hopbody.h): a is a thin wrapper's callee riding after it, not a ranked row — the ranked rows + # are still exactly the score top three; a follow body must name a wrapper the fold serves + pieces = [attrs(x.group(1)) for x in re.finditer(r'<(?:b|skip|slice) ([^>]*)>', m.group(1))] + got = [a.get("n") for a in pieces if "follow_of" not in a] + # a follow names (as Owner.name or bare) the body just before it: a ranked row, an earlier follow (a 2-level chain), or the + # slot owner's body served ahead of the fold (review F6: each of the three is a legitimate wrapper) + slot = [attrs(x.group(1)).get("n") for x in re.finditer(r']*)>', t[:t.find(']*>.*?', load(args[1]), re.S) if not mo or mo.group(0) != m.group(0): fail("the fold differs from the RIPWIRE_NO_HEAD_RR=1 answer's") @@ -164,6 +174,9 @@ run "$TMP/mono.on" "$FIX/mono" 0 --for="$Q_MONO"; run "$TMP/mono.off" "$FIX/mono if ran "$TMP/mono.on" H1 && ran "$TMP/mono.off" "H1 (twin)"; then chk "H1 one file holds most rows: the head names each file first" rr "$TMP/mono.on" "$TMP/mono.off" morefiles chk "H3 mono: the fold serves the score top three" fold "$TMP/mono.on" "$TMP/mono.off" + # fix1 (review F6): dispatchQueuedJobs is a loop aggregator (let n; for … { dispatchJobToWorker(…); n++ } return n), not a thin + # delegator — its one call is not its last statement, so it is never followed (RED on 5351fbbb, where H3 went red in CI) + if grep -q 'follow_of="[^"]*dispatchQueuedJobs"' "$TMP/mono.on"; then no "H3b mono: the loop aggregator dispatchQueuedJobs is followed"; else ok "H3b mono: the loop aggregator dispatchQueuedJobs is never followed"; fi fi run "$TMP/pair.on" "$FIX/pair" 0 --for="$Q_PAIR"; run "$TMP/pair.off" "$FIX/pair" 1 --for="$Q_PAIR" if ran "$TMP/pair.on" H2 && ran "$TMP/pair.off" "H2 (twin)"; then diff --git a/test/forroutecheck.sh b/test/forroutecheck.sh index 41a6f5f10..6cb847f64 100755 --- a/test/forroutecheck.sh +++ b/test/forroutecheck.sh @@ -7,7 +7,9 @@ # (p1) path form: the --path answer for the two named symbols, reachable="1", once, right before , well-formed # (p2) the qualifier tail is cut (to_span= stops before "for"), and `main` is the one plain word that is an endpoint # (c1) callers form: the --callers answer (both callers listed); (c2) the qualifier tail "with ..." is cut -# (v1)/(v2) the block's verb answer is byte-identical to the bare --path= / --callers= call (default caps, compact legend) +# (v1)/(v2) the block's verb answer is byte-identical to the bare --path= / --callers= call (default caps, compact legend); +# v1 under RIPWIRE_NO_HOP_BODY=1 (the pre-PATH-HOP-BODY premise). (v1b) hop bodies on: identical but for a middle hop body +# whose text the --for answer already carries outside the block (src/hopbody.h; not printed twice) # (m1) common-callers form: the --connect answer for both names # (t1) an unresolved last-first candidate falls through to the next one, and tried="2" says so # (n1) "path from X to serialising a dict" does NOT fire, though `dict` is a symbol here @@ -99,15 +101,37 @@ import re, sys s = open( sys.argv[1], encoding="utf8", errors="replace" ).read() m = re.search( r"]*>(.*)$", s, re.S ) sys.stdout.write( m.group( 1 ) if m else "NO-BLOCK" )' "$1"; } -for arm in "v1|p1|--path=startEntry,leaf_sink" "v2|c1|--callers=leaf_sink" +# (v1) under the PATH-HOP-BODY A/B handle (src/hopbody.h), so its premise is the pre-lane one: the block's bytes ARE the bare call's. +# With hop bodies on, (v1b) below states the one by-design difference. +RIPWIRE_NO_HOP_BODY=1 run --for="path from startEntry to leaf_sink" >"$TMP/p1off.xml" +for arm in "v1|p1off|--path=startEntry,leaf_sink" "v2|c1|--callers=leaf_sink" do id="${arm%%|*}"; rest="${arm#*|}"; src="${rest%%|*}"; flag="${rest#*|}" - run "$flag" >"$TMP/$id.bare"; inner "$TMP/$src.xml" >"$TMP/$id.inner" + if [ "$id" = v1 ]; then RIPWIRE_NO_HOP_BODY=1 run "$flag" >"$TMP/$id.bare"; else run "$flag" >"$TMP/$id.bare"; fi + inner "$TMP/$src.xml" >"$TMP/$id.inner" if [ -s "$TMP/$id.bare" ] && cmp -s "$TMP/$id.bare" "$TMP/$id.inner" then ok "($id) the block's verb answer is byte-identical to the bare $flag call (same default caps, same compact legend)" else no "($id) the block's verb answer differs from the bare $flag call ($( wc -c <"$TMP/$id.inner" ) vs $( wc -c <"$TMP/$id.bare" ) B)"; fi done +# (v1b) PATH-HOP-BODY, hop bodies on: the block is the bare call, except that a middle hop body whose text the --for answer +# already carries (outside the block) is not printed twice — then the bare call's and its legend reading are the +# whole difference. A middle hop the answer does not carry stays in the block byte for byte. +run --path=startEntry,leaf_sink >"$TMP/v1b.bare"; inner "$TMP/p1.xml" >"$TMP/v1b.inner" +if python3 - "$TMP/v1b.bare" "$TMP/v1b.inner" "$TMP/p1.xml" <<'PY' +import re, sys +bare, inner, full = (open(f, encoding="utf8", errors="replace").read() for f in sys.argv[1:4]) +if bare == inner: sys.exit(0) +el = re.search(r'', bare, re.S) +if not el or ']*p="%s"' % re.escape(m.group(1)), outside) for m in re.finditer(r']*p="([^"]*)"', el.group(0))) +nolegend = lambda t: re.sub(r'', '', t, flags=re.S) # its present-only readings (bodies of=, the window) go with it; (v1) pins the legend +sys.exit(0 if held and nolegend(bare.replace(el.group(0), "")) == nolegend(inner) else 1) +PY +then ok "(v1b) hop bodies on: the block is the bare --path call, less only a middle hop body the answer already carries" +else no "(v1b) hop bodies on: the block differs from the bare --path call beyond the already-carried middle hop body"; fi + run --for="common callers of mid_hop and other_caller" >"$TMP/m1.xml" f="$( facts "$TMP/m1.xml" )" if [ "${f%% tried=*}" = "n=1 endpoints=subject verb=--connect=mid_hop,other_caller from=- to=- of=mid_hop and=other_caller" ] && grep -q ' (rule p=, the readers and the method each passes the name to, the posted classes widest first, +# the handler defs listed only when at most 16, else next=). reachable= / hops= / reaches= never move. +# +# ARMS +# (A1) --path=post_message,on_* : with p= at the rule line, base=Message attr=handler_name, the reader +# Pump._on_message passes= Pump._get_dispatch_methods, , and the +# two handler defs as rows (handlers=2 <= 16, no next=); reachable="0". +# (A2) --impact=widgets.py:on_button_pressed: reaches_hedged=1 reaches_hedged_via=name-key (Button.press), reaches="0". +# (A3) a nested class's name is owner-qualified, a namespace= keyword replaces the owner: on_worker_changed is reached hedged. +# (A4) a top-level class posted by name (Tick()) hops to on_tick: the bridge --path=tick,leaf crosses via="name-key" cands="1". +# (N1) a variable argument (msg = Tick(); post_message( msg )) is no post site: --path=poke,leaf has no name-key crossing. +# (N2) a repo whose __init_subclass__ builds the name another way (no converter) has no rule: no , no name-key. +# (N3) a call that is not a poster's (log( Tick() )) is no post site. +# (W1) 17 handler defs: handlers="17", no rows, next= lists one name's defs. +# (L1) the clause rides only with the block (full + compact); every answer parses as XML. +# (M1) MCP path_between carries the same . +# FIX ROUND 1 (review rv-on-name-v2; each arm red on 00d4c21d) +# (R1) the f-string's hole is TRACED to its assignments: a converter called for another attribute while the hole is built by +# .lower() is no rule; a converter call split over lines is still one. +# (R2) a rule write under an if/else (a guard, a kwarg branch) is no rule; one after a closed block, at the top level, is. +# (R3) the NEAREST rule base wins (Command's own do_ rule beats Message's on_); a class below an intermediate that rewrites the +# attribute with something else is never named; the intermediate itself still obeys the base above it. +# (R4) a subclass of an unrelated same-named base gets no hop; the one whose import resolves to the ruled base keeps it. +# (R5) readers and posters are per rule: Bus.emit reads the other rule's attribute, so it is not the Message block's reader and +# posts no Message; Pump posts no Event. +# (R6) the hrd legend names where passes= comes from: the read's line, or a later call passing the local it was read into. +# (R7) `self.X(` names the caller's class FAMILY only (Input.Changed + MaskedInput.Changed, never Select.Changed). +# +# Usage: test/handlerrulecheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "handlerrulecheck: BIN=$BIN" +[ -x "$BIN" ] || { echo " FAIL no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo " FAIL python3 required"; exit 2; } +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT + +mkdir -p "$TMP/fw" "$TMP/norule" "$TMP/wide" +cat > "$TMP/fw/case.py" <<'EOF' +import re + + +def camel_to_snake(name, _re=re.compile("[a-z][A-Z]")): + def repl(m): + lower, upper = m.group() + return f"{lower}_{upper.lower()}" + + return _re.sub(repl, name).lower() +EOF +cat > "$TMP/fw/message.py" <<'EOF' +from case import camel_to_snake + + +class Message: + namespace = "" + + def __init_subclass__(cls, namespace=None): + super().__init_subclass__() + if namespace is not None: + name = f"{namespace}_{camel_to_snake(cls.__name__)}" + else: + parts = cls.__qualname__.rsplit(".", 2)[-2:] + name = "_".join(camel_to_snake(p) for p in parts) + cls.handler_name = f"on_{name}" +EOF +cat > "$TMP/fw/pump.py" <<'EOF' +class Pump: + def post_message(self, message): + self._queue.append(message) + return True + + def _on_message(self, message): + handler_name = message.handler_name + for method in self._get_dispatch_methods(handler_name, message): + method(message) + + def _get_dispatch_methods(self, method_name, message): + for cls in self.__class__.__mro__: + method = cls.__dict__.get(method_name) + if method is not None: + yield method.__get__(self, cls) +EOF +cat > "$TMP/fw/widgets.py" <<'EOF' +from message import Message +from pump import Pump + + +def leaf(x): + return x + + +class Tick(Message): + pass + + +class Button(Pump): + class Pressed(Message): + pass + + def press(self): + self.post_message(self.Pressed()) + + +class Worker(Pump): + class Changed(Message, namespace="worker"): + pass + + def change(self): + self.post_message(Worker.Changed()) + + +class Form(Pump): + def on_button_pressed(self, m): + return leaf(m) + + def on_worker_changed(self, m): + return m + + def on_tick(self, m): + return leaf(m) + + +def log(m): + return m + + +class Clock(Pump): + def tick(self): + self.post_message(Tick()) + + def poke(self): + msg = Tick() + self.post_message(msg) + + def note(self): + log(Tick()) +EOF +cat > "$TMP/fw/other.py" <<'EOF' +class Panel: + def on_button_pressed(self, m): + return m +EOF +cp "$TMP/fw/case.py" "$TMP/fw/pump.py" "$TMP/fw/widgets.py" "$TMP/norule/" +cat > "$TMP/norule/message.py" <<'EOF' +class Message: + def __init_subclass__(cls): + super().__init_subclass__() + name = cls.__name__.lower() + cls.handler_name = f"on_{name}" +EOF +cp "$TMP/fw/case.py" "$TMP/fw/message.py" "$TMP/fw/pump.py" "$TMP/wide/" +{ + printf 'from message import Message\nfrom pump import Pump\n\n\nclass Wide(Message):\n pass\n\n\nclass Src(Pump):\n def send(self):\n self.post_message(Wide())\n' + for i in $( seq 1 17 ); do printf '\n\nclass H%s:\n def on_wide(self, m):\n return m\n' "$i"; done +} > "$TMP/wide/w.py" + +python3 - "$BIN" "$TMP" <<'PY' +import json, os, re, subprocess, sys +binp, tmp = sys.argv[1], sys.argv[2] +fail = 0 +def ok( m ): print( " PASS " + m ) +def no( m ): + global fail + fail = 1 + print( " FAIL " + m ) +def check( c, m ): ( ok if c else no )( m ) +def run( d, *args ): + p = subprocess.run( [ binp, d, "--no-cache", *args ], cwd = d, capture_output = True, text = True, timeout = 120 ) + return p.stdout if p.returncode == 0 else None +def xml_ok( t ): + if t is None: + return False + q = subprocess.run( [ "xmllint", "--noout", "-" ], input = t, capture_output = True, text = True ) + return q.returncode == 0 +def root( t, tag ): + m = re.search( "<" + tag + r"\b[^>]*>", t or "" ) + return m.group( 0 ) if m else "" +def attr( el, a ): + m = re.search( r'\s' + a + r'="([^"]*)"', el or "" ) + return m.group( 1 ) if m else None +fw, nr, wd = f"{tmp}/fw", f"{tmp}/norule", f"{tmp}/wide" + +t = run( fw, "--path=post_message,on_*" ) +hr = root( t, "handler_rule" ) +check( xml_ok( t ), "A1 answer parses as XML" ) +check( attr( root( t, "path" ), "reachable" ) == "0", "A1 reachable stays 0" ) +check( attr( hr, "p" ) == "message.py:14" and attr( hr, "base" ) == "Message" and attr( hr, "attr" ) == "handler_name", f"A1 the rule is read from message.py:14 ({hr[:200]})" ) +check( '' in ( t or "" ), "A1 the reader passes the name to the MRO lookup" ) +check( re.search( r'', t or "" ) is not None, "A1 Button.Pressed -> on_button_pressed, cands=2" ) +check( '", t or "" ) +check( br is not None and '' in br.group( 0 ), f"A4 bridge tick -> on_tick via=name-key cands=1 ({br.group( 0 )[:240] if br else t and t[-300:]})" ) +check( attr( root( t, "path" ), "reachable" ) == "0" and attr( root( t, "path" ), "hops" ) == "0", "A4 reachable/hops stay 0" ) +t = run( fw, "--path=poke,leaf" ) +check( t is not None and 'in="poke"' not in t and ", no clause" ) +t = run( nr, "--impact=widgets.py:on_button_pressed" ) +check( t is not None and attr( root( t, "impact" ), "reaches_hedged_via" ) is None, "N2 no rule: no name-key hop" ) +t = run( wd, "--path=post_message,on_*" ) +hr = root( t, "handler_rule" ) +check( attr( hr, "handlers" ) == "17" and " on_command)" ) +d = fixture( "r3b", msg, "from case import camel_to_snake\n\n\n" + CMD % ( ' name = camel_to_snake(cls.__name__)\n cls.handler_name = f"do_{name}"', "do_save" ) ) +t = run( d, "--path=post_message,do_*" ) +hr = root( t, "handler_rule" ) +check( 'base="Command"' in hr and 'name="do_{snake(Class)}"' in hr and 'h="do_save"' in ( t or "" ) and hedged( d, "do_save" ) == "1" and hedged( d, "on_save" ) is None, + f"R3 the nearest rule (Command's do_) wins in the name AND in the block's rule pick ({hr[:160]})" ) + +# R4: a same-named base of another module +TWO = { "other/__init__.py": "", "other/message.py": "class Message:\n pass\n", + "other/widgets.py": "from other.message import Message\nfrom pump import Pump\n\n\nclass Ping(Message):\n pass\n\n\nclass Sender(Pump):\n def send(self):\n self.post_message(Ping())\n\n\nclass Form(Pump):\n def on_ping(self, m):\n return m\n" } +d = fixture( "r4", msg, "class Pong(Message):\n pass\n\n\nclass Caller(Pump):\n def call(self):\n self.post_message(Pong())\n\n\nclass Panel(Pump):\n def on_pong(self, m):\n return leaf(m)\n", TWO ) +t = run( d, "--path=post_message,on_ping" ) +check( t is not None and "" ) +sys.exit( fail ) +PY +rc=$? # the harness's own status, read immediately (checklist 14): 0 every arm passed, 1 an arm failed, anything else a crash +fail=0 +if [ "$rc" -ne 0 ]; then + printf ' FAIL the arm harness exited %s (an arm failed above, or the harness crashed)\n' "$rc" + fail=1 +fi +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit "$fail" diff --git a/test/hazardpatterncheck.sh b/test/hazardpatterncheck.sh index f1478bd1a..b576933e2 100755 --- a/test/hazardpatterncheck.sh +++ b/test/hazardpatterncheck.sh @@ -559,7 +559,7 @@ verbs_change.h runCommandCapture pipe 1 both ends closed in the child, the write verbs_doctor.h doctorParseProbe ts_parser_new 1 deleted before the return verbs_doctor.h doctorParseProbe ts_parser_parse_string 1 deleted inside the same if verbs_doctor.h doctorProbeGrammars ts_query_new 1 a compile probe, deleted inside the same if -verbs_for.h appendSubjectGraphVerb dup 1 closed on the failure path and after the restore on the success path (runWithCompactLegend's capture, for the --for graph-verb block) +verbs_for.h appendSubjectGraphVerbBlock dup 1 closed on the failure path and after the restore on the success path (runWithCompactLegend's capture, for the --for graph-verb block) REGE judge_static(){ # $1 = scan output dir, $2 = label (src | probe); echoes one line per violation diff --git a/test/hopbodycheck.sh b/test/hopbodycheck.sh new file mode 100644 index 000000000..d1de84006 --- /dev/null +++ b/test/hopbodycheck.sh @@ -0,0 +1,424 @@ +#!/usr/bin/env bash +# hopbodycheck.sh — THE BODY OF A MIDDLE HOP (lane path-hop-body, train 41; src/hopbody.h). +# +# THE CONTRACT THIS PINS (arms tagged [P] are RED on the pre-change binary 5163ffbdb; [G] arms are twins and near-miss negatives +# that hold on both binaries and must keep holding — PROCESS rule 4, no gate erosion): +# +# PATH-HOP-BODY a reached --path (and MCP path_between) whose shown chain has 2 hops prints, after its hop rows, +# holding the body of its INTERMEDIATE hop (never an endpoint), each +# the --expand body element (b t= l= p= n=, CDATA = the definition's source text, no callee rows). A body +# over 4000 B is cut at a line end and says so (truncated="1" lines="1-K/T" next= serves the rest). A chain of +# more hops prints no body and its root carries next= (the first middle hop's --expand). Inside a --for answer's +# graph-verb block a middle hop whose text the answer already carries (a served class holding it) prints no copy. +# WRAPPER-FOLLOW a --for fold body that is a THIN DELEGATOR (one proven callee, no other call, at most 3 statement lines) is +# followed by its callee's body , at most 2 levels, never a text the answer already +# carries; a by-name edge, a second call, or a fourth statement line is not a delegator. +# LEGEND every new reading is present-only: the compact path reading `bodies of=hops`, the full path clause, the +# fold's follow_of= clause ride exactly when the element/attribute does. +# A/B RIPWIRE_NO_HOP_BODY=1 is the answer without either mechanism. +# +# Arms (colour on 5163ffbdb in brackets): +# P1 [P] fetch -> dispatch -> compose: one with dispatch's body, CDATA = its source text; no endpoint body +# P2 [P] a 3-hop chain (past the gate) prints no body; root next="--expand=src/chain.ts:5:stepTwo" +# P3 [P] a 4-hop chain prints no body; root next="--expand=src/chain.ts:5:stepTwo", and that call serves stepTwo's body +# P9 [P] the --for graph-verb block: gatewayEntry -> relayHop prints relayHop's body (P9a); Server.fetch's twin, whose +# answer already serves the Server class holding dispatch, prints no copy (P9b) — kills a NodeId-only dedupe +# P4 [P] a middle body over the guard: truncated="1" lines="1-K/T" next=, capped="1", served text <= 4000 B, next= serves the rest +# P5 [G] a 1-hop chain and an unreached pair: no , no next= +# P6 [P] legend present-only, compact and full: the reading rides iff the element does; the past-the-gate next= reading in full +# P7 [P] MCP path_between prints the CLI's element byte for byte (P1, P4) and the 3- and 4-hop next= +# P8 [G] RIPWIRE_NO_HOP_BODY=1: the path answer is the head answer minus the element, its clause and the 4-hop next= +# F1 [P] the 2-level follow: handleRequest -> forwardInput -> runPipeline, each right after its wrapper, +# CDATA = the source text; the follow_of clause rides +# F2 [G] a one-call body of four statement lines (handleTimedRequest) is never followed +# F3 [G] a two-call body (listen) is never followed; a delegate inside a served body (fetch -> dispatch in Server) prints no copy +# F4 [G] a by-name delegate (worker.crunchInput, via=name) is never followed +# F5 [G] each text once: no two of one answer share p= and l= +# F6 [G] RIPWIRE_NO_HOP_BODY=1: no follow_of=, no clause, answer == head minus the follow bodies and clause +# X1 [G] determinism X2 [G] xmllint well-formed X3 [G] no "--" inside a legend comment +# I [optional, RIPWIRE_BASE_BIN=] RIPWIRE_NO_HOP_BODY=1 head == the base binary on every question here +# +# FIX1 arms (lane path-hop-body-fix1; [M] = RED on the path-hop-body head 5351fbbb / the merge binary, the review's findings): +# H1 [M] a BY-NAME middle hop (P1.process / P2.process): its body carries via="name" cands="2" on the (F1), the hedge clause +# rides; a proven middle hop (P1's dispatch) carries none +# H2 [M] a declined iface call (ts15 m.match) or an undefined call (missingHelperCall) beside the one proven call: never followed (F2) +# H3 [M] a call bound only through a pybind11 binding (amb=, one call site): never followed — pins the ambOut guard (review mutant MA) +# H4 [M] C stores `*out = …` are statement lines: scale_and_store is not a thin wrapper (F3) +# H5 [M] a documented Python delegator is followed (F4), follow_of names the wrapper as Owner.name (F7) +# H6 [M] a class with a field initializer is never a wrapper (F5) +# H7 [M] logic beside one call (computeScoreTotal) and a .filter().map().reduce() pipeline are not delegators (F2/F6) +# H8 [M] a one-line wrapper over a > 4000 B callee: the follow is cut, truncated="1" lines= next= (kills the review's mutant MB) +# H9 [M] the answer-wide follow guard: 3 follow bodies, then naming the first unfollowed callee +# R1 [M] BODY-ON-ROUTE: a reachable=0 poster -> queue -> dispatch answer prints with post_message, +# _process_messages_loop (via="queue" cands="2"), _dispatch_message and the hrd reader _on_message (via="name-key"), CDATA = +# source; never the to= row (on_event); reachable= and hops= unchanged; the of=route clause rides (compact and full) +# R2 [M] the route spew guard: a fifth route body is cut, capped="1" next="--expand=pump.py:45:_trace_message" +# R3 [M] the hop bound: the 4-hop bridge press -> on_event prints its rows 0..2 only (press, post_message, the loop) +# H4b[M] three `*p =` stores before the one call: 4 statement lines, never followed (pins the * comment rule alone) +# R6-R9 [M] FIX2 (review rv-phbf1): R6 a by-name bridge edge row's body carries via=name cands=2 (G1); R7 a > 4000 B route body is cut, +# truncated=1 lines= next= (G2); R8 a reader that is also a bridge row prints once (G3); R9 a slot crossing's body carries slot_fns= +# and the bare-b reading rides (S1, S4) +# R10 [M] CR pre-pass 41: a route/hop body prints a definition, never lists it again (post_message bound to no definition because +# a second class defines it too): lists the other class's post_message, not the pump.py:10 the route body +# printed; the same on MCP path_between; [G] a definition no body printed is still listed +# R5 [G] the gain gate: inside a --for answer's graph-verb block the unreached path prints no route bodies +# R4 [G] RIPWIRE_NO_HOP_BODY=1: no route element; MCP path_between prints the CLI's route element byte for byte +# +# Usage: bash test/hopbodycheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/hopbodycheck.sh +# BIN is $1, else RIPWIRE_BIN, else build/ripwire; the first output line names the binary used. Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" +unset RIPWIRE_NO_HOP_BODY RIPWIRE_NO_BODY_FOLD RIPWIRE_NO_BODY_SLOT +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "hopbodycheck: BIN=$BIN" +fail=0 +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +cd "$ROOT" +WEB="test/hopbodyfix/web"; FOL="test/hopbodyfix/follow"; LOO="test/hopbodyfix/loose" +[ -d "$WEB/src" ] && [ -d "$FOL/src" ] && [ -d "$LOO/src" ] || { echo "fixture test/hopbodyfix missing"; exit 2; } + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +export HOME="$TMP/home"; mkdir -p "$HOME" +run(){ local d="$1"; shift; ( cd "$d" || exit 1; "$BIN" "$ROOT/$d" --no-cache "$@" 2>/dev/null ); } + +T_FOLLOW="How is a request handled?" +T_TIMED="How does handling a request work?" +T_WEB="How does a request flow from fetch to its Response?" +T_LISTEN="How does the server listen for a request?" +T_LOOSE="How is a loose request handled?" + +run "$WEB" --path=fetch,compose >"$TMP/p1" +run "$WEB" --path=stepOne,stepFour >"$TMP/p2" +run "$WEB" --path=stepOne,stepFive >"$TMP/p3" +run "$WEB" --path=bigEntry,bigSink >"$TMP/p4" +run "$WEB" --path=stepFour,stepFive >"$TMP/p5a" +run "$WEB" --path=compose,fetch >"$TMP/p5b" +run "$WEB" --for="Trace the path from gatewayEntry() to finalStage()" >"$TMP/p9a" +run "$WEB" --for="Trace the path from Server.fetch to compose()" >"$TMP/p9b" +run "$WEB" --path=fetch,compose --legend=full >"$TMP/p1f" +run "$WEB" --path=stepOne,stepFive --legend=full >"$TMP/p3f" +run "$WEB" --path=stepFour,stepFive --legend=full >"$TMP/p5af" +RIPWIRE_NO_HOP_BODY=1 run "$WEB" --path=fetch,compose >"$TMP/p1.off" +RIPWIRE_NO_HOP_BODY=1 run "$WEB" --path=stepOne,stepFour >"$TMP/p2.off" +RIPWIRE_NO_HOP_BODY=1 run "$WEB" --path=stepOne,stepFive >"$TMP/p3.off" +RIPWIRE_NO_HOP_BODY=1 run "$WEB" --path=bigEntry,bigSink >"$TMP/p4.off" +run "$FOL" --for="$T_FOLLOW" >"$TMP/f1" +run "$FOL" --for="$T_FOLLOW" --legend=full >"$TMP/f1f" +RIPWIRE_NO_HOP_BODY=1 run "$FOL" --for="$T_FOLLOW" >"$TMP/f1.off" +run "$FOL" --for="$T_TIMED" >"$TMP/f2" +run "$WEB" --for="$T_WEB" >"$TMP/f3a" +run "$WEB" --for="$T_LISTEN" >"$TMP/f3b" +run "$LOO" --for="$T_LOOSE" >"$TMP/f4" +run "$LOO" --callees=handleLooseRequest >"$TMP/f4c" + +# FIX1 fixtures (the route ones run from a copy outside test/: a reader under a test path is not a handler_rule reader) +HB="test/hopbodyfix" +cp -R "$HB/route" "$HB/routecap" "$HB/routeslot" "$TMP/" +run "$HB/byname" --path=startJ,sinkJ >"$TMP/h1" +run "$HB/byname" --path=startJ,sinkJ --legend=full >"$TMP/h1f" +run "$HB/declined" --for="how does relayWorkItem relay a work item" >"$TMP/h2a" +run "$HB/declined" --callees=relayWorkItem >"$TMP/h2c" +run "$HB/external" --for="how does relayWorkItem relay a work item" >"$TMP/h2b" +run "$HB/ffi" --for="How does relay_transform relay a value?" >"$TMP/h3" +run "$HB/ffi" --callees=relay_transform >"$TMP/h3c" +run "$HB/cstore" --for="how does scale_and_store scale and store" >"$TMP/h4" +run "$HB/cstore" --for="how does prime_then_fill prime the buffers then fill" >"$TMP/h4b" +run "$HB/pydoc" --for="how does Widget post_message post a message" >"$TMP/h5a" +run "$HB/pydoc" --for="how does Widget post_message_plain post a message" >"$TMP/h5b" +run "$HB/klass" --for="how does PanelHolder compute the panel size for the holder" >"$TMP/h6" +run "$HB/klass" --for="how does ChildRunner launch the runner" >"$TMP/h6b" +run "$HB/logic" --for="how does computeScoreTotal compute the score total" >"$TMP/h7a" +run "$HB/logic" --for="how does pipelineScore score the pipeline items" >"$TMP/h7b" +run "$HB/bigfollow" --for="how does relayWorkItem relay a work item" >"$TMP/h8" +run "$HB/many" --for="How does shipOrder ship an order?" >"$TMP/h9" +run "$HB/many" --for="How does shipOrder ship an order?" --legend=full >"$TMP/h9f" +rrun(){ local d="$1"; shift; ( cd "$TMP/$d" || exit 1; "$BIN" "$TMP/$d" --no-cache "$@" 2>/dev/null ); } +rrun route '--path=post_message,on_*' >"$TMP/r1" +rrun route '--path=post_message,on_*' --legend=full >"$TMP/r1f" +rrun routecap '--path=post_message,on_*' >"$TMP/r2" +rrun route '--path=press,on_event' >"$TMP/r3" +rrun route --for="Trace the path from post_message() to on_event()" >"$TMP/r5" +# the R6-R8 fixtures are the route fixture with one edit each (so no committed clone): R6 a by-name call (two same-named routers), R7 four bodies +# over 4000 B, R8 the reader _on_message is also reached as a bridge row (the loop calls it by way of _dispatch_message) +python3 - "$TMP" <<'PY' +import os, shutil, sys +T = sys.argv[1]; src = open(os.path.join(T, "route", "pump.py")).read().split("\n") +def make(name, lines): + shutil.copytree(os.path.join(T, "route"), os.path.join(T, name)); open(os.path.join(T, name, "pump.py"), "w").write("\n".join(lines)) +by = [l.replace("self._dispatch_message(message)", "self.router.dispatch_message(message)") for l in src] +make("routename", by + ["", "", "class RouterA:", " def dispatch_message(self, message):", " self.pump.on_event(message)", "", "", "class RouterB:", " def dispatch_message(self, message):", " return message", ""]) +ded = list(src); ded[26:26] = [" name = message.handler_name", " getattr(self, name)(message)"] +make("routededupe", ded) +big = list(src) +for at in (33, 25, 20, 12): big[at:at] = [" x%d = len(str(message)) + %d" % (i, i) for i in range(160)] +make("routebig", big) +shutil.copytree(os.path.join(T, "route"), os.path.join(T, "routealt")) +open(os.path.join(T, "routealt", "other.py"), "w").write("class Other:\n def post_message(self, message):\n return message\n") +PY +rrun routename '--path=post_message,on_*' >"$TMP/r6" +rrun routebig '--path=post_message,on_*' >"$TMP/r7" +rrun routededupe '--path=post_message,on_*' >"$TMP/r8" +rrun routealt '--path=post_message,on_*' >"$TMP/r10" +rrun routeslot '--path=start_job,sink_write' >"$TMP/r9" +rrun routeslot '--path=start_job,sink_write' --legend=full >"$TMP/r9f" +( cd "$TMP/route" && RIPWIRE_NO_HOP_BODY=1 "$BIN" "$TMP/route" --no-cache '--path=post_message,on_*' 2>/dev/null ) >"$TMP/r1.off" + +cat > "$TMP/chk.py" <<'PY' +import html, re, shlex, shutil, subprocess, sys, os +T, BIN, ROOT = sys.argv[1], sys.argv[2], sys.argv[3] +fails = 0 +def rd(n): return open(os.path.join(T, n), "rb").read().decode("utf-8", "replace") +def ok(m): print(" PASS " + m) +def no(m): + global fails; fails += 1; print(" FAIL " + m) +def check(c, m): ok(m) if c else no(m) +def attrs(s): return dict((k, html.unescape(v)) for k, v in re.findall(r'(\w+)="([^"]*)"', s)) +def cdata(x): return "".join(re.findall(r'', x, re.S)) +def bodies(x): return [(attrs(m.group(1)), cdata(m.group(2)), m.group(0)) for m in re.finditer(r']*)>(.*?)', x, re.S)] +def hopel(t): + m = re.search(r']*>.*?', t, re.S); return m.group(0) if m else "" +def root(t): + m = re.search(r']*)>', t); return attrs(m.group(1)) if m else {} +def legend(t): return " ".join(re.findall(r'', t, re.S)) +def is_source(b, d): + """the body's CDATA is its definition's source text: a substring of the file p= starting on line l=""" + a, cd = b[0], b[1] + try: src = open(os.path.join(ROOT, d, a.get("p", "")), encoding="utf-8").read() + except OSError: return False + i = src.find(cd) + return cd != "" and i >= 0 and src[:i].count("\n") + 1 == int(a.get("l", "0")) +def paste(nxt, d): + argv = shlex.split(nxt) + return subprocess.run([BIN, os.path.join(ROOT, d), "--no-cache"] + argv, cwd=os.path.join(ROOT, d), capture_output=True).stdout.decode("utf-8", "replace") +WEB, FOL = "test/hopbodyfix/web", "test/hopbodyfix/follow" + +# P1 +p1 = rd("p1"); el = hopel(p1); bs = bodies(el) +check(len(bs) == 1 and bs[0][0].get("n") == "dispatch" and bs[0][0].get("l") == "10", "(P1) one middle-hop body: dispatch l=10") +check(bs and is_source(bs[0], WEB) and "return composed(request)" in bs[0][1], "(P1) its CDATA is the definition's source text, whole") +check(not re.search(r']*n="(fetch|compose)"', el) and "', t, re.S) or re.search(r'$', t)).group(0) +g9a, g9b = gv(rd("p9a")), gv(rd("p9b")) +b9a = bodies(hopel(g9a)) +check(g9a != "" and len(b9a) == 1 and b9a[0][0].get("n") == "relayHop" and is_source(b9a[0], WEB), "(P9a) the graph-verb path block prints the middle hop's body") +check(re.search(r']*n="Server"', rd("p9b")) and g9b != "" and hopel(g9b) == "", "(P9b) a middle hop inside a body the answer serves prints no copy") +# P3 +p3 = rd("p3"); r = root(p3) +check(hopel(p3) == "" and r.get("hops") == "4", "(P3) a 4-hop chain prints no body") +check(r.get("next") == "--expand=src/chain.ts:5:stepTwo", "(P3) root next= is the first middle hop's --expand: %r" % r.get("next")) +check(r.get("next") and "return stepThree(n * 2)" in cdata(paste(r["next"], WEB)), "(P3) the pasted next= serves stepTwo's body") +# P4 +el = hopel(rd("p4")); bs = bodies(el); a = bs[0][0] if bs else {} +check(a.get("truncated") == "1" and re.fullmatch(r'1-\d+/\d+', a.get("lines", "")) and a.get("next", "").startswith("--expand=src/big.ts:5:bigMiddle:"), "(P4) the long middle body is cut and says so: %r" % {k: a.get(k) for k in ("truncated", "lines", "next")}) +check('capped="1"' in el and bs and len(bs[0][1].encode()) <= 4000, "(P4) capped=1 and the served text is <= 4000 B") +if bs and a.get("next"): + rest = cdata(paste(a["next"], WEB)); hi = int(a["lines"].split("-")[1].split("/")[0]); tot = int(a["lines"].split("/")[1]) + check(rest.count("\n") + 1 == tot - hi and bs[0][1].endswith("\n") is False, "(P4) next= serves exactly the lines the cut left (%d of %d)" % (tot - hi, tot)) +# P5 +for f in ("p5a", "p5b"): + t = rd(f); check(hopel(t) == "" and "next=" not in (re.search(r']*>', t).group(0) if re.search(r']*>', t) else ""), "(P5) %s: no , no root next=" % f) +check('reachable="0"' in rd("p5b"), "(P5) compose,fetch is the unreached twin") +# P6 +check("bodies of=hops" in legend(p1) and "bodies of=hops" not in legend(rd("p3")) and "bodies of=hops" not in legend(rd("p5a")), "(P6) compact: the reading rides iff the element does") +check("bodies of=hops" in legend(rd("p1f")) and "bodies of=hops" not in legend(rd("p5af")), "(P6) full: the clause rides iff the element does") +check("first middle hop" in legend(rd("p3f")) and "first middle hop" not in legend(rd("p1f")), "(P6) full: the 4-hop next= reading rides iff that next= does") +# P8 +for f in ("p1", "p4"): + on, off = rd(f), rd(f + ".off") + strip = re.sub(r'', '', on, flags=re.S) + strip = re.sub(r' bodies of=hops: .*?(?= -->|\. (?=[a-z_]+[=:])|$)\.?', '', strip, flags=re.S) + check(hopel(off) == "" and re.sub(r'', '', strip, flags=re.S) == re.sub(r'', '', off, flags=re.S), "(P8) %s: RIPWIRE_NO_HOP_BODY=1 == head minus the element (payload)" % f) +check('next=' not in re.search(r']*>', rd("p3.off")).group(0) and 'next=' not in re.search(r']*>', rd("p2.off")).group(0), "(P8) RIPWIRE_NO_HOP_BODY=1: no past-the-gate next=") +# F1 +f1 = rd("f1"); bs = bodies(f1); names = [b[0].get("n") for b in bs] +fo = {b[0].get("n"): b for b in bs if "follow_of" in b[0]} +check(fo.get("forwardInput", [{}])[0].get("follow_of") == "handleRequest" and fo.get("runPipeline", [{}])[0].get("follow_of") == "forwardInput", "(F1) the 2-level follow: %r" % {k: v[0].get("follow_of") for k, v in fo.items()}) +check("handleRequest" in names and names.index("forwardInput") == names.index("handleRequest") + 1 and names.index("runPipeline") == names.index("forwardInput") + 1 if "forwardInput" in names and "runPipeline" in names and "handleRequest" in names else False, "(F1) each follow right after its wrapper") +check("forwardInput" in fo and is_source(fo["forwardInput"], FOL) and "runPipeline" in fo and is_source(fo["runPipeline"], FOL) and "segments.length === 0" in fo["runPipeline"][1], "(F1) follow CDATA are the definitions' source text, whole") +check("follow_of=W" in legend(f1) and "follow_of=W" in legend(rd("f1f")), "(F1) the follow_of clause rides in both legends") +# F2-F4 +check(not re.search(r'follow_of="handleTimedRequest"', rd("f2")) and re.search(r']*n="handleTimedRequest"', rd("f2")) and not re.search(r']*n="stampInput"', rd("f2")), "(F2) the 4-statement-line wrapper is served and never followed (its callee stampInput never prints)") +check("follow_of=" not in rd("f3a") and "follow_of=W" not in legend(rd("f3a")), "(F3) fetch's delegate lies inside the served Server body: no copy, no clause") +check(re.search(r']*n="listen"', rd("f3b")) and 'follow_of="listen"' not in rd("f3b"), "(F3) the two-call listen is never followed") +check('via="name"' in rd("f4c") and re.search(r']*n="handleLooseRequest"', rd("f4")) and "follow_of=" not in rd("f4"), "(F4) a by-name delegate is never followed") +# F5 +for f in ("f1", "f2", "f3a", "f3b", "f4", "p1", "p9a", "p9b"): + keys = [(b[0].get("p"), b[0].get("l")) for b in bodies(rd(f))] + check(len(keys) == len(set(keys)), "(F5) %s: each text once" % f) +# F6 +off = rd("f1.off") +check("follow_of" not in off, "(F6) RIPWIRE_NO_HOP_BODY=1: no follow_of= and no clause") +strip = re.sub(r']*follow_of="[^"]*"[^>]*>.*?', '', f1, flags=re.S) +mask = lambda t: re.sub(r' est_tokens="\d+"', '', re.sub(r'', '', t, flags=re.S)) +check(mask(strip) == mask(off), "(F6) the --for answer minus the follow bodies == the RIPWIRE_NO_HOP_BODY=1 answer (payload, est_tokens masked)") +# FIX1 H1-H9: the review's findings +el = hopel(rd("h1")); bs = bodies(el) +check(len(bs) == 1 and bs[0][0].get("n") == "process" and bs[0][0].get("via") == "name" and bs[0][0].get("cands") == "2", "(H1) a by-name middle hop's body carries via=name cands=2: %r" % [b[0] for b in bs]) +check("b via= cands=" in legend(rd("h1")) and "no proven call" in legend(rd("h1f")), "(H1) the hedge clause rides, compact and full") +check(bodies(hopel(rd("p1"))) and "via" not in bodies(hopel(rd("p1")))[0][0] and "b via= cands=" not in legend(rd("p1")), "(H1) a proven middle hop carries no hedge and no clause") +fol = lambda t: [b[0].get("follow_of") for b in bodies(t) if "follow_of" in b[0]] +check('via="declined"' in rd("h2c") and re.search(r']*n="relayWorkItem"', rd("h2a")) and fol(rd("h2a")) == [], "(H2) a declined call beside the proven one: relayWorkItem served, never followed") +check(re.search(r']*n="relayWorkItem"', rd("h2b")) and fol(rd("h2b")) == [], "(H2) an undefined call beside the proven one: never followed") +check('graph_ambiguous="1"' in rd("h3c") and re.search(r']*n="relay_transform"', rd("h3")) and fol(rd("h3")) == [] and not re.search(r']*n="scale_impl"', rd("h3")), "(H3) a binding-only call (amb=) is never followed") +check(re.search(r']*n="scale_and_store"', rd("h4")) and fol(rd("h4")) == [], "(H4) C *out stores count as statements: no follow") +check(re.search(r']*n="prime_then_fill"', rd("h4b")) and fol(rd("h4b")) == [], "(H4b) three *p stores + the call = 4 statement lines: no follow (pins the * rule alone)") +check(fol(rd("h5a")) == ["Widget.post_message"] and fol(rd("h5b")) == ["Widget.post_message_plain"], "(H5) documented and plain Python delegators both followed, follow_of=Owner.name: %r %r" % (fol(rd("h5a")), fol(rd("h5b")))) +check(re.search(r']*n="PanelHolder"', rd("h6")) and fol(rd("h6")) == [], "(H6) a class is never a wrapper") +check(fol(rd("h6b")) == ["ChildRunner.launch"], "(H5/F7) a super-override follow names its wrapper Owner.name: %r" % fol(rd("h6b"))) +check(re.search(r']*n="computeScoreTotal"', rd("h7a")) and fol(rd("h7a")) == [] and fol(rd("h7b")) == [], "(H7) logic beside one call and a builtin pipeline are not delegators: %r %r" % (fol(rd("h7a")), fol(rd("h7b")))) +fb = [b for b in bodies(rd("h8")) if "follow_of" in b[0]]; fa = fb[0][0] if fb else {} +check(fa.get("follow_of") == "relayWorkItem" and fa.get("truncated") == "1" and re.fullmatch(r'1-\d+/\d+', fa.get("lines", "")) and fa.get("next", "").startswith("--expand=src/work.ts:") and len(fb[0][1].encode()) <= 4000, "(H8) the follow of a > 4000 B callee is cut and says so: %r" % {k: fa.get(k) for k in ("follow_of", "truncated", "lines", "next")}) +h9 = rd("h9"); fc = re.search(r']*)/>', h9) +check(len(fol(h9)) == 3 and fc and attrs(fc.group(1)) == {"n": "2", "next": "--expand=src/ship.ts:19:sealOrder2"}, "(H9) 3 follow bodies, then : %r %r" % (fol(h9), fc.group(0) if fc else None)) +check("follow_cut n= next=" in legend(h9) and "follow_cut n= next=" in legend(rd("h9f")), "(H9) the follow_cut reading rides, compact and full") +# FIX1 R1-R4: BODY-ON-ROUTE +routel = lambda t: (re.search(r'', t, re.S) or re.search(r'$', t)).group(0) +r1 = rd("r1"); el = routel(r1); bs = bodies(el); rt = root(r1) +check(rt.get("reachable") == "0" and rt.get("hops") == "0" and re.search(r']*>', r1), "(R1) reachable=0 hops=0 and the bridge stay") +check([b[0].get("n") for b in bs] == ["post_message", "_process_messages_loop", "_dispatch_message", "_on_message"], "(R1) the route bodies in order: %r" % [b[0].get("n") for b in bs]) +check(el.startswith(''), "(R1) envelope proven=0 shown=4 total=4 capped=0") +hed = {b[0].get("n"): (b[0].get("via"), b[0].get("cands")) for b in bs} +check(hed.get("_process_messages_loop") == ("queue", "2") and hed.get("_on_message") == ("name-key", None) and hed.get("post_message") == (None, None) and hed.get("_dispatch_message") == (None, None), "(R1) each body carries its hop's hedge: %r" % hed) +check(all(is_source((b[0], b[1]), os.path.join(T, "route")) for b in bs) and "put_nowait" in el and "self._parent.post_message" in el, "(R1) CDATA = source text (the put, the bubble)") +check('n="on_event"' not in el, "(R1) never the to= row's body") +check("bodies of=route proven=0" in legend(r1) and "bodies of=route proven=0" in legend(rd("r1f")) and "b via= cands=" in legend(r1), "(R1) the of=route and hedge clauses ride, compact and full") +el2 = routel(rd("r2")) +check(el2.startswith('') and len(bodies(el2)) == 4, "(R2) a fifth route body is cut: capped=1, next= the first unshown") +b3 = [b[0].get("n") for b in bodies(routel(rd("r3")))] +check(b3 == ["press", "post_message", "_process_messages_loop"], "(R3) a 4-hop bridge prints rows 0..2 only: %r" % b3) +g5 = (re.search(r'', rd("r5"), re.S) or re.search(r'$', rd("r5"))).group(0) +check(g5 != "" and 'reachable="0"' in g5 and routel(g5) == "" and "bodies of=route" not in legend(rd("r5")), "(R5) a --for graph-verb path block prints no route bodies (the gain gate)") +check(routel(rd("r1.off")) == "" and "bodies of=route" not in legend(rd("r1.off")), "(R4) RIPWIRE_NO_HOP_BODY=1: no route element, no clause") +# FIX2 R6-R9 (lane path-hop-body-fix1, review rv-phbf1 G1/G2/G3/S1/S4: each arm is RED on its review mutant) +rb = lambda f: {b[0].get("n"): b[0] for b in bodies(routel(rd(f)))} +b6 = rb("r6") +check(b6.get("dispatch_message", {}).get("via") == "name" and b6.get("dispatch_message", {}).get("cands") == "2", "(R6) a by-name edge row inside the bridge: its route body carries via=name cands=2, never bare (the edge-row hedge): %r" % b6.get("dispatch_message")) +b7 = bodies(routel(rd("r7"))) +check(len(b7) == 4 and all(b[0].get("truncated") == "1" and re.fullmatch(r'1-\d+/\d+', b[0].get("lines", "")) and b[0].get("next", "").startswith("--expand=pump.py:") and len(b[1].encode()) <= 4000 for b in b7) and routel(rd("r7")).startswith(''), "(R7) a route body over 4000 B is cut: truncated=1 lines= next=, <= 4000 B each, capped=1") +b8 = bodies(routel(rd("r8"))); n8 = [b[0].get("n") for b in b8] +check(n8 == ["post_message", "_process_messages_loop", "_dispatch_message", "_on_message"] and len(set(n8)) == len(n8), "(R8) a reader that is also a bridge row prints once and the true reader _on_message is still shown: %r" % n8) +b9 = rb("r9") +check(b9.get("run_fast", {}).get("via") == "slot" and b9.get("run_fast", {}).get("slot_callers") == "1" and b9.get("run_fast", {}).get("slot_fns") == "3", "(R9) a slot-crossing route body carries slot_fns= beside slot_callers= (S1): %r" % b9.get("run_fast")) +check("proven call from the row above" in legend(rd("r9")) and "proven call from the row above" in legend(rd("r9f")), "(R9) the bare-b reading rides beside a hedged route, compact and full (S4)") +# R10 (CR pre-pass 41): defs-lookup's must not list a definition the route body printed (pathAltsOf's bodyIds) +def alts_p(t): return re.findall(r'.*?', t, re.S) or re.search(r'$', t)).group(0)) +r10 = rd("r10"); printed10 = {(b[0].get("p"), b[0].get("l")) for b in bodies(routel(r10))} +check(("pump.py", "10") in printed10 and root(r10).get("from_defs") == "2", "(R10pre) the route body prints pump.py:10 post_message and the endpoint binds 2 definitions") +check(alts_p(r10) == ["other.py:2"], "(R10) lists the other class's post_message only, never the one a route body printed: %r" % alts_p(r10)) +check(all(a.split(":")[0] + ":" + a.split(":")[1] not in {p + ":" + l for p, l in printed10} for a in alts_p(r10)), "(R10) no row repeats a printed ") +if shutil.which("xmllint"): # CR 4408459933: the shell X2 arm below skips cleanly without xmllint; so does this one + for f in ("h1", "r1", "r2", "r3"): + check(subprocess.run(["xmllint", "--noout", os.path.join(T, f)], capture_output=True).returncode == 0, "(X2) %s well-formed" % f) +else: + print(" SKIP (X2) xmllint absent") +# X3 +for f in ("p1", "p1f", "p3f", "p4", "f1", "f1f", "h1f", "r1f", "h9f"): + check(all("--" not in c for c in re.findall(r'', rd(f), re.S)), "(X3) %s: no -- inside a legend comment" % f) +sys.exit(1 if fails else 0) +PY +python3 "$TMP/chk.py" "$TMP" "$BIN" "$ROOT" || fail=1 + +# P7: MCP path_between twin +python3 - "$BIN" "$ROOT/$WEB" "$TMP" <<'PY' || fail=1 +import json, re, subprocess, sys +binp, web, tmp = sys.argv[1], sys.argv[2], sys.argv[3] +cases = [("fetch", "compose", "p1"), ("stepOne", "stepFour", "p2"), ("bigEntry", "bigSink", "p4"), ("stepOne", "stepFive", "p3")] +reqs = [{"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "g", "version": "1"}}}] +for i, (a, b, _) in enumerate(cases): + reqs.append({"jsonrpc": "2.0", "id": i + 1, "method": "tools/call", "params": {"name": "path_between", "arguments": {"path": web, "from": a, "to": b}}}) +p = subprocess.run([binp, "--mcp", "--mcp-legend=inline"], input="".join(json.dumps(r) + "\n" for r in reqs), capture_output=True, text=True, timeout=120, cwd=web) +resp = {} +for line in p.stdout.splitlines(): + try: o = json.loads(line) + except ValueError: continue + if isinstance(o.get("id"), int) and o["id"] > 0: + resp[o["id"]] = "".join(c.get("text", "") for c in o.get("result", {}).get("content", [])) +bad = 0 +el = lambda t: (re.search(r'', t, re.S) or re.search(r'$', t)).group(0) +for i, (a, b, f) in enumerate(cases): + cli = open(tmp + "/" + f).read(); mcp = resp.get(i + 1, "") + if f in ("p2", "p3"): + m = re.search(r']*next="([^"]*)"', mcp); good = bool(m) and m.group(1) == "--expand=src/chain.ts:5:stepTwo" and el(mcp) == "" + else: + good = el(mcp) != "" and el(mcp) == el(cli) + print(" %s (P7) MCP path_between %s,%s: %s" % ("PASS" if good else "FAIL", a, b, "the CLI's element" if f not in ("p2", "p3") else "the past-the-gate next=")) + bad += 0 if good else 1 +sys.exit(1 if bad else 0) +PY + +# R4: MCP path_between prints the CLI's route element byte for byte +python3 - "$BIN" "$TMP/route" "$TMP" <<'PY' || fail=1 +import json, re, subprocess, sys +binp, d, tmp = sys.argv[1], sys.argv[2], sys.argv[3] +reqs = [{"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "g", "version": "1"}}}, + {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "path_between", "arguments": {"path": d, "from": "post_message", "to": "on_*"}}}] +p = subprocess.run([binp, "--mcp", "--mcp-legend=inline"], input="".join(json.dumps(r) + "\n" for r in reqs), capture_output=True, text=True, timeout=120, cwd=d) +mcp = "" +for line in p.stdout.splitlines(): + try: o = json.loads(line) + except ValueError: continue + if o.get("id") == 1: mcp = "".join(c.get("text", "") for c in o.get("result", {}).get("content", [])) +el = lambda t: (re.search(r'', t, re.S) or re.search(r'$', t)).group(0) +good = el(mcp) != "" and el(mcp) == el(open(tmp + "/r1").read()) +print(" %s (R4) MCP path_between post_message,on_*: the CLI's route element" % ("PASS" if good else "FAIL")) +sys.exit(0 if good else 1) +PY + +# R10m: MCP path_between lists the same (the route body's definition is not listed again) +python3 - "$BIN" "$TMP/routealt" "$TMP" <<'PY' || fail=1 +import json, re, subprocess, sys +binp, d, tmp = sys.argv[1], sys.argv[2], sys.argv[3] +reqs = [{"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "g", "version": "1"}}}, + {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "path_between", "arguments": {"path": d, "from": "post_message", "to": "on_*"}}}] +p = subprocess.run([binp, "--mcp", "--mcp-legend=inline"], input="".join(json.dumps(r) + "\n" for r in reqs), capture_output=True, text=True, timeout=120, cwd=d) +mcp = "" +for line in p.stdout.splitlines(): + try: o = json.loads(line) + except ValueError: continue + if o.get("id") == 1: mcp = "".join(c.get("text", "") for c in o.get("result", {}).get("content", [])) +al = lambda t: (re.search(r'', t, re.S) or re.search(r'$', t)).group(0) +good = '' in mcp and al(mcp) == al(open(tmp + "/r10").read()) and 'p="pump.py:10"' not in al(mcp) and ', without the route body's definition" % ("PASS" if good else "FAIL")) +sys.exit(0 if good else 1) +PY + +# X1 determinism, X2 well-formed +run "$WEB" --path=fetch,compose >"$TMP/x1"; run "$FOL" --for="$T_FOLLOW" >"$TMP/x2" +if cmp -s "$TMP/p1" "$TMP/x1" && cmp -s "$TMP/f1" "$TMP/x2"; then echo " PASS (X1) deterministic"; else echo " FAIL (X1) two runs differ"; fail=1; fi +if command -v xmllint >/dev/null 2>&1; then + for f in p1 p2 p3 p4 p1f f1 f1f p9a p9b; do + if xmllint --noout "$TMP/$f" 2>/dev/null; then echo " PASS (X2) $f well-formed"; else echo " FAIL (X2) $f not well-formed"; fail=1; fi + done +else + echo " SKIP (X2) xmllint absent" +fi + +# I: optional base-binary identity of the A/B handle +if [ -n "${RIPWIRE_BASE_BIN:-}" ] && [ -x "${RIPWIRE_BASE_BIN}" ]; then + i=0 + while IFS='|' read -r d a; do + i=$(( i + 1 )) + ( cd "$d" || exit 1; "$RIPWIRE_BASE_BIN" "$ROOT/$d" --no-cache "$a" 2>/dev/null ) | sed 's/ at="[^"]*"//' >"$TMP/i.base" + ( cd "$d" || exit 1; RIPWIRE_NO_HOP_BODY=1 "$BIN" "$ROOT/$d" --no-cache "$a" 2>/dev/null ) | sed 's/ at="[^"]*"//' >"$TMP/i.head" + if [ -s "$TMP/i.base" ] && cmp -s "$TMP/i.base" "$TMP/i.head"; then echo " PASS (I$i) RIPWIRE_NO_HOP_BODY=1 == base: $d $a"; else echo " FAIL (I$i) differs from base: $d $a"; fail=1; fi + done < 100 ? 100 : v; + int lo = lim < 0 ? 0 : lim; + return lo * 2; +} +int store_and_scale_twice(int v) +{ + return ( v * 3 ) - ( v % 7 ); +} +int prime_then_fill(int *out, int *aux, int *tag, int n) +{ + *aux = n; + *tag = n + 1; + *out = n + 2; + return commit_slots(out, n); +} diff --git a/test/hopbodyfix/cstore/src/commit.c b/test/hopbodyfix/cstore/src/commit.c new file mode 100644 index 000000000..d57449636 --- /dev/null +++ b/test/hopbodyfix/cstore/src/commit.c @@ -0,0 +1,6 @@ +int commit_slots(int *out, int n) +{ + int COMMIT_MARK = n * 5; + *out = COMMIT_MARK; + return COMMIT_MARK; +} diff --git a/test/hopbodyfix/cstore/src/fill.c b/test/hopbodyfix/cstore/src/fill.c new file mode 100644 index 000000000..5923ad18f --- /dev/null +++ b/test/hopbodyfix/cstore/src/fill.c @@ -0,0 +1,6 @@ +int fill_buffer(int *out, int n) +{ + int FILL_MARK = n * 2; + *out = FILL_MARK; + return FILL_MARK; +} diff --git a/test/hopbodyfix/declined/src/m.ts b/test/hopbodyfix/declined/src/m.ts new file mode 100644 index 000000000..5c3271793 --- /dev/null +++ b/test/hopbodyfix/declined/src/m.ts @@ -0,0 +1,10 @@ +export interface Matcher { match(x: number): number; } +export class M1 implements Matcher { match(x: number): number { const k1 = x + 1; return k1 * 2; } } +export class M2 implements Matcher { match(x: number): number { const k2 = x + 2; return k2 * 2; } } +export class M3 implements Matcher { match(x: number): number { const k3 = x + 3; return k3 * 2; } } +export class M4 implements Matcher { match(x: number): number { const k4 = x + 4; return k4 * 2; } } +export class M5 implements Matcher { match(x: number): number { const k5 = x + 5; return k5 * 2; } } +export class M6 implements Matcher { match(x: number): number { const k6 = x + 6; return k6 * 2; } } +export class M7 implements Matcher { match(x: number): number { const k7 = x + 7; return k7 * 2; } } +export class M8 implements Matcher { match(x: number): number { const k8 = x + 8; return k8 * 2; } } +export class M9 implements Matcher { match(x: number): number { const k9 = x + 9; return k9 * 2; } } diff --git a/test/hopbodyfix/declined/src/relay.ts b/test/hopbodyfix/declined/src/relay.ts new file mode 100644 index 000000000..106fc53c0 --- /dev/null +++ b/test/hopbodyfix/declined/src/relay.ts @@ -0,0 +1,21 @@ +import { realWorker } from "./work"; +import { Matcher } from "./m"; +export function relayWorkItem(m: Matcher, x: number): number { + m.match(x); + return realWorker(x); +} +export function relayItemCount(a: number): number { + const p = a * 2; + const q = p + 1; + return q; +} +export function workItemRelayMode(a: number): number { + const w = a * 3; + const v = w - 1; + return v; +} +export function itemWorkRelayLimit(a: number): number { + const s = a * 4; + const r = s + 2; + return r; +} diff --git a/test/hopbodyfix/declined/src/work.ts b/test/hopbodyfix/declined/src/work.ts new file mode 100644 index 000000000..27797ccfd --- /dev/null +++ b/test/hopbodyfix/declined/src/work.ts @@ -0,0 +1,4 @@ +export function realWorker(y: number): number { + const REAL_WORKER_MARK = y * 11; + return REAL_WORKER_MARK; +} diff --git a/test/hopbodyfix/external/src/relay.ts b/test/hopbodyfix/external/src/relay.ts new file mode 100644 index 000000000..5a5bf56d7 --- /dev/null +++ b/test/hopbodyfix/external/src/relay.ts @@ -0,0 +1,20 @@ +import { realWorker } from "./work"; +export function relayWorkItem(x: number, sink: any): number { + missingHelperCall(x); + return realWorker(x); +} +export function relayItemCount(a: number): number { + const p = a * 2; + const q = p + 1; + return q; +} +export function workItemRelayMode(a: number): number { + const w = a * 3; + const v = w - 1; + return v; +} +export function itemWorkRelayLimit(a: number): number { + const s = a * 4; + const r = s + 2; + return r; +} diff --git a/test/hopbodyfix/external/src/work.ts b/test/hopbodyfix/external/src/work.ts new file mode 100644 index 000000000..27797ccfd --- /dev/null +++ b/test/hopbodyfix/external/src/work.ts @@ -0,0 +1,4 @@ +export function realWorker(y: number): number { + const REAL_WORKER_MARK = y * 11; + return REAL_WORKER_MARK; +} diff --git a/test/hopbodyfix/ffi/src/module.cpp b/test/hopbodyfix/ffi/src/module.cpp new file mode 100644 index 000000000..8978a371a --- /dev/null +++ b/test/hopbodyfix/ffi/src/module.cpp @@ -0,0 +1,15 @@ +// hopbodycheck H2: a C++ function Python reaches only through a pybind11 binding alias (a binding edge: amb= "verify in source"). +#include + +namespace py = pybind11; + +double scale_impl( double x ) +{ + double y = x * 2.0 + 1.0; + return y; +} + +PYBIND11_MODULE( ffimod, m ) +{ + m.def( "fast_transform", &scale_impl ); +} diff --git a/test/hopbodyfix/ffi/src/relay.py b/test/hopbodyfix/ffi/src/relay.py new file mode 100644 index 000000000..b2a668e84 --- /dev/null +++ b/test/hopbodyfix/ffi/src/relay.py @@ -0,0 +1,5 @@ +import ffimod + + +def relay_transform(value): + return ffimod.fast_transform(value) diff --git a/test/hopbodyfix/ffi/src/relays.py b/test/hopbodyfix/ffi/src/relays.py new file mode 100644 index 000000000..5355b7a82 --- /dev/null +++ b/test/hopbodyfix/ffi/src/relays.py @@ -0,0 +1,17 @@ +def relay_value_log(value): + print("relay", value) + print("value", value) + return value + + +def relay_value_count(value): + total = 0 + for v in value: + total += 1 + return total + + +def relay_value_check(value): + if value is None: + raise ValueError("relay value") + return value diff --git a/test/hopbodyfix/follow/src/core.ts b/test/hopbodyfix/follow/src/core.ts new file mode 100644 index 000000000..20ebc54d6 --- /dev/null +++ b/test/hopbodyfix/follow/src/core.ts @@ -0,0 +1,10 @@ +// where the flow lives +export async function runPipeline(input: Request): Promise { + const url = new URL(input.url) + const segments = url.pathname.split('/').filter(Boolean) + if (segments.length === 0) { + return new Response('index', { status: 200 }) + } + const text = segments.map((s) => s.toUpperCase()).join('-') + return new Response(text, { status: 200 }) +} diff --git a/test/hopbodyfix/follow/src/entry.ts b/test/hopbodyfix/follow/src/entry.ts new file mode 100644 index 000000000..d21eee8bb --- /dev/null +++ b/test/hopbodyfix/follow/src/entry.ts @@ -0,0 +1,14 @@ +import { forwardInput, stampInput } from './route' + +// the public entry: a one-line wrapper +export function handleRequest(request: Request): Promise { + return forwardInput(request) +} + +// one call, but four statement lines: not thin, never followed +export function handleTimedRequest(request: Request): Promise { + const started = Date.now() + const label = request.url + const tag = label + started + return stampInput(new Request(tag)) +} diff --git a/test/hopbodyfix/follow/src/route.ts b/test/hopbodyfix/follow/src/route.ts new file mode 100644 index 000000000..6be8900b4 --- /dev/null +++ b/test/hopbodyfix/follow/src/route.ts @@ -0,0 +1,11 @@ +import { runPipeline } from './core' + +// a second one-line wrapper: the follow goes on to its callee +export function forwardInput(input: Request): Promise { + return runPipeline(input) +} + +// the four-line wrapper's callee: reached from nowhere else, so only a (wrong) follow would print it +export function stampInput(input: Request): Promise { + return Promise.resolve(new Response(input.url)) +} diff --git a/test/hopbodyfix/follow/src/stats.ts b/test/hopbodyfix/follow/src/stats.ts new file mode 100644 index 000000000..9381ca6b3 --- /dev/null +++ b/test/hopbodyfix/follow/src/stats.ts @@ -0,0 +1,11 @@ +// bookkeeping about handled requests (ranked by the question's words, never on the path) +let handledRequests = 0 + +export function countHandledRequest(request: Request): number { + handledRequests = handledRequests + (request.url.length > 0 ? 1 : 0) + return handledRequests +} + +export function resetHandledRequests(): void { + handledRequests = 0 +} diff --git a/test/hopbodyfix/klass/src/cfg.ts b/test/hopbodyfix/klass/src/cfg.ts new file mode 100644 index 000000000..ca473dda9 --- /dev/null +++ b/test/hopbodyfix/klass/src/cfg.ts @@ -0,0 +1,16 @@ +export function makePanelConfig(): number { + const MAKE_CONFIG_MARK = 3; + return MAKE_CONFIG_MARK * 7; +} +export class BaseRunner { + launch(x: number): number { + const BASE_LAUNCH_MARK = x + 100; + return BASE_LAUNCH_MARK; + } +} +export class OtherRunner { + launch(x: number): number { + const OTHER_LAUNCH_MARK = x + 200; + return OTHER_LAUNCH_MARK; + } +} diff --git a/test/hopbodyfix/klass/src/panel.ts b/test/hopbodyfix/klass/src/panel.ts new file mode 100644 index 000000000..5e10328a2 --- /dev/null +++ b/test/hopbodyfix/klass/src/panel.ts @@ -0,0 +1,34 @@ +import { makePanelConfig, BaseRunner } from "./cfg"; +export class PanelHolder { + size = makePanelConfig(); +} +export class ChildRunner extends BaseRunner { + launch(x: number): number { + return super.launch(x); + } +} +export function panelHolderSize(a: number): number { + const p = a * 2; + const q = p + 1; + return q; +} +export function holderPanelWidth(a: number): number { + const w = a * 3; + const v = w - 1; + return v; +} +export function panelSizeHolder(a: number): number { + const s = a * 4; + const r = s + 2; + return r; +} +export function childRunnerLaunchCount(n: number): number { + const c = n + 1; + const d = c * 2; + return d; +} +export function runnerChildLaunchMode(n: number): number { + const e = n - 1; + const f = e * 3; + return f; +} diff --git a/test/hopbodyfix/logic/src/w.ts b/test/hopbodyfix/logic/src/w.ts new file mode 100644 index 000000000..86dfd4802 --- /dev/null +++ b/test/hopbodyfix/logic/src/w.ts @@ -0,0 +1,39 @@ +export function auditTrail(t: number): void { + const AUDIT_BODY_MARK = t; + console.log("audit", AUDIT_BODY_MARK); +} +export function computeScoreTotal(a: number, b: number): number { + const t = a * b + 7 - (a % 3) * (b % 5); + auditTrail(t); + return t * t + a - b; +} +export function transformItem(x: number): number { + const TRANSFORM_MARK = x * 4; + return TRANSFORM_MARK; +} +export function pipelineScore(items: number[]): number { + return items.filter((x) => x > 0).map((x) => transformItem(x)).reduce((p, q) => p + q, 0); +} +export class BaseRunner { + run(x: number): number { + const BASE_RUN_MARK = x + 100; + return BASE_RUN_MARK; + } +} +export class OtherRunner { + run(x: number): number { + const OTHER_RUN_MARK = x + 200; + return OTHER_RUN_MARK; + } +} +export class ChildRunner extends BaseRunner { + run(x: number): number { + return super.run(x); + } +} +export function makeWidgetConfig(): number { + return 3; +} +export class WidgetHolder { + size = makeWidgetConfig(); +} diff --git a/test/hopbodyfix/loose/src/entry.ts b/test/hopbodyfix/loose/src/entry.ts new file mode 100644 index 000000000..c7fb249e0 --- /dev/null +++ b/test/hopbodyfix/loose/src/entry.ts @@ -0,0 +1,4 @@ +// one call through an untyped receiver: bound by name alone, so never followed +export function handleLooseRequest(request: Request, worker: any): Promise { + return worker.crunchInput(request) +} diff --git a/test/hopbodyfix/loose/src/tally.ts b/test/hopbodyfix/loose/src/tally.ts new file mode 100644 index 000000000..2c39e847c --- /dev/null +++ b/test/hopbodyfix/loose/src/tally.ts @@ -0,0 +1,11 @@ +// bookkeeping about handled loose requests (ranked by the question's words, never on the path) +let looseRequests = 0 + +export function countLooseRequest(request: Request): number { + looseRequests = looseRequests + (request.url.length > 0 ? 1 : 0) + return looseRequests +} + +export function resetLooseRequests(): void { + looseRequests = 0 +} diff --git a/test/hopbodyfix/loose/src/worker.ts b/test/hopbodyfix/loose/src/worker.ts new file mode 100644 index 000000000..6909524c2 --- /dev/null +++ b/test/hopbodyfix/loose/src/worker.ts @@ -0,0 +1,7 @@ +export class Cruncher { + crunchInput(input: Request): Promise { + const url = new URL(input.url) + const parts = url.pathname.split('/') + return Promise.resolve(new Response(parts.join(':'))) + } +} diff --git a/test/hopbodyfix/many/src/ship.ts b/test/hopbodyfix/many/src/ship.ts new file mode 100644 index 000000000..98dcead5d --- /dev/null +++ b/test/hopbodyfix/many/src/ship.ts @@ -0,0 +1,59 @@ +export function shipOrder1(order: number): number { + return packOrder1(order); +} +export function packOrder1(order: number): number { + return sealOrder1(order); +} +export function sealOrder1(order: number): number { + const SEAL_MARK_1 = order * 11; + const sealed1 = SEAL_MARK_1 + 1; + return sealed1; +} + +export function shipOrder2(order: number): number { + return packOrder2(order); +} +export function packOrder2(order: number): number { + return sealOrder2(order); +} +export function sealOrder2(order: number): number { + const SEAL_MARK_2 = order * 12; + const sealed2 = SEAL_MARK_2 + 2; + return sealed2; +} + +export function shipOrder3(order: number): number { + return packOrder3(order); +} +export function packOrder3(order: number): number { + return sealOrder3(order); +} +export function sealOrder3(order: number): number { + const SEAL_MARK_3 = order * 13; + const sealed3 = SEAL_MARK_3 + 3; + return sealed3; +} + +export function shipOrder4(order: number): number { + return packOrder4(order); +} +export function packOrder4(order: number): number { + return sealOrder4(order); +} +export function sealOrder4(order: number): number { + const SEAL_MARK_4 = order * 14; + const sealed4 = SEAL_MARK_4 + 4; + return sealed4; +} + +export function shipOrder5(order: number): number { + return packOrder5(order); +} +export function packOrder5(order: number): number { + return sealOrder5(order); +} +export function sealOrder5(order: number): number { + const SEAL_MARK_5 = order * 15; + const sealed5 = SEAL_MARK_5 + 5; + return sealed5; +} diff --git a/test/hopbodyfix/pydoc/src/q.py b/test/hopbodyfix/pydoc/src/q.py new file mode 100644 index 000000000..08c5783ef --- /dev/null +++ b/test/hopbodyfix/pydoc/src/q.py @@ -0,0 +1,5 @@ +class Queue: + def put_nowait_item(self, item): + QUEUE_PUT_MARK = item + self.items = [QUEUE_PUT_MARK] + return True diff --git a/test/hopbodyfix/pydoc/src/w.py b/test/hopbodyfix/pydoc/src/w.py new file mode 100644 index 000000000..136dcc1fd --- /dev/null +++ b/test/hopbodyfix/pydoc/src/w.py @@ -0,0 +1,36 @@ +from .q import Queue + + +class Widget: + def __init__(self): + self._queue: Queue = Queue() + + def post_message(self, message): + """Post a message to this widget. + + Args: + message: the message to post. + """ + return self._queue.put_nowait_item(message) + + def post_message_plain(self, message): + return self._queue.put_nowait_item(message) + + +def message_post_count(widget, message): + count = 0 + count += 1 + count *= 2 + return count + + +def post_message_log(message): + text = str(message) + text = text.strip() + return text + + +def widget_message_post(message): + a = len(str(message)) + b = a * 2 + return b diff --git a/test/hopbodyfix/route/case.py b/test/hopbodyfix/route/case.py new file mode 100644 index 000000000..980b8ceb8 --- /dev/null +++ b/test/hopbodyfix/route/case.py @@ -0,0 +1,9 @@ +import re + + +def camel_to_snake(name, _re=re.compile("[a-z][A-Z]")): + def repl(m): + lower, upper = m.group() + return f"{lower}_{upper.lower()}" + + return _re.sub(repl, name).lower() diff --git a/test/hopbodyfix/route/message.py b/test/hopbodyfix/route/message.py new file mode 100644 index 000000000..6ab2d25fa --- /dev/null +++ b/test/hopbodyfix/route/message.py @@ -0,0 +1,14 @@ +from case import camel_to_snake + + +class Message: + namespace = "" + + def __init_subclass__(cls, namespace=None): + super().__init_subclass__() + if namespace is not None: + name = f"{namespace}_{camel_to_snake(cls.__name__)}" + else: + parts = cls.__qualname__.rsplit(".", 2)[-2:] + name = "_".join(camel_to_snake(p) for p in parts) + cls.handler_name = f"on_{name}" diff --git a/test/hopbodyfix/route/pump.py b/test/hopbodyfix/route/pump.py new file mode 100644 index 000000000..3de1f17bf --- /dev/null +++ b/test/hopbodyfix/route/pump.py @@ -0,0 +1,43 @@ +from message import Message as Event +import queue + + +class Pump: + def __init__(self, parent=None): + self._queue = queue.Queue() + self._parent = parent + + def post_message(self, message): + if self._closed: + return False + self._queue.put_nowait(message) + return True + + def _get_message(self): + return self._queue.get() + + def _process_messages_loop(self): + while not self._closed: + message = self._get_message() + self._dispatch_message(message) + + def _dispatch_message(self, message): + if message.no_dispatch: + return + self.on_event(message) + + async def on_event(self, event: Event) -> None: + self._on_message(event) + + def _on_message(self, message): + handler_name = message.handler_name + for method in self._get_dispatch_methods(handler_name, message): + method(message) + if message.bubble and self._parent is not None: + self._parent.post_message(message) + + def _get_dispatch_methods(self, method_name, message): + for cls in self.__class__.__mro__: + method = cls.__dict__.get(method_name) + if method is not None: + yield method.__get__(self, cls) diff --git a/test/hopbodyfix/route/widgets.py b/test/hopbodyfix/route/widgets.py new file mode 100644 index 000000000..c05704316 --- /dev/null +++ b/test/hopbodyfix/route/widgets.py @@ -0,0 +1,53 @@ +from message import Message +from pump import Pump + + +def leaf(x): + return x + + +class Tick(Message): + pass + + +class Button(Pump): + class Pressed(Message): + pass + + def press(self): + self.post_message(self.Pressed()) + + +class Worker(Pump): + class Changed(Message, namespace="worker"): + pass + + def change(self): + self.post_message(Worker.Changed()) + + +class Form(Pump): + def on_button_pressed(self, m): + return leaf(m) + + def on_worker_changed(self, m): + return m + + def on_tick(self, m): + return leaf(m) + + +def log(m): + return m + + +class Clock(Pump): + def tick(self): + self.post_message(Tick()) + + def poke(self): + msg = Tick() + self.post_message(msg) + + def note(self): + log(Tick()) diff --git a/test/hopbodyfix/routecap/case.py b/test/hopbodyfix/routecap/case.py new file mode 100644 index 000000000..980b8ceb8 --- /dev/null +++ b/test/hopbodyfix/routecap/case.py @@ -0,0 +1,9 @@ +import re + + +def camel_to_snake(name, _re=re.compile("[a-z][A-Z]")): + def repl(m): + lower, upper = m.group() + return f"{lower}_{upper.lower()}" + + return _re.sub(repl, name).lower() diff --git a/test/hopbodyfix/routecap/message.py b/test/hopbodyfix/routecap/message.py new file mode 100644 index 000000000..6ab2d25fa --- /dev/null +++ b/test/hopbodyfix/routecap/message.py @@ -0,0 +1,14 @@ +from case import camel_to_snake + + +class Message: + namespace = "" + + def __init_subclass__(cls, namespace=None): + super().__init_subclass__() + if namespace is not None: + name = f"{namespace}_{camel_to_snake(cls.__name__)}" + else: + parts = cls.__qualname__.rsplit(".", 2)[-2:] + name = "_".join(camel_to_snake(p) for p in parts) + cls.handler_name = f"on_{name}" diff --git a/test/hopbodyfix/routecap/pump.py b/test/hopbodyfix/routecap/pump.py new file mode 100644 index 000000000..2f3d210d1 --- /dev/null +++ b/test/hopbodyfix/routecap/pump.py @@ -0,0 +1,47 @@ +from message import Message as Event +import queue + + +class Pump: + def __init__(self, parent=None): + self._queue = queue.Queue() + self._parent = parent + + def post_message(self, message): + if self._closed: + return False + self._queue.put_nowait(message) + return True + + def _get_message(self): + return self._queue.get() + + def _process_messages_loop(self): + while not self._closed: + message = self._get_message() + self._dispatch_message(message) + + def _dispatch_message(self, message): + if message.no_dispatch: + return + self.on_event(message) + + async def on_event(self, event: Event) -> None: + self._on_message(event) + + def _on_message(self, message): + handler_name = message.handler_name + for method in self._get_dispatch_methods(handler_name, message): + method(message) + if message.bubble and self._parent is not None: + self._parent.post_message(message) + + def _get_dispatch_methods(self, method_name, message): + for cls in self.__class__.__mro__: + method = cls.__dict__.get(method_name) + if method is not None: + yield method.__get__(self, cls) + + def _trace_message(self, message): + handler_name = message.handler_name + return handler_name diff --git a/test/hopbodyfix/routecap/widgets.py b/test/hopbodyfix/routecap/widgets.py new file mode 100644 index 000000000..c05704316 --- /dev/null +++ b/test/hopbodyfix/routecap/widgets.py @@ -0,0 +1,53 @@ +from message import Message +from pump import Pump + + +def leaf(x): + return x + + +class Tick(Message): + pass + + +class Button(Pump): + class Pressed(Message): + pass + + def press(self): + self.post_message(self.Pressed()) + + +class Worker(Pump): + class Changed(Message, namespace="worker"): + pass + + def change(self): + self.post_message(Worker.Changed()) + + +class Form(Pump): + def on_button_pressed(self, m): + return leaf(m) + + def on_worker_changed(self, m): + return m + + def on_tick(self, m): + return leaf(m) + + +def log(m): + return m + + +class Clock(Pump): + def tick(self): + self.post_message(Tick()) + + def poke(self): + msg = Tick() + self.post_message(msg) + + def note(self): + log(Tick()) diff --git a/test/hopbodyfix/routeslot/src/ops.c b/test/hopbodyfix/routeslot/src/ops.c new file mode 100644 index 000000000..d78c39edc --- /dev/null +++ b/test/hopbodyfix/routeslot/src/ops.c @@ -0,0 +1,14 @@ +struct ops { void (*run)(int v); }; +void sink_write(int v); +static void run_fast(int v) { sink_write(v + 1); } +static void run_slow(int v) { int w = v * 2; w = w + 3; sink_write(w); } +static void run_noop(int v) { (void)v; } +static struct ops g_ops_a = { .run = run_fast }; +static struct ops g_ops_b = { .run = run_slow }; +static struct ops g_ops_c = { .run = run_noop }; +struct ops *pick(int k) { return k ? &g_ops_a : (k < 0 ? &g_ops_c : &g_ops_b); } +void start_job(struct ops *o, int v) +{ + o->run(v); +} +void sink_write(int v) { (void)v; } diff --git a/test/hopbodyfix/web/src/big.ts b/test/hopbodyfix/web/src/big.ts new file mode 100644 index 000000000..6614b7933 --- /dev/null +++ b/test/hopbodyfix/web/src/big.ts @@ -0,0 +1,161 @@ +// A middle hop whose body is longer than the per-body guard. +export function bigEntry(n: number): number { + return bigMiddle(n) +} +export function bigMiddle(n: number): number { + let total = n + total = total + 0 * n - (total % 7) // accumulate step 000 of the long middle body + total = total + 1 * n - (total % 8) // accumulate step 001 of the long middle body + total = total + 2 * n - (total % 9) // accumulate step 002 of the long middle body + total = total + 3 * n - (total % 10) // accumulate step 003 of the long middle body + total = total + 4 * n - (total % 11) // accumulate step 004 of the long middle body + total = total + 5 * n - (total % 12) // accumulate step 005 of the long middle body + total = total + 6 * n - (total % 13) // accumulate step 006 of the long middle body + total = total + 7 * n - (total % 14) // accumulate step 007 of the long middle body + total = total + 8 * n - (total % 15) // accumulate step 008 of the long middle body + total = total + 9 * n - (total % 16) // accumulate step 009 of the long middle body + total = total + 10 * n - (total % 17) // accumulate step 010 of the long middle body + total = total + 11 * n - (total % 18) // accumulate step 011 of the long middle body + total = total + 12 * n - (total % 19) // accumulate step 012 of the long middle body + total = total + 13 * n - (total % 20) // accumulate step 013 of the long middle body + total = total + 14 * n - (total % 21) // accumulate step 014 of the long middle body + total = total + 15 * n - (total % 22) // accumulate step 015 of the long middle body + total = total + 16 * n - (total % 23) // accumulate step 016 of the long middle body + total = total + 17 * n - (total % 24) // accumulate step 017 of the long middle body + total = total + 18 * n - (total % 25) // accumulate step 018 of the long middle body + total = total + 19 * n - (total % 26) // accumulate step 019 of the long middle body + total = total + 20 * n - (total % 27) // accumulate step 020 of the long middle body + total = total + 21 * n - (total % 28) // accumulate step 021 of the long middle body + total = total + 22 * n - (total % 29) // accumulate step 022 of the long middle body + total = total + 23 * n - (total % 30) // accumulate step 023 of the long middle body + total = total + 24 * n - (total % 31) // accumulate step 024 of the long middle body + total = total + 25 * n - (total % 32) // accumulate step 025 of the long middle body + total = total + 26 * n - (total % 33) // accumulate step 026 of the long middle body + total = total + 27 * n - (total % 34) // accumulate step 027 of the long middle body + total = total + 28 * n - (total % 35) // accumulate step 028 of the long middle body + total = total + 29 * n - (total % 36) // accumulate step 029 of the long middle body + total = total + 30 * n - (total % 37) // accumulate step 030 of the long middle body + total = total + 31 * n - (total % 38) // accumulate step 031 of the long middle body + total = total + 32 * n - (total % 39) // accumulate step 032 of the long middle body + total = total + 33 * n - (total % 40) // accumulate step 033 of the long middle body + total = total + 34 * n - (total % 41) // accumulate step 034 of the long middle body + total = total + 35 * n - (total % 42) // accumulate step 035 of the long middle body + total = total + 36 * n - (total % 43) // accumulate step 036 of the long middle body + total = total + 37 * n - (total % 44) // accumulate step 037 of the long middle body + total = total + 38 * n - (total % 45) // accumulate step 038 of the long middle body + total = total + 39 * n - (total % 46) // accumulate step 039 of the long middle body + total = total + 40 * n - (total % 47) // accumulate step 040 of the long middle body + total = total + 41 * n - (total % 48) // accumulate step 041 of the long middle body + total = total + 42 * n - (total % 49) // accumulate step 042 of the long middle body + total = total + 43 * n - (total % 50) // accumulate step 043 of the long middle body + total = total + 44 * n - (total % 51) // accumulate step 044 of the long middle body + total = total + 45 * n - (total % 52) // accumulate step 045 of the long middle body + total = total + 46 * n - (total % 53) // accumulate step 046 of the long middle body + total = total + 47 * n - (total % 54) // accumulate step 047 of the long middle body + total = total + 48 * n - (total % 55) // accumulate step 048 of the long middle body + total = total + 49 * n - (total % 56) // accumulate step 049 of the long middle body + total = total + 50 * n - (total % 57) // accumulate step 050 of the long middle body + total = total + 51 * n - (total % 58) // accumulate step 051 of the long middle body + total = total + 52 * n - (total % 59) // accumulate step 052 of the long middle body + total = total + 53 * n - (total % 60) // accumulate step 053 of the long middle body + total = total + 54 * n - (total % 61) // accumulate step 054 of the long middle body + total = total + 55 * n - (total % 62) // accumulate step 055 of the long middle body + total = total + 56 * n - (total % 63) // accumulate step 056 of the long middle body + total = total + 57 * n - (total % 64) // accumulate step 057 of the long middle body + total = total + 58 * n - (total % 65) // accumulate step 058 of the long middle body + total = total + 59 * n - (total % 66) // accumulate step 059 of the long middle body + total = total + 60 * n - (total % 67) // accumulate step 060 of the long middle body + total = total + 61 * n - (total % 68) // accumulate step 061 of the long middle body + total = total + 62 * n - (total % 69) // accumulate step 062 of the long middle body + total = total + 63 * n - (total % 70) // accumulate step 063 of the long middle body + total = total + 64 * n - (total % 71) // accumulate step 064 of the long middle body + total = total + 65 * n - (total % 72) // accumulate step 065 of the long middle body + total = total + 66 * n - (total % 73) // accumulate step 066 of the long middle body + total = total + 67 * n - (total % 74) // accumulate step 067 of the long middle body + total = total + 68 * n - (total % 75) // accumulate step 068 of the long middle body + total = total + 69 * n - (total % 76) // accumulate step 069 of the long middle body + total = total + 70 * n - (total % 77) // accumulate step 070 of the long middle body + total = total + 71 * n - (total % 78) // accumulate step 071 of the long middle body + total = total + 72 * n - (total % 79) // accumulate step 072 of the long middle body + total = total + 73 * n - (total % 80) // accumulate step 073 of the long middle body + total = total + 74 * n - (total % 81) // accumulate step 074 of the long middle body + total = total + 75 * n - (total % 82) // accumulate step 075 of the long middle body + total = total + 76 * n - (total % 83) // accumulate step 076 of the long middle body + total = total + 77 * n - (total % 84) // accumulate step 077 of the long middle body + total = total + 78 * n - (total % 85) // accumulate step 078 of the long middle body + total = total + 79 * n - (total % 86) // accumulate step 079 of the long middle body + total = total + 80 * n - (total % 87) // accumulate step 080 of the long middle body + total = total + 81 * n - (total % 88) // accumulate step 081 of the long middle body + total = total + 82 * n - (total % 89) // accumulate step 082 of the long middle body + total = total + 83 * n - (total % 90) // accumulate step 083 of the long middle body + total = total + 84 * n - (total % 91) // accumulate step 084 of the long middle body + total = total + 85 * n - (total % 92) // accumulate step 085 of the long middle body + total = total + 86 * n - (total % 93) // accumulate step 086 of the long middle body + total = total + 87 * n - (total % 94) // accumulate step 087 of the long middle body + total = total + 88 * n - (total % 95) // accumulate step 088 of the long middle body + total = total + 89 * n - (total % 96) // accumulate step 089 of the long middle body + total = total + 90 * n - (total % 97) // accumulate step 090 of the long middle body + total = total + 91 * n - (total % 98) // accumulate step 091 of the long middle body + total = total + 92 * n - (total % 99) // accumulate step 092 of the long middle body + total = total + 93 * n - (total % 100) // accumulate step 093 of the long middle body + total = total + 94 * n - (total % 101) // accumulate step 094 of the long middle body + total = total + 95 * n - (total % 102) // accumulate step 095 of the long middle body + total = total + 96 * n - (total % 103) // accumulate step 096 of the long middle body + total = total + 97 * n - (total % 104) // accumulate step 097 of the long middle body + total = total + 98 * n - (total % 105) // accumulate step 098 of the long middle body + total = total + 99 * n - (total % 106) // accumulate step 099 of the long middle body + total = total + 100 * n - (total % 107) // accumulate step 100 of the long middle body + total = total + 101 * n - (total % 108) // accumulate step 101 of the long middle body + total = total + 102 * n - (total % 109) // accumulate step 102 of the long middle body + total = total + 103 * n - (total % 110) // accumulate step 103 of the long middle body + total = total + 104 * n - (total % 111) // accumulate step 104 of the long middle body + total = total + 105 * n - (total % 112) // accumulate step 105 of the long middle body + total = total + 106 * n - (total % 113) // accumulate step 106 of the long middle body + total = total + 107 * n - (total % 114) // accumulate step 107 of the long middle body + total = total + 108 * n - (total % 115) // accumulate step 108 of the long middle body + total = total + 109 * n - (total % 116) // accumulate step 109 of the long middle body + total = total + 110 * n - (total % 117) // accumulate step 110 of the long middle body + total = total + 111 * n - (total % 118) // accumulate step 111 of the long middle body + total = total + 112 * n - (total % 119) // accumulate step 112 of the long middle body + total = total + 113 * n - (total % 120) // accumulate step 113 of the long middle body + total = total + 114 * n - (total % 121) // accumulate step 114 of the long middle body + total = total + 115 * n - (total % 122) // accumulate step 115 of the long middle body + total = total + 116 * n - (total % 123) // accumulate step 116 of the long middle body + total = total + 117 * n - (total % 124) // accumulate step 117 of the long middle body + total = total + 118 * n - (total % 125) // accumulate step 118 of the long middle body + total = total + 119 * n - (total % 126) // accumulate step 119 of the long middle body + total = total + 120 * n - (total % 127) // accumulate step 120 of the long middle body + total = total + 121 * n - (total % 128) // accumulate step 121 of the long middle body + total = total + 122 * n - (total % 129) // accumulate step 122 of the long middle body + total = total + 123 * n - (total % 130) // accumulate step 123 of the long middle body + total = total + 124 * n - (total % 131) // accumulate step 124 of the long middle body + total = total + 125 * n - (total % 132) // accumulate step 125 of the long middle body + total = total + 126 * n - (total % 133) // accumulate step 126 of the long middle body + total = total + 127 * n - (total % 134) // accumulate step 127 of the long middle body + total = total + 128 * n - (total % 135) // accumulate step 128 of the long middle body + total = total + 129 * n - (total % 136) // accumulate step 129 of the long middle body + total = total + 130 * n - (total % 137) // accumulate step 130 of the long middle body + total = total + 131 * n - (total % 138) // accumulate step 131 of the long middle body + total = total + 132 * n - (total % 139) // accumulate step 132 of the long middle body + total = total + 133 * n - (total % 140) // accumulate step 133 of the long middle body + total = total + 134 * n - (total % 141) // accumulate step 134 of the long middle body + total = total + 135 * n - (total % 142) // accumulate step 135 of the long middle body + total = total + 136 * n - (total % 143) // accumulate step 136 of the long middle body + total = total + 137 * n - (total % 144) // accumulate step 137 of the long middle body + total = total + 138 * n - (total % 145) // accumulate step 138 of the long middle body + total = total + 139 * n - (total % 146) // accumulate step 139 of the long middle body + total = total + 140 * n - (total % 147) // accumulate step 140 of the long middle body + total = total + 141 * n - (total % 148) // accumulate step 141 of the long middle body + total = total + 142 * n - (total % 149) // accumulate step 142 of the long middle body + total = total + 143 * n - (total % 150) // accumulate step 143 of the long middle body + total = total + 144 * n - (total % 151) // accumulate step 144 of the long middle body + total = total + 145 * n - (total % 152) // accumulate step 145 of the long middle body + total = total + 146 * n - (total % 153) // accumulate step 146 of the long middle body + total = total + 147 * n - (total % 154) // accumulate step 147 of the long middle body + total = total + 148 * n - (total % 155) // accumulate step 148 of the long middle body + total = total + 149 * n - (total % 156) // accumulate step 149 of the long middle body + return bigSink(total) +} +export function bigSink(n: number): number { + return n +} diff --git a/test/hopbodyfix/web/src/chain.ts b/test/hopbodyfix/web/src/chain.ts new file mode 100644 index 000000000..cd1c22c4e --- /dev/null +++ b/test/hopbodyfix/web/src/chain.ts @@ -0,0 +1,16 @@ +// A four-hop chain: no middle body prints, next= names the first middle hop. +export function stepOne(n: number): number { + return stepTwo(n + 1) +} +export function stepTwo(n: number): number { + return stepThree(n * 2) +} +export function stepThree(n: number): number { + return stepFour(n - 3) +} +export function stepFour(n: number): number { + return stepFive(n / 4) +} +export function stepFive(n: number): number { + return n +} diff --git a/test/hopbodyfix/web/src/compose.ts b/test/hopbodyfix/web/src/compose.ts new file mode 100644 index 000000000..c6e14a952 --- /dev/null +++ b/test/hopbodyfix/web/src/compose.ts @@ -0,0 +1,11 @@ +export type Handler = (r: Request) => Promise + +export function compose(handlers: Handler[]): Handler { + return async (r: Request) => { + let res = new Response('') + for (const h of handlers) { + res = await h(r) + } + return res + } +} diff --git a/test/hopbodyfix/web/src/router.ts b/test/hopbodyfix/web/src/router.ts new file mode 100644 index 000000000..686d54617 --- /dev/null +++ b/test/hopbodyfix/web/src/router.ts @@ -0,0 +1,11 @@ +import type { Handler } from './compose' + +const table: Record = {} + +export function getPath(request: Request): string { + return new URL(request.url).pathname +} + +export function matchRoute(path: string): Handler[] { + return table[path] ?? [] +} diff --git a/test/hopbodyfix/web/src/server.ts b/test/hopbodyfix/web/src/server.ts new file mode 100644 index 000000000..3739e8bd4 --- /dev/null +++ b/test/hopbodyfix/web/src/server.ts @@ -0,0 +1,29 @@ +// A tiny request pipeline: Server.fetch is a one-line wrapper, Server.dispatch holds the flow. +import { compose } from './compose' +import { getPath, matchRoute } from './router' + +export class Server { + fetch(request: Request): Promise { + return this.dispatch(request) + } + + dispatch(request: Request): Promise { + const path = getPath(request) + const handlers = matchRoute(path) + if (handlers.length === 0) { + return Promise.resolve(new Response('not found', { status: 404 })) + } + const composed = compose(handlers) + return composed(request) + } + + // two calls: not a thin wrapper, never followed + listen(request: Request): Promise { + auditRequest(request) + return this.dispatch(request) + } +} + +export function auditRequest(request: Request): void { + console.log(request.url) +} diff --git a/test/hopbodyfix/web/src/trip.ts b/test/hopbodyfix/web/src/trip.ts new file mode 100644 index 000000000..1f4c1f4a3 --- /dev/null +++ b/test/hopbodyfix/web/src/trip.ts @@ -0,0 +1,26 @@ +// A two-hop chain whose middle hop no question word names: the graph-verb block prints its body. +export function gatewayEntry(input: string): string { + const a = input.length + const b = a + 1 + const c = String(b) + return relayHop(input + c) +} +export function relayHop(input: string): string { + const trimmed = input.trim() + const upper = trimmed.toUpperCase() + const marked = upper + '!' + return finalStage(marked) +} +export function finalStage(input: string): string { + return input +} +// decoys the question's words rank above the middle hop +export function gatewayEntryCount(): number { + return 1 +} +export function finalStageCount(): number { + return 2 +} +export function gatewayFinalStageLabel(): string { + return 'gateway final stage' +} diff --git a/test/mcpcontractcheck.sh b/test/mcpcontractcheck.sh index ea2a21e64..b25337776 100755 --- a/test/mcpcontractcheck.sh +++ b/test/mcpcontractcheck.sh @@ -371,6 +371,9 @@ TWIN = { # lane lego-transitive (2026-10-10): --lego joined the paging set the same conditional way for its DEEPER implementor # rows (serialize.h legoClosurePage); its twin takes the same limit/offset through mcpPageArgs. "--lego": "lego", + # lane D2 DEFS-LOOKUP (2026-10-28): --defs pages (one name's definition rows); no MCP twin yet -- MCP find_symbol keeps its own + # table until PLAN_DEFS_LOOKUP part 2 moves it onto rw::lookupDefs (card DEFS-ONE-CORE), so --defs is CLI-only for now. + "--defs": "", } unmapped = sorted( v for v in pagingCli if v not in TWIN ) check( not unmapped, "(G) every paging CLI verb is classified twin-or-not (%s)" % ( ",".join( unmapped ) or "none unmapped" ) ) diff --git a/test/memguardcheck.sh b/test/memguardcheck.sh index 0ca7f4c0e..2c7814ddb 100755 --- a/test/memguardcheck.sh +++ b/test/memguardcheck.sh @@ -366,13 +366,29 @@ else fi # (B6) MCP: a soft stop answers with the envelope disclosure, and every answer from that partial index keeps it +# WHICH files survive crawl:30 is the filesystem's readdir order (recursive_directory_iterator, no sort before the +# stop): APFS keeps a/f02.c, ext4's hashed order can keep it or not (a simulation over random orders keeps it +# ~37 % of the time). find_symbol of a name outside the kept files is a JSON-RPC ERROR, which has no +# result._memory_stop at all (CI 40671724898 and #458 runs: B6b "_memory_stop=''" on ubuntu only). So the +# per-answer assertions below use verbs whose answer is a RESULT on ANY partial index (grep, an empty match is +# still an envelope), and the find_symbol probe is asserted only on the shape it actually returned. printf '%s\n' \ "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"grep\",\"arguments\":{\"path\":\"$FX\",\"pattern\":\"a_01\"}}}" \ - "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"find_symbol\",\"arguments\":{\"path\":\"$FX\",\"symbol\":\"a_02\"}}}" \ + "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"grep\",\"arguments\":{\"path\":\"$FX\",\"pattern\":\"a_02\"}}}" \ + "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"find_symbol\",\"arguments\":{\"path\":\"$FX\",\"symbol\":\"a_02\"}}}" \ | RIPWIRE_TEST_MEMGUARD=crawl:30 mcp_call "$TMP" --max-memory=64M >"$TMP/b6.out" ms2="$( mcp_field "$TMP/b6.out" 2 _memory_stop )"; ms3="$( mcp_field "$TMP/b6.out" 3 _memory_stop )" case "$ms2" in *"memory guard"*crawl*64M*) ok "(B6) MCP grep under a crawl stop carries _memory_stop";; *) no "(B6) id=2 _memory_stop='${ms2:0:200}' resp: $( head -c 300 "$TMP/b6.out" )";; esac -case "$ms3" in *"memory guard"*) ok "(B6b) the next answer from the same partial index still carries it";; *) no "(B6b) id=3 _memory_stop='${ms3:0:200}'";; esac +case "$ms3" in *"memory guard"*) ok "(B6b) the next answer from the same partial index still carries it";; *) no "(B6b) id=3 _memory_stop='${ms3:0:200}' resp: $( grep '"id":3' "$TMP/b6.out" | head -c 300 )";; esac +# (B6b2) a third verb on the same index: when the symbol is inside the kept files the answer is a result and must carry +# the disclosure; when it is outside them the answer is a not-found ERROR, whose shape (no envelope) is not what +# this arm pins — said on a NOTE line, not a PASS, so a reader sees which shape ran +if [ -n "$( mcp_field "$TMP/b6.out" 4 error )" ]; then + printf ' NOTE (B6b2) find_symbol a_02 fell outside the kept files here (readdir order): a not-found error, no envelope to check\n' +else + ms4="$( mcp_field "$TMP/b6.out" 4 _memory_stop )" + case "$ms4" in *"memory guard"*) ok "(B6b2) find_symbol from the same partial index still carries it";; *) no "(B6b2) id=4 _memory_stop='${ms4:0:200}'";; esac +fi printf '%s\n' "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"grep\",\"arguments\":{\"path\":\"$FX\",\"pattern\":\"a_01\"}}}" \ | mcp_call "$TMP" >"$TMP/b6c.out" if grep -q '_memory_stop' "$TMP/b6c.out"; then @@ -519,8 +535,11 @@ fi # beside the ingest; a memory-guard refusal used to join that walk's future before exiting, so the exit code waited for # the end of the log. With a git shim whose `log --name-only` (the walk's own stream; its sha-list probe and every other # git call reach the real git) streams one line every 0.1 s for 6 s, the refusal must return well before the stream ends: -# the walker stops at its next line and the join ends there (<= 4 s against the 6 s stream: measured 2 s plain and under -# ASan on a loaded 10-core host; the pre-fix binary took 17-25 s). Fresh TMPDIR, so no cached stream from an earlier arm +# the walker stops at its next line and the join ends there (measured 2 s plain and under ASan on a loaded 10-core host; +# the pre-fix binary took 17-25 s). The verdict is NOT a wall-clock bound: a fixed 4 s failed at 5 s on a host at load +# average 13-19, where the shim's own 60 spawned sleeps stretch the 6 s stream too. A join can only end when the stream +# does, so the shim writes log-done after its last line and the refusal must have returned without it (the abandoned +# walker gets SIGPIPE at its next echo and exits before reaching it); the elapsed seconds stay in the message. Fresh TMPDIR, so no cached stream from an earlier arm # stands in for the walk; the shim records that it served the walk. SHIM="$TMP/shim"; mkdir -p "$SHIM" "$TMP/b21tmp" REALGIT="$( command -v git )" @@ -531,6 +550,7 @@ for a in "\$@"; do : > "$SHIM/log-served" i=0 while [ \$i -lt 60 ]; do echo "shim-line \$i" || exit 0; sleep 0.1; i=\$((i+1)); done + : > "$SHIM/log-done" exit 0 fi done @@ -542,10 +562,10 @@ PATH="$SHIM:$PATH" TMPDIR="$TMP/b21tmp" run_trip crawl:30 "$FXG" --no-cache --fo el=$(( $( date +%s ) - t0 )) if [ ! -e "$SHIM/log-served" ]; then no "(B21) premise: the shim never served a git log --name-only — the history walk did not run against it (rc=$rc)" -elif [ "$rc" = 5 ] && grep -q '^ripwire: .*cannot answer from a partial index' "$TMP/b21.err" && [ "$el" -le 4 ]; then +elif [ "$rc" = 5 ] && grep -q '^ripwire: .*cannot answer from a partial index' "$TMP/b21.err" && [ ! -e "$SHIM/log-done" ]; then ok "(B21) --for over a crawl stop refuses in ${el}s while the shim's log still streams: the walk is abandoned, not joined" else - no "(B21) rc=$rc elapsed=${el}s (the shim's log streams for 6 s; a join waits it out) stderr: $( grep '^ripwire:' "$TMP/b21.err" | head -c 200 )" + no "(B21) rc=$rc elapsed=${el}s stream-finished-before-return=$( [ -e "$SHIM/log-done" ] && echo yes || echo no ) (the shim's log streams for 6 s; a join waits it out) stderr: $( grep '^ripwire:' "$TMP/b21.err" | head -c 200 )" fi # (B16) --query --format=candidates is a report verb, so it refuses a partial index like the rest (pinned: its # root has no header to carry the cut) diff --git a/test/pagingsweepcheck.sh b/test/pagingsweepcheck.sh index 91181a247..2acce221d 100755 --- a/test/pagingsweepcheck.sh +++ b/test/pagingsweepcheck.sh @@ -752,6 +752,10 @@ TABLE = { # has_more="1" next=), so the root is uncut by construction and the "quintet absent" branch applies; the page # itself is test/legotransitivecheck.sh's arm. "--lego": ( [ "--lego=CalleeCallsSink" ], None ), + # lane D2 DEFS-LOOKUP (2026-10-28): --defs=NAME pages ONE name's definition rows (--limit/--offset, shown=/offset=/next= on the + # child; test/defslookupcheck.sh (F) is the page itself). escapeXml binds a handful of definitions here (under the 16-row + # page), so the root is uncut and the "cut nothing => quintet absent" branch applies, as for --callers. + "--defs": ( [ "--defs=escapeXml" ], None ), } fail = 0 missing = [ v for v in universe if v not in TABLE ] diff --git a/test/pargatescheck.sh b/test/pargatescheck.sh index 002a97e6e..878a16d7e 100755 --- a/test/pargatescheck.sh +++ b/test/pargatescheck.sh @@ -688,6 +688,11 @@ PREREVIEW = '''def _stop_group(p): # run_gate() as e442a5d8 had it: the stop was checked only after a STOP_POLL_SEC wait, so a gate admitted as the signal # was recorded ran that long before anything looked. Re-spelled 2026-09-11 against the file-backed capture; the defect is # the ORDER of the two checks, not how the output is collected. +# The first wait is 3.0 s here, not STOP_POLL_SEC (0.5 s): the arm needs the probe gate (bash start + a 0.3 s sleep, then its +# "late" line) to get past its 0.3 s mark INSIDE that wait. At 0.5 s the 0.2 s of slack is eaten by process starts on a loaded host +# (the (I control) admission arm FAILED 3 of 3 runs at load average 9-13 on unmodified main 12056c16, "late recorded: False") and +# the mutant was stopped before it could show its defect. A longer wait changes no ordering: the defect is still +# "wait first, look at the stop second"; the fixed run_gate() checks the stop before any wait and is not affected. READFIRST = '''def run_gate(argv, env, limit): deadline = time.monotonic() + limit fd, capture = tempfile.mkstemp(prefix="ripwire-pargates-capture-", suffix=".out") @@ -697,7 +702,7 @@ READFIRST = '''def run_gate(argv, env, limit): subprocess.Popen(argv, cwd=root, env=env, stdout=fh, stderr=subprocess.STDOUT, start_new_session=True) as p: while True: - if _wait_for(p, max(0.0, min(STOP_POLL_SEC, deadline - time.monotonic()))): + if _wait_for(p, max(0.0, min(3.0, deadline - time.monotonic()))): return p.returncode, _capture_read(capture), "exited" if stop_signal is not None: _stop_group(p) diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index 2ecf5a5e0..0a2bfa9cf 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -646,9 +646,10 @@ done # CPython (3.13) truth per variant: starmid/starinit/starqual/starqualmid: the hop's star import `from pkg.y.base2 import *` follows the import, so # Base is pkg.y.base2.Base; assignmid/assigninit/assignqual/globalmid: `Base = Other` (or a `global Base; Base = Other` in a def that runs) makes it # pkg.x.a.Other; globalsinit: `globals()['a'] = base2` in the package binds `a` to pkg.y.base2; setattrinit: setattr(sys.modules[__name__], 'a', ...) -# does the same; augmid/forinit/withinit/walrusinit/defmid/classmid: later rebinds of every other form. Controls (stay a way in): the K22 pair. +# does the same; globalsmid/setattrmid/globalsinitre: the same two writes after the hop's own re-export of Base (`globals()['Base'] = Other`), in a mid.py hop and in +# an __init__ hop, which reads the write marker in pythonFileBindsOnlyByImport (the re-export hop, not the package-attribute path); augmid/forinit/withinit/walrusinit/defmid/classmid: later rebinds of every other form. Controls (stay a way in): the K22 pair. # K23 begin -for v in starmid starinit starqual starqualmid assignmid assigninit assignqual globalmid globalsinit setattrinit augmid forinit withinit walrusinit defmid classmid; do d="$TMP/c7_$v"; mkdir -p "$d/pkg/x" "$d/pkg/y" "$d/other" +for v in starmid starinit starqual starqualmid assignmid assigninit assignqual globalmid globalsinit setattrinit globalsmid setattrmid globalsinitre augmid forinit withinit walrusinit defmid classmid; do d="$TMP/c7_$v"; mkdir -p "$d/pkg/x" "$d/pkg/y" "$d/other" printf 'class Base:\n def render_line(self):\n return 1\n\nclass Other:\n pass\n' > "$d/pkg/x/a.py" printf 'from pkg.x.a import Base\ndef run(obj):\n return obj.render_line()\n' > "$d/pkg/x/c.py" printf 'class Base:\n def other(self):\n return 3\n' > "$d/pkg/y/base2.py" @@ -677,6 +678,12 @@ for v in starmid starinit starqual starqualmid assignmid assigninit assignqual g out="from pkg.x import a as q\n\n${tgt}$(sub q.Base)\n" ;; setattrinit) printf "import sys\nfrom pkg.y import base2 as _b\nsetattr(sys.modules[__name__], 'a', _b)\n" > "$d/pkg/x/__init__.py" out="from pkg.x import a as q\n\n${tgt}$(sub q.Base)\n" ;; + globalsmid) printf "${hop}globals()['Base'] = Other\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + setattrmid) printf "import sys\n${hop}setattr(sys.modules[__name__], 'Base', Other)\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + globalsinitre) printf "${hop}globals()['Base'] = Other\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; augmid) printf "${hop}Base += Other\n" > "$d/pkg/x/mid.py" out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; forinit) printf "${hop}for Base in (Other,):\n pass\n" > "$d/pkg/x/__init__.py" @@ -1289,7 +1296,7 @@ done # K23 (lane ind4b fix 7): a hop file whose binding of the name is not its one unrenamed import ends the chain - at EVERY hop, whatever the shape. # Every arm is shown + bridged at fix 6's head (a02e8439); the K22 controls above stay a way in. -for v in starmid starinit starqual starqualmid assignmid assigninit assignqual globalmid globalsinit setattrinit augmid forinit withinit walrusinit defmid classmid; do +for v in starmid starinit starqual starqualmid assignmid assigninit assignqual globalmid globalsinit setattrinit globalsmid setattrmid globalsinitre augmid forinit withinit walrusinit defmid classmid; do nsArm "(K23) 2c hop file rebinds the name ($v) - withheld" "$TMP/c7_$v" "run,target" \ '' \ "" diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 540355e21..be927960a 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 1dd0a41c39f89e4b35fb40750a4b2fd56376daf2161d24b03d32423d788b352d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 ecc9e1ea480e502fb228fb129f1309e37e30918d8baf3f36e9b9f01be0277d59 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 8b4e21e4ea09ab3316a40a16ddaa689942d3e66a1fe62dcc6e2f2fb8d3317469 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 0cca7164a48d6543694bf07b4252209558d189771d50e913e46783bb98ae4bed e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 47510713fadffc2c6f8745bc4dbb895bedf00062bd27deb026709c72605f0d61 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -30,13 +30,13 @@ bad_flag 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 18c3 callers_json 0 b4cb44825705aaef95881ec889ba75da78fc3f18ba9a6b8c5e4dfec7122de4aa e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees_json 0 da139bc671d2f0676022a938cee69537c4f9282951fa6f487fdae19f7d7cd65f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact_json 0 c9cf3824750b16b493eb177644306987b7420db4ae37f07be18e9fa088574b8b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -uses_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 96ca20ae3803368f04315f1f2f0062893e7827cee740fb44e04278ff5f32e791 -grep_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 278827e2a7740aeeefa653b87ec47f74c98ec97a1758ce2e5426855a845a80c4 +uses_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 367ea0426632880940ea61ba78927234e09539dcc9076b04d20d9b37272abe77 +grep_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5f71cf9056a7cf72301ae4f9f577509330a04a4e9129d84cb804111f8f625af4 expand_missing 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 10f2dda880ec34c8d5a3f5bc50bd1bf50d0abe20c308de11ca8a16714f87b496 callers_missing 1 39a4cf4b2702863152edcc6c270255bac015a17d25e834dbdff181d809181326 0770e05552f4baf1c25d213e35c5f273c6f341e3c518e8233a83ff849abcd1c9 safe_delete 0 16990750bd7c003d73fa9c4ebb85ca8a9a3e6ffe8bf8f9987ed47f5459232403 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 98a50bcc6b32183d7967ee6af8f9219f083c00bde882f3aff4ebe1f39f4166e4 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 c297d80e427d8b1a709ed3df1164ce411c21d4295a9521162c1163295035b7c6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 8bb536a5956018df14882d87cc7bc61879711b880a03ad05569322668b272697 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/pyaliascallcheck.sh b/test/pyaliascallcheck.sh index da504ee60..a3aff275b 100644 --- a/test/pyaliascallcheck.sh +++ b/test/pyaliascallcheck.sh @@ -13,6 +13,10 @@ # c_cond.py if X: from m import f as g ; g() one conditional import: the only target it can bind # c_class.py from m import Widget as Gadget ; Gadget() a class (target m.py:9 Widget) # c_shadow.py names = [g for g in fs] ; return g() a comprehension target does not leak: the call after it is the import +# c_override.py mo_ga.py: f's def + a module `def __getattr__` PEP 562: __getattr__ answers only names m lacks, so it cannot +# override the def (PY-ALIAS-SPLIT-MARKER; target mo_ga.py:1); +# mo_gadeco.py: the same with a decorated def a decorator is not a conditional (target mo_gadeco.py:6) +# c_getattr.py the CALLER file defines `__getattr__` never consulted for a name the file binds (target m.py:1) # NEAR MISSES (must NOT bind m.py's f) # c_param.py def call_param(g): g() a parameter shadows the import # c_local.py g = other ; g() a local assignment shadows it @@ -30,7 +34,9 @@ # c_override.py the TARGET module rebinds f after its def: mo_import.py (`from fast_over import f`), mo_try.py (try-import), # mo_star.py (`from mstar import *` with an f), mo_assign.py (`f = other`), mo_dyn.py (`globals()["f"] = len`), # mo_ostar.py (a star import of a file outside the tree: unknown), mo_global.py (`global f` in a def): m.f is not -# provably the live name, so no pin (the unchanged ladder); each caller must not reach its target's line-1 f +# provably the live name, so no pin (the unchanged ladder); each caller must not reach its target's line-1 f. +# With a module `__getattr__`: mo_gacond.py (f's def under `if`), mo_gatry.py (under `try`) may not run, so +# __getattr__ may answer f; mo_gadyn.py (`globals()["f"] = len` too) still withholds (PY-ALIAS-SPLIT-MARKER) # c_shadow.py [g() for g in fs] / list(g() for g in fs) / lambda g: g() a comprehension target or lambda parameter named g # covers the call (the site-spanned records localNameSet skips) # c_member.py o.gm() / self.gm() through the alias spelling a member call is not the bare name: never m.py:1 @@ -60,7 +66,7 @@ callees(){ ( cd "$FIX" || exit 1; "$BIN" "$FIX" --no-cache --callees="$1" --lege # ── 0. presence: every fixture file parses (a fixture that fails to parse would pass the near misses inert) ── ( cd "$FIX" || exit 1; "$BIN" "$FIX" --no-cache --skipped --legend=compact ) >"$TMP/skipped" 2>/dev/null perl -pe 's///g' "$TMP/skipped" >"$TMP/skipped.body" -if ! grep -q 'degraded_parse="0"' "$TMP/skipped.body" || ! grep -q 'indexed="37"' "$TMP/skipped.body" || grep -q '//; s/^/ | if grep -q 'n="call_plain"' "$TMP/callers" && grep -q 'n="call_cond"' "$TMP/callers"; then ok "--callers=m.py:f names call_plain and call_cond" else no "--callers=m.py:f misses an aliased caller"; sed 's///; s/^/ | /' "$TMP/callers"; fi +# ── 1b. a module `__getattr__` cannot override a def that exists (PY-ALIAS-SPLIT-MARKER): target side mo_ga.py:1, caller side m.py:1 ── +for a in "call_mo_ga mo_ga.py:1" "call_mo_gadeco mo_gadeco.py:6" "call_cgetattr m.py:1"; do + set -- $a + callees "$1" + row=$( grep -o "]*p=\"$2\"[^>]*/>" "$TMP/$1" ) + if [ -n "$row" ] && ! printf '%s' "$row" | grep -q 'via=' && ! grep -q 'p="decoy.py' "$TMP/$1"; then + ok "$1 binds $2 f as a proven edge although a module __getattr__ is present" + else + no "$1: expected $2 f (proven, no via=) and no decoy row"; sed 's///; s/^/ | /' "$TMP/$1" + fi +done + # ── 2. near misses: never m.py's f ── for c in call_param call_local call_rebind call_dynamic call_star call_twomod call_fnimport call_inner call_disagree call_reexport call_nested call_external call_twice call_comp call_gen call_lambda call_obj call_self; do callees "$c" || continue @@ -89,6 +107,15 @@ for c in call_param call_local call_rebind call_dynamic call_star call_twomod ca else ok "$c: no import-pinned edge"; fi done +# ── 2a. a module `__getattr__` with a def that may not run (if / try), or with a globals() write: no pin ── +for a in "call_mo_gacond mo_gacond.py:4" "call_mo_gatry mo_gatry.py:2" "call_mo_gadyn mo_gadyn.py:1"; do + set -- $a + callees "$1" + if grep -q "p=\"$2\"" "$TMP/$1"; then + no "$1: pinned $2 though __getattr__ may answer f (must stay undecided)"; sed 's///; s/^/ | /' "$TMP/$1" + else ok "$1: no pin to $2"; fi +done + # ── 2b. the target module rebinds f after its def: a def-line pin to mo_*.py:1 would name the overridden def ── for c in call_mo_import call_mo_try call_mo_star call_mo_assign call_mo_dyn call_mo_ostar call_mo_global; do callees "$c" || continue diff --git a/test/pyaliascallfix/c_getattr.py b/test/pyaliascallfix/c_getattr.py new file mode 100644 index 000000000..9daca2e35 --- /dev/null +++ b/test/pyaliascallfix/c_getattr.py @@ -0,0 +1,9 @@ +from m import f as gm0 + + +def __getattr__(name): + return len + + +def call_cgetattr(): + return gm0() diff --git a/test/pyaliascallfix/c_override.py b/test/pyaliascallfix/c_override.py index f13bd1636..c07e8443e 100644 --- a/test/pyaliascallfix/c_override.py +++ b/test/pyaliascallfix/c_override.py @@ -33,3 +33,32 @@ def call_mo_ostar(): def call_mo_global(): return g7() + + +from mo_ga import f as g8 +from mo_gacond import f as g9 +from mo_gatry import f as g10 +from mo_gadyn import f as g11 + + +def call_mo_ga(): + return g8() + + +def call_mo_gacond(): + return g9() + + +def call_mo_gatry(): + return g10() + + +def call_mo_gadyn(): + return g11() + + +from mo_gadeco import f as g12 + + +def call_mo_gadeco(): + return g12() diff --git a/test/pyaliascallfix/mo_ga.py b/test/pyaliascallfix/mo_ga.py new file mode 100644 index 000000000..813dac34c --- /dev/null +++ b/test/pyaliascallfix/mo_ga.py @@ -0,0 +1,6 @@ +def f(): + return 1 + + +def __getattr__(name): + raise AttributeError(name) diff --git a/test/pyaliascallfix/mo_gacond.py b/test/pyaliascallfix/mo_gacond.py new file mode 100644 index 000000000..57533bd82 --- /dev/null +++ b/test/pyaliascallfix/mo_gacond.py @@ -0,0 +1,9 @@ +import sys + +if sys.platform == "never": + def f(): + return 1 + + +def __getattr__(name): + return len diff --git a/test/pyaliascallfix/mo_gadeco.py b/test/pyaliascallfix/mo_gadeco.py new file mode 100644 index 000000000..273b3ea9c --- /dev/null +++ b/test/pyaliascallfix/mo_gadeco.py @@ -0,0 +1,11 @@ +def deco(fn): + return fn + + +@deco +def f(): + return 1 + + +def __getattr__(name): + return len diff --git a/test/pyaliascallfix/mo_gadyn.py b/test/pyaliascallfix/mo_gadyn.py new file mode 100644 index 000000000..ed7652740 --- /dev/null +++ b/test/pyaliascallfix/mo_gadyn.py @@ -0,0 +1,9 @@ +def f(): + return 1 + + +def __getattr__(name): + return len + + +globals()["f"] = len diff --git a/test/pyaliascallfix/mo_gatry.py b/test/pyaliascallfix/mo_gatry.py new file mode 100644 index 000000000..7f3de676d --- /dev/null +++ b/test/pyaliascallfix/mo_gatry.py @@ -0,0 +1,9 @@ +try: + def f(): + return 1 +except ImportError: + pass + + +def __getattr__(name): + return len diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 0b9e25d5c..c773a1130 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -f4d5b120fc9f3f60c021a9fdec08844f52c4cea0606ff57bf4833329e2e7fe92 +7273ed82499a3ea224e7923f2c954067f9c8293e54d04ccae65e02ae518ac92a diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index d4b67e5c8..7e2f5c584 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,15 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-28, train 41 (lanes ci-flakes-3, defs-lookup, py-split-marker, fn-slot-2, path-hop-body-fix1, upward-2hop merged onto main +# 12056c16): RE-DERIVED ONCE with UPDATE_GOLDEN=1 on the composed tree (main's pin f4d5b120… and the py-split-marker and fn-slot-2 +# (127443d5…) branch pins conflicted). Watched text: kParserVer 251 -> 259 (rich 260; above the placeholders 249, 255 and fn-slot-s2's +# 257), quality.h's kIngestParserVerMirror with it. kCacheVersion stays 46 and kQSnapCacheScheme stays 24: no record changes shape +# and no watched quality.h function moved. +# 2026-10-27, lane py-split-marker (PY-ALIAS-SPLIT-MARKER, base integration/train-40 1318069a; PLACEHOLDER, the train renumbers): +# RE-PINNED ONCE with UPDATE_GOLDEN=1. Watched text: kParserVer 251 -> 255 (rich 256; above fn-slot-2's 249 and train 40's 251/252: +# a Python module-scope def/class inside an if/try/with/loop files a second VarDecl `?name`), quality.h's kIngestParserVerMirror with it. +# kCacheVersion stays 46 (no record changes shape) and kQSnapCacheScheme stays 24: no watched quality.h function moved. # 2026-10-27, train 40 (lanes ci-flakes-2, r1-followup, js-def-naming, testonly-dead, string-keyed, alias-channel, fn-slot, for-route # merged onto main aba4d28e): RE-DERIVED ONCE with UPDATE_GOLDEN=1 on the composed tree (main's pin b4eb6e92… and the js-def-naming, string-keyed and # fn-slot (18dfbcac…) branch pins conflicted -> f4d5b120…). Watched text: kParserVer 239 -> 251 (rich 252; above the placeholders 238, 241, 247 and @@ -44,6 +53,10 @@ PIN="$ROOT/test/qschemetrip.hash" # UPDATE_GOLDEN=1 (b4eb6e92… -> the pin above). The only watched text that moved is kParserVer 239 -> 241 (rich file tag 242; quality.h's # kIngestParserVerMirror with it): an IX-3 Pattern reference at a Python hasattr test or a JS/TS subscript store records qualifier "t". # kCacheVersion stays 44 and kQSnapCacheScheme stays 24: no record layout and no watched quality.h function moved. +# 2026-10-27, lane fn-slot-2 (part 2: Rust slots, base lane/fn-slot f2e6ef1f): RE-PINNED ONCE with UPDATE_GOLDEN=1 (18dfbcac… -> 127443d5…). +# Watched text: kParserVer 247 -> 249, a PLACEHOLDER (train 41 renumbers; Rust files now record slot-join value rows, so the dead +# set is extraction-dependent), its quality.h mirror with it. kCacheVersion stays 44 (no record changes shape). +# kQSnapCacheScheme stays 24: no watched quality.h function moved. # 2026-10-27, lane fn-slot (IX1-V2, base integration/train-39 3d03da53): RE-PINNED ONCE with UPDATE_GOLDEN=1 (b4eb6e92… -> 18dfbcac…). # Watched text: kParserVer 239 -> 247, a PLACEHOLDER (train 40 renumbers; Go value refs now include field assignments, method # values and registrar-parameter slot facts, so the dead set is extraction-dependent), its quality.h mirror with it. diff --git a/test/regression.sh b/test/regression.sh index fb40292ad..fead89513 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 attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck changelogfragcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composedhopscheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck danglingwarncheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck epmanifestcheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fcallforwardcheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck fnslotcheck forautobodycheck forblowupcheck forbodyfoldcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcallerhopcheck forcompresscheck forctorhopcheck fordisclosurecheck forhdrshapecheck forheadrrcheck forinitslotscheck forlenscheck formatgatecheck formaxtokenscheck formembernamescheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forroutecheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck goscopecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck honestlexcheck hookcheck hop1check hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck ifacedispatchcheck impactdepthcheck impactimportcheck impactpartitioncheck impacttypecheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jsdefnamingcheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localdeclcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck mapstemcheck 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 namekeyhopcheck 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 pathownercheck pathpaircheck 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 pyaliascallcheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnamecheck qualnewcheck querycheck queryfilescancheck queuehopcheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck receiverfixescheck 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 situhunkcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck slotjoincheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatehunkcheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testonlydeadcheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesliteralcheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck writtenspellingcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck changelogfragcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composedhopscheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck danglingwarncheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck defslookupcheck 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 epmanifestcheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fcallforwardcheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck fnslotcheck forautobodycheck forblowupcheck forbodyfoldcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcallerhopcheck forcompresscheck forctorhopcheck fordisclosurecheck forhdrshapecheck forheadrrcheck forinitslotscheck forlenscheck formatgatecheck formaxtokenscheck formembernamescheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forroutecheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck goscopecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck handlerrulecheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck honestlexcheck hookcheck hop1check hopbodycheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck ifacedispatchcheck impactdepthcheck impactimportcheck impactpartitioncheck impacttypecheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jsdefnamingcheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localdeclcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck mapstemcheck 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 namekeyhopcheck 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 pathownercheck pathpaircheck 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 pyaliascallcheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnamecheck qualnewcheck querycheck queryfilescancheck queuehopcheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck receiverfixescheck 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 situhunkcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck slotjoincheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatehunkcheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testonlydeadcheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesliteralcheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck writtenspellingcheck 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/ripwirepubliccheck.sh b/test/ripwirepubliccheck.sh index b36e4d76b..2cc3ef282 100755 --- a/test/ripwirepubliccheck.sh +++ b/test/ripwirepubliccheck.sh @@ -1295,6 +1295,9 @@ fi # docs/captures/COMMANDS_showcase_2026-10-27.md:606,621 — the same two prose lines, byte for byte (same content hashes), in the # train 40 re-capture (the recall picks that docs/COMMANDS.md section again after the regenerated COMMANDS.md). Re-keyed under # this date as above; nothing walks git history there. +# docs/captures/COMMANDS_showcase_2026-10-28.md:618,633 — the same two prose lines, byte for byte (same content hashes), in the +# lane/defs-lookup re-capture (adds --defs; the recall picks that docs/COMMANDS.md section again). Re-keyed under this date +# as above; nothing walks git history there. ARM9_OK='bench/cppbench/run_cppbench.py:e5d3f485c59b0187 bench/cppbench/run_cppbench.py:7e89ac16d50cd5d2 bench/cppbench/run_cppbench.py:5d65fe73e9406e01 @@ -1311,7 +1314,9 @@ 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 docs/captures/COMMANDS_showcase_2026-10-27.md:118fbfbb8f6823fe -docs/captures/COMMANDS_showcase_2026-10-27.md:22be6025a57af773' +docs/captures/COMMANDS_showcase_2026-10-27.md:22be6025a57af773 +docs/captures/COMMANDS_showcase_2026-10-28.md:118fbfbb8f6823fe +docs/captures/COMMANDS_showcase_2026-10-28.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/shapingflagcheck.sh b/test/shapingflagcheck.sh index b8b8d01ff..e64c241fe 100755 --- a/test/shapingflagcheck.sh +++ b/test/shapingflagcheck.sh @@ -308,6 +308,14 @@ if [ "$nHonor" -ge 3 ]; then ok "(B) $nHonor honouring rows proved to actually && ok "(C) a report/paging verb still REFUSES --top-k and does not also emit the notice" \ || no "(C) --hotspots --top-k=3 exited $rcH with: [$( head -c 200 "$TMP/hot.err" )]" +# CR nitpick (harvest 463, review 5692571864, src/cli.h:4239): --defs is an honorsPaging() member, so it holds no kShapingVerbs row +# (the header's invariant: a verb is in one table, never both). The row was dead — noticeShapingFlagIgnored returns before reading it. +if grep -n -E '^[[:space:]]*\{ "--defs"[[:space:]]*,' "$ROOT/src/cli.h" >/dev/null; then no "(C) --defs holds a kShapingVerbs row while honorsPaging() already owns it"; else ok "(C) --defs is in honorsPaging() only: no kShapingVerbs row"; fi +"$BIN" . --defs=main --max-tokens=200 >/dev/null 2>"$TMP/defs.err"; rcD=$? +{ [ "$rcD" -eq 1 ] && grep -q 'honoring set' "$TMP/defs.err" && ! grep -q 'is not read by' "$TMP/defs.err"; } \ + && ok "(C) --defs --max-tokens=200 still REFUSES and does not also emit the notice" \ + || no "(C) --defs --max-tokens=200 exited $rcD with: [$( head -c 200 "$TMP/defs.err" )]" + # ── (D) the default map and its riders honour both and must stay silent ──────────────────────────────── for probe in "--top-k=3" "--max-tokens=200"; do "$BIN" . "$probe" >"$TMP/d.out" 2>"$TMP/d.err"; rcD=$? diff --git a/test/showcase_capture.py b/test/showcase_capture.py index 5a49f0e10..36415e3d4 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -390,6 +390,7 @@ def add(section, cmd, what, **opts): add(S2, f"{BIN} . --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.") add(S2, f"{BIN} . --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).") add(S2, f"{BIN} . --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\").") +add(S2, f"{BIN} . --defs=main,rankGraphTeleport", "Which definitions each name binds, one row per definition group with sel= (the spelling that picks it alone in any verb), class=source|test and in= callers; source first.") add(S2, f"{BIN} . --connect=rankGraphTeleport,runEval,getIndex", "Minimal connecting subgraph over 3 symbols (finds shared-caller joins).") add(S2, f"{BIN} . --impact=rankGraphTeleport", "Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.") add(S2, f"{BIN} . --mentions=rankGraphTeleport", "Markdown docs that name SYM in a backtick (doc<->code edges).") diff --git a/test/writtenspellingcheck.sh b/test/writtenspellingcheck.sh index 3901df0b1..47eb972f6 100755 --- a/test/writtenspellingcheck.sh +++ b/test/writtenspellingcheck.sh @@ -60,6 +60,17 @@ refuse ts 'AliasDriver.beginTransaction' # a value alias is not an owner refuse ts 'Missing.beginTransaction' refuse ts 'driver.RuntimeDriver.inner' # no such member +echo "-- (1b) a JS/TS private member as the rows print it, Owner.#name (lane D2, readout-40 G; RED on train-40 1318069a: every bind arm rc=1)" +bind ts 'RuntimeDriver.#dispatch' tsPrivHelper tsOtherPrivHelper +bind ts 'Other.#dispatch' tsOtherPrivHelper tsPrivHelper +bind ts 'src/driver/runtime-driver.ts:RuntimeDriver.#dispatch' tsPrivHelper tsOtherPrivHelper +bind ts '#dispatch' tsPrivHelper # the bare name: both, as before (a union) +bind ts '#dispatch' tsOtherPrivHelper # ... and the other one too: a union names BOTH definitions' callees (CR 4408459940) +refuse ts 'Missing.#dispatch' # no owner of that name +refuse ts 'RuntimeDriver.#nope' # no such private member +refuse ts 'RuntimeDriver.dispatch' # the '#' is part of the name: the unprefixed spelling names nothing +refuse ts 'src/other.ts:RuntimeDriver.#dispatch' # the owner is not in that file + echo "-- (2) module / crate / package prefixes" bind py 'asyncio.run' py_asyncio_run_helper py_runner_method_helper py_app_run_helper bind py 'json.dump' py_json_dump_helper py_app_dump_helper diff --git a/test/writtenspellingfix/ts/src/driver/runtime-driver.ts b/test/writtenspellingfix/ts/src/driver/runtime-driver.ts index 21563f5e3..f8235ffcd 100644 --- a/test/writtenspellingfix/ts/src/driver/runtime-driver.ts +++ b/test/writtenspellingfix/ts/src/driver/runtime-driver.ts @@ -1,7 +1,9 @@ export class RuntimeDriver { async beginTransaction(x: number): Promise { return tsBeginHelper(x) } commitTransaction(x: number): number { return tsCommitHelper(x) } + #dispatch(x: number): number { return tsPrivHelper(x) } } export const AliasDriver = RuntimeDriver function tsBeginHelper(x: number): number { return x } function tsCommitHelper(x: number): number { return x } +function tsPrivHelper(x: number): number { return x } diff --git a/test/writtenspellingfix/ts/src/other.ts b/test/writtenspellingfix/ts/src/other.ts index caf6059d8..caa2a82da 100644 --- a/test/writtenspellingfix/ts/src/other.ts +++ b/test/writtenspellingfix/ts/src/other.ts @@ -1,4 +1,6 @@ export class Other { beginTransaction(): number { return tsOtherHelper() } + #dispatch(): number { return tsOtherPrivHelper() } } function tsOtherHelper(): number { return 1 } +function tsOtherPrivHelper(): number { return 2 }

@@ -3082,7 +3118,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3112,13 +3148,13 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - - - - + + + + + ``` @@ -3166,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":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":794,"graph_ambiguous":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":797,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3187,12 +3223,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":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":794,"graph_ambiguous":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":797,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3238,12 +3274,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":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":794,"graph_ambiguous":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":797,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3302,11 +3338,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":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"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":2980,"graph_unresolved":15905,"graph_unindexed":275,"counts_floor":true}}]} +"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3352,19 +3388,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] ``` @@ -3389,17 +3425,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] @@ -3443,10 +3479,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:11977 +$ ./build/ripwire . --at=src/graph.h:11993 - - + + ``` @@ -3470,7 +3506,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3496,11 +3532,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3575,7 +3611,7 @@ _A second selection, the `fix/` ref family, picked at capture time from the refs ``` $ ./build/ripwire . --stray-content=fix/ - + ``` @@ -3600,7 +3636,7 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + @@ -3636,20 +3672,20 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - + - - - + + - + -... [7 more line(s); run it to see the whole thing] + +... [2 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -3671,11 +3707,11 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - + + + + + ... [14 more line(s); run it to see the whole thing] ``` @@ -3701,10 +3737,10 @@ _The same page with every t= line whole: the page above previews each line over ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text - - - - + + + ... [24 more line(s); run it to see the whole thing] @@ -3729,11 +3765,11 @@ _The whole hit list asked for by name: under the same 60-row cap HEAD's referenc ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - + + - - + + ... [23 more line(s); run it to see the whole thing] @@ -3761,7 +3797,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3842,8 +3878,8 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - + @@ -3877,7 +3913,7 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + @@ -3911,7 +3947,7 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + @@ -3944,8 +3980,8 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - + + @@ -3978,7 +4014,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -4013,10 +4049,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:11977 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:12018 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1754 - #3 0x102e01a44 in main src/main.cpp:4175 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:11993 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:12034 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1755 + #3 0x102e01a44 in main src/main.cpp:4177 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -4043,7 +4079,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4143,7 +4179,7 @@ $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" (run= when derivable). window: shown= total= capped= (capped=1 cut). est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the ind … [line truncated: 384 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 ) @@ -4176,7 +4212,7 @@ $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --part - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4203,7 +4239,7 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - + @@ -4276,14 +4312,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + @@ -4310,14 +4346,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4346,7 +4382,7 @@ $ ./build/ripwire . --skipped - @@ -4431,11 +4467,11 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + - - + + @@ -4444,8 +4480,8 @@ $ ./build/ripwire . --rank-by=churn --top-k=5 - -... [8 more line(s); run it to see the whole thing] + +... [9 more line(s); run it to see the whole thing] ``` **Shaped by:** `--since`, `--in` @@ -4466,22 +4502,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 --exclude=changelog.d --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --exclude=changelog.d --in=src --limit=3 - - - - - - - - - - - - -... [15 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4543,7 +4579,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4571,7 +4607,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=a3aaaa12b102080c entries=958 +ripwire legend dictionary ripwire.dict/v1 dictv=05b48ed93c807ee2 entries=981 : 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=) @@ -4615,7 +4651,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 --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. +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 --defs. 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** @@ -4624,12 +4660,12 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - + @@ -4661,17 +4697,17 @@ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4692,17 +4728,17 @@ $ ./build/ripwire . --map-diff --top-k=5 - - + + - + - - - - + + + + ... [9 more line(s); run it to see the whole thing] ``` @@ -4726,17 +4762,17 @@ $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4776,17 +4812,17 @@ $ ./build/ripwire . --no-cache --top-k=3 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4807,17 +4843,17 @@ $ ./build/ripwire . --no-ignore --top-k=3 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4843,14 +4879,14 @@ $ ./build/ripwire . --max-file-size=8K --top-k=3 - + - + - + @@ -4923,17 +4959,17 @@ $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - - + + - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4976,10 +5012,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` | 729 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **726 gate scripts**, all of which exist on disk. +naming **729 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 726. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 729. 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 1470fb7b8..e3d188281 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -30,10 +30,10 @@ a small explicit bound" is safe only when nothing inside the group repeats; it i | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 287 | 113 | 170 | **117** | +| 298 | 115 | 180 | **118** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 287 + 7 is the 294 constants this generator parses out of `src/`. +are not counted as caps, and 298 + 7 is the 305 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -51,8 +51,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. **140 of 287 caps are classified -(50 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 147 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **142 of 298 caps are classified +(50 INDEXING, 46 OUTPUT, 46 BOUNDARY); the remaining 156 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 @@ -102,7 +102,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 113 files that declare a cap — the 287 caps counted above, and no parameter. +One table for each of the 115 files that declare a cap — the 298 caps counted above, and no parameter. ### `src/abicheck.h` @@ -252,6 +252,14 @@ Discloses: **none** | --- | --- | --- | --- | | `kDefTableCap` | `16` | — | — | +### `src/defslookup.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kDefsNamesMax` | `16` | — | — | + ### `src/didyoumean.h` Discloses: **none** @@ -390,6 +398,8 @@ Discloses: `capped` | --- | --- | --- | --- | | `kForCallerHopDepth2Rows` | `4` | — | — | | `kForCallerHopRows` | `16` | — | — | +| `kForRankedHopHeadRows` | `3` | BOUNDARY | r=1..3: the emitted head the seed is read from | +| `kForRankedHopMaxCallers` | `3` | BOUNDARY | the narrow-funnel bar: a wider fan-in is the callers verb's page, not a chain | ### `src/forctorhop.h` @@ -507,6 +517,17 @@ Discloses: `capped`, `syms_capped` | `kHandoffSymbolsPerCodeFile` | `50` | OUTPUT | — | | `kHandoffSymbolsPerDocFile` | `12` | OUTPUT | — | +### `src/hopbody.h` + +Discloses: `capped` + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kHopBodyCapBytes` | `4000` | — | — | +| `kPathHopBodyMaxHops` | `2` | — | — | +| `kWrapperFollowDepth` | `2` | — | — | +| `kWrapperFollowMax` | `3` | — | — | + ### `src/infra/blanktext.h` Discloses: **none** @@ -868,9 +889,13 @@ Discloses: `gap_syms_capped`, `rows_capped` | constant | value | class | note | | --- | --- | --- | --- | | `kBridgeMaxHops` | `8` | — | a longer chain is no answer a reader would follow: none is printed | +| `kHandlerRuleListMax` | `16` | — | the bridge's own width limit: at most this many defs are listed as | +| `kHandlerRuleShown` | `3` | — | rows: the widest posted classes first | | `kNearSinkDepth` | `2` | — | proven caller hops back from to= (1 = a direct caller) | | `kNearSinkRows` | `4` | — | rows printed; rows= counts them all, rows_capped="1" says so | | `kPathGapRows` | `3` | — | rows printed; gap_syms= counts them all, gap_syms_capped="1" says so | +| `kRouteBodyMax` | `4` | — | spew guard: bodies per answer; past it capped="1" next= the first unshown | +| `kRouteBodyMaxHops` | `2` | — | bridge rows 0..2 (from= and the next two), never the to= row | ### `src/pathpairs.h` diff --git a/docs/TUNING.md b/docs/TUNING.md index 4b8df8701..5c05400c6 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 195 | 194 | 112 | 12 | **37** | 75 | +| 206 | 205 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **195 cap -declarations** under **194 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 **206 cap +declarations** under **205 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 194 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **71 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 205 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **82 declared since the sweep was prepared, which no measurement has touched** (`kBackHopWidth`, `kBaseScopeScanCap`, -`kBridgeMaxHops`, `kCallSitesPerRowCap`, `kCallersBandMaxBytes`, `kChurnMergeBombMaxFiles`, +65 -more). 111 + 12 + 71 = 194, and `emit` refuses to render a partition that does not add up. The +`kBridgeMaxHops`, `kCallSitesPerRowCap`, `kCallersBandMaxBytes`, `kChurnMergeBombMaxFiles`, +76 +more). 111 + 12 + 82 = 205, 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 195" would be wrong in both halves +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 206" 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 195-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 206-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/docs/captures/COMMANDS_showcase_2026-10-28.md b/docs/captures/COMMANDS_showcase_2026-10-28.md new file mode 100644 index 000000000..43879096d --- /dev/null +++ b/docs/captures/COMMANDS_showcase_2026-10-28.md @@ -0,0 +1,7973 @@ +# ripwire — every verb, run for real + +- **Date:** 2026-10-28 (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.8 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=c8b49812b)` +- **Repo:** the ripwire repo @ `c8b49812` — **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` (213 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.* + +**wall time: 3.34s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [750 more display lines; full output is 24662 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +**wall time: 1.99s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [68 more display lines; full output is 8648 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.* + +**wall time: 2.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --top-k=0 --expand=rankGraphTeleport` + +*NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* + +**wall time: 1.82s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --token-budget=100` + +*GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape).* + +**exit code: 3** — **wall time: 1.59s** + +````` + + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=9940 > 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: 7.20s** + +````` + + + + +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 +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +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 bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult& ou … [line truncated: 7 more bytes on this line] +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& imple … [line truncated: 62 more bytes on this line] +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>& cach … [line truncated: 98 more bytes on this line] + +persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex +inline const McpIndex& getIndex( const std::string& root ) +inline std::uint64_t contentHash64( std::string_view s ) noexcept +struct CacheEntry +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 CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +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::uint64_t byteHash( const char* data, std::size_t n ) noexcept +void emitGrepHandleLegend( bool enabled ) +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +… [127 more display lines; full output is 19923 bytes on 8 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +**wall time: 3.84s** + +````` + + + + + +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 ) +… [28 more display lines; full output is 5863 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --no-route` + +*Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* + +**wall time: 3.24s** + +````` + + + + + +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… +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 std::string didYouMean( const IngestResult& ing, std::string_view name ) +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] +inline const McpIndex& getIndex( const std::string& root ) +struct DataSectionsCut +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 36 more bytes on this line] +### 2. Gates for A +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +… [155 more display lines; full output is 24097 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --signatures-only` + +*T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* + +**wall time: 2.82s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + + + +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --adaptive` + +*Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* + +**wall time: 3.75s** + +````` + + + + +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 +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 +inline bool hasPytestTable( std::string_view source ) +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [139 more display lines; full output is 19839 bytes on 44 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` + +*Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* + +**wall time: 2.93s** + +````` + + + + +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 ) +struct NameCorpusStats +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +inline bool hasIdentifierShape( std::string_view w ) noexcept +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +struct LensRanking +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline constexpr int kLexWeightDoc = 2 +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 std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +std::optional<int> runEvalViews( const MainDispatch& d ) +inline double binomialChoose( std::size_t n, std::size_t k ) +struct RecallSectionPick +… [147 more display lines; full output is 21622 bytes on 18 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` + +*Same task with the anchor disabled — the contrast the flag exists for.* + +**wall time: 3.11s** + +````` + + + + +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 ) +struct NameCorpusStats +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +inline bool hasIdentifierShape( std::string_view w ) noexcept +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +struct LensRanking +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline constexpr int kLexWeightDoc = 2 +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 std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +std::optional<int> runEvalViews( const MainDispatch& d ) +inline double binomialChoose( std::size_t n, std::size_t k ) +struct RecallSectionPick +… [156 more display lines; full output is 21826 bytes on 18 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).* + +**wall time: 1.43s** + +````` + + + + + +````` + +## `./build/ripwire . --help-task="calls(runDefaultMap, rankGraphTeleport)"` + +*Deterministic enhanced help: a closed claim in the task is a structured shape, so the router recommends the ONE command that answers it (--verify) with the evidence behind the pick. Advice only — nothing executes.* + +**wall time: 1.47s** + +````` + + + + +ripwire '.' --verify='calls(runDefaultMap, rankGraphTeleport)' + + +````` + +## `./build/ripwire . --help-task="write a cheerful release announcement"` + +*The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing.* + +**wall time: 2.36s** + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +**wall time: 4.05s** + +````` +ripwire recall — "quality delta gating exit codes" — 124 relevant of 314 document files, best-first — total=124 shown=8 capped=1 truncated=6 generated_demoted=3 max_tokens=8000 share_bytes=2468 est_tokens=4165 + +━━ README.md (relevance 8.079) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 222723 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.531) ━━ [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 +… [114 more lines, 10620 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 1297195-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* + +````` +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 + +━━ commands.md (relevance 11.020) ━━ [sections: 1 of 165 selected (193 in doc), section-granular; whole doc 595976 B; lines="557-582"; dropped_by_budget=164] [truncated: 1854 of 4257 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base + +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? + +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: + +1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd + `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, + `.log`, `.json` and extensionless files are **not** documents to it. A directory of those + answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched + your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, + never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir + holds five dumps, and the header says `2 relevant of 2 document files` because only the two + `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not + in the population at all. + +2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so + an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document + SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R + selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got + and what the ceiling cost. … + +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +````` + +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).* + +**wall time: 3.16s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [367 more display lines; full output is 11127 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 10.81s** + +````` +(empty) +````` + +Artifact written: + +````` + 156086 /aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +**wall time: 3.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [68 more display lines; full output is 8666 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.* + +**wall time: 4.40s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [165 more display lines; full output is 8029 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-depth=2` + +*The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* + +**wall time: 2.69s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1965 more display lines; full output is 53822 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-fanout=4` + +*The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* + +**wall time: 2.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [11 more display lines; full output is 3893 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --callers=rankGraphTeleport` + +*Who calls SYM (1-hop in-edges).* + +**wall time: 1.49s** + +````` + + + + + + + + + + +````` + +## `./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.* + +**wall time: 1.95s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=DoesNotExist` + +*Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* + +**exit code: 1** — **wall time: 4.15s** + +````` + + +````` + +stderr: + +````` +ripwire: --callers symbol not found: DoesNotExist +````` + +## `./build/ripwire . --callees=rankGraphTeleport` + +*What SYM calls (1-hop out-edges).* + +**wall time: 1.67s** + +````` + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --uses=rankGraphTeleport` + +*The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* + +**wall time: 1.15s** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +**wall time: 1.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [28 more display lines; full output is 3650 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 5602 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 5481 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --path=main,rankGraphTeleport` + +*Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0").* + +**wall time: 1.01s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --defs=main,rankGraphTeleport` + +*Which definitions each name binds, one row per definition group with sel= (the spelling that picks it alone in any verb), class=source|test and in= callers; source first.* + +**wall time: 1.06s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex` + +*Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* + +**wall time: 1.07s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --impact=rankGraphTeleport` + +*Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* + +**wall time: 1.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [71 more display lines; full output is 6959 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +**wall time: 1.18s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +**wall time: 1.33s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --situ` + +*Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* + +**wall time: 2.74s** + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: c8b49812b + (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: 4.46s** + +````` + + +````` + +## `./build/ripwire . --grep=DISCLOSE` + +*Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* + +**wall time: 1.02s** + +````` + +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.76s** + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16840 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --query="teleport pagerank" --top-k=5` + +*Raw BM25 ranking (debug lens; --for is the real verb).* + +**wall time: 1.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [22 more display lines; full output is 3459 bytes on 1 raw line(s)] +````` + + +--- + +# zoom the detail ladder + +## `./build/ripwire . --for="pagerank power iteration" --detail=2` + +*Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* + +**wall time: 2.54s** + +````` + + + + + +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] +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 + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +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 +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +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] +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [156 more display lines; full output is 22220 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [151 more display lines; full output is 15392 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --outline=rankGraphTeleport --top-k=0` + +*Control-flow skeleton of one symbol, payload-only via the new --top-k=0.* + +**wall time: 1.18s** + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +## `./build/ripwire . --outline=rankGraphTeleport:1-10 --top-k=0` + +*CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).* + +**wall time: 1.40s** + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +stderr: + +````` +ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — outlining the whole symbol (use --expand=rankGraphTeleport:1-10 for a body slice) +````` + +## `./build/ripwire . --expand=rankGraphTeleport --top-k=0` + +*Full body + inline callee signatures.* + +**wall time: 1.59s** + +````` + + + + +: files the ranked rows show / do not (sum = files=), each named or counted below. : a program entry by its language's convention (C/Go/Rust main, Java/C# static main, a script main its module scope calls, __main__.py), callees as c rows; manifest entries not read. : n= unshown files under dir p=; f= names its code files, the rest (tests, docs, config, test/doc/example dirs) counted. declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry. external=K: K calls proven outside the tree, no edge. locality_pinned=K: K calls pinned by locality alone (a guess). extent_suspect_syms=K: K defs failed containment, corpus-wide. macro_blanked_files=K: K files indexed from a macro-blanked re-parse. prov=scip|binding|import|split|final-segment: how that edge bound (absent: one unique name); split = one arm of an amb= pick; final-segment = a qualified type matched by last name only. files=/symbols=: files and symbols indexed; edges= distinct call edges; shown= symbols printed, a merged row counting each def; ambiguous= calls split over several defs, corpus-wide; unresolved= calls with in-tree evidence and no edge (every def language-filtered or unreachable, or binding refused); order= rows by rank (important-first, important-last; (auto:fill) = flipped past a size threshold) or by path (stable). skipped_oversize=K: K files over a size ceiling, not indexed. unindexed=ext:N: N text files of that extension no grammar reads (6 extensions at most). unindexed_exts=E: E such extensions in all, the list cut. : a non-test row an indexed test transitively reaches (absent otherwise, never 0). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. locals_floor=1: locals= is a floor. role=hub: in= is 8 or more. s in= out= cx= ccx= loc= params= nest= cbo= lcom4= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, class cohesion, 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=). --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [98 more display lines; full output is 11208 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +**wall time: 1.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [885 more display lines; full output is 23687 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 5.79s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5767 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 3.87s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [308 more display lines; full output is 16263 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 4.54s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 8.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5144 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --arch=test/archfix/rules.txt` + +*Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* + +**wall time: 3.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1179 more display lines; full output is 173724 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 7.05s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [682 more display lines; full output is 70696 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` + +*User lint rules (YAML, ast-grep style) from a directory.* + +**wall time: 2.90s** + +````` + + + + +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf + +````` + +stderr: + +````` +ripwire: lint-rules: test/lintrulesfix/rules/malformed.yaml:5: expected 'key: value' — file skipped +[math degraded] lint-rules: malformed rule file skipped (lintrules.h:399, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* + +Input file: + +````` +#PROF_TSV_BEGIN one row per scope, aggregated across threads; counters are RAW integers +scope file line calls total_ms l1d_mpki +walk: chase pass x.cpp 9 12 48.500 7.250 +#PROF_TSV_END +````` + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 3428 bytes on 1 raw line(s)] +````` + +The joined finding — past the display cut above, extracted so the join is visible: + +````` +p = p->next +````` + +## `./build/ripwire . --communities` + +*Cluster the call graph into cohesive modules.* + +**wall time: 1.54s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [190 more display lines; full output is 16945 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=.* + +**wall time: 1.89s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [137 more display lines; full output is 7479 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom --zoom-levels=3` + +*The restoring knob: --zoom-levels=N prints N levels (0 = the whole tree); here three, ~11 KB.* + +**wall time: 1.71s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [291 more display lines; full output is 13185 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +**wall time: 1.77s** + +````` + + +# ripwire architecture report + +4056 files · 33884 symbols · 31822 edges · 3979 modules (15964 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 4203 declaration, 2539 header, 3947 source, 5275 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 3979) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1373 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 556 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 259 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:9108 [pack,allow,insert]** — 182 symbols +- **test/ownerhopfix/py/pkg::big_handler_001@broadcast.py:3:72** — 106 symbols +- **src::buf@infra/svector.h:125:8942 [insert,find,push]** — 96 symbols +- **src/infra::fail@os_win32.cpp:63:2553 [open,get,close]** — 89 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,collect,compute]** — 68 symbols +- **test::no@estchargecheck.sh:52:4108 [skip,set]** — 63 symbols +- **src::child@ingest_valuerefs.h:685:36070 [emit,resolve,open]** — 59 symbols +- **src::viaLinkLegend@graphlegend.h:1217:112851 [route,run,split]** — 52 symbols +- **src::sliceKindIs@slice.h:368:24239 [match,collect,push]** — 48 symbols + +## God files (most depended-on; showing 10 of 239) +- `src/model.h` — 110 dependents +- `src/infra/emit.h` — 88 dependents +- `src/infra/Diagnostics.h` — 65 dependents +- `src/graph.h` — 51 dependents +- `src/serialize.h` — 50 dependents +… [35 more lines, 3929 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +**wall time: 2.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [82 more display lines; full output is 12194 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mermaid` + +*Module (directory) dependency graph as a Mermaid diagram.* + +**wall time: 1.81s** + +````` +%% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. +flowchart LR + subgraph sg0 ["src"] + n102["src
10011"] + n103["src/infra
1301"] + end + subgraph sg1 ["test"] + n104["test
5600"] + n642["test/legochargefix
546"] + n420["test/fixtures/recallpassage
314"] + n1069["test/slotjoinfix
175"] + n879["test/ownerhopfix/py/pkg
158"] + n357["test/expandmodefix
151"] + n359["test/expandsibsfix
149"] + n1052["test/rubyschemafix
99"] + n471["test/forbodyfoldfix/py
98"] + n1034["test/rubyattrsfix
85"] + end + n74["docs
867"] + n2[".github
633"] + n0["(root)
606"] + 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"] +… [19 more lines, 1577 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 6.81s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [981 more display lines; full output is 107432 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --dead-code=src` + +*Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* + +**wall time: 1.74s** + +````` + + + + +````` + +## `./build/ripwire . --exercises=test/regression.sh` + +*Which symbols a TEST FILE exercises — the reverse direction of --affected.* + +**wall time: 1.07s** + +````` + + + +````` + +## `./build/ripwire . --community=0` + +*Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* + +**wall time: 1.55s** + +````` + + + +````` + +## `./build/ripwire . --quality-delta` + +*On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* + +**wall time: 40.67s** + +````` + + +````` + +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: 31.92s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [142 more display lines; full output is 16388 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: 31.27s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +**wall time: 6.34s** + +````` + + + +````` + +## `./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: 16.93s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [69 more display lines; full output is 7118 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* + +**wall time: 28.99s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 85.99s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [230 more display lines; full output is 30484 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=fix/` + +*A second selection, the `fix/` ref family, picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* + +**wall time: 6.71s** + +````` + + + +````` + +## `./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: 180.28s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [33522 more display lines; full output is 4640644 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` + +*A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* + +**exit code: 1** — **wall time: 6.89s** + +````` +(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: 571.53s** + +````` + + + + + + + + + + + + + + + +````` + +## `./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: 20.97s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 41.65s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 13366 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code` + +*A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.* + +**wall time: 20.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14379 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: 40.48s** + +````` + + + + + + + +````` + +## `./build/ripwire . --flags` + +*The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* + +**wall time: 2.65s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [427 more display lines; full output is 26080 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASAN` + +*Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.* + +**wall time: 2.45s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASA` + +*Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN).* + +**exit code: 1** — **wall time: 2.92s** + +````` +(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: 15.43s** + +````` +{"v":1,"verb":"plan-lanes","at":"c8b49812b","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":4056,"symbols":33884,"edges":318 … [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":659,"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":1211,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":10,"churn":61,"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":1793,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":61,"tested":0}, +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":3033,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":11,"churn":455,"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":399,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":28,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":693,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":455,"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":5916,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":498,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":57}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":40}, +{"p":"./src/editcheck.h","symbols":1,"churn":61,"ccx":10,"hotspot_rank":43}, +{"p":"./src/main.cpp","symbols":2,"churn":455,"ccx":12,"hotspot_rank":5}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":498,"ccx":2,"hotspot_rank":4}, +{"p":"./src/pageview.h","symbols":1,"churn":28,"ccx":2,"hotspot_rank":134}, +{"p":"./src/testmap.h","symbols":1,"churn":61,"ccx":4,"hotspot_rank":35}, +{"p":"./src/verbs_change.h","symbols":1,"churn":82,"ccx":9,"hotspot_rank":27}]},"blast_radius":{"reaches":47,"files_total":16,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editcheck.h","./src/editplan.h","./src/editpreview.h","./src/flipimpact.h","./src/main.cpp","./src/mcp.h … [line truncated: 182 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-terra","reasoning":"high","rule":"moderate-cross-module","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":1,"max_ccx":11,"sum_ccx":61,"ambiguous_calls":6,"blast_reaches":47,"contract_touches":0,"conflicts":0,"untested":47 … [line truncated: 142 more bytes on this line] +{"id":"lane-1","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/cli.h","n":"inHostActive","scope":"rw","key":"4446994deb74226c","id":"./src/cli.h::rw::inHostActive","id_addressable":true,"id_collides_with":0,"l":4675,"ord":0,"overloads":1," … [line truncated: 46 more bytes on this line] +{"p":"./src/jsrunner.h","n":"looksLikeJsTestFile","scope":"rw::jsrunner","key":"64417bc2d02fc89b","id":"./src/jsrunner.h::rw::jsrunner::looksLikeJsTestFile","id_addressable":true,"id_collides_with":0,"l":511,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":8,"churn":17,"tested":0}, +… [69 more display lines; full output is 20523 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: 15.52s** + +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":"c8b49812b","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":4056,"symbols":33884,"edges":31822,"ambiguous":3015,"unresolved":15989},"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":659,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":498,"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/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/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":82,"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":5916,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":498,"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/docdrift.h","symbols":7,"churn":36,"ccx":60,"hotspot_rank":40}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":107}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":498,"ccx":2,"hotspot_rank":4}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":65}, +{"p":"./src/verbs_change.h","symbols":1,"churn":82,"ccx":4,"hotspot_rank":27}], +"symbols_total":3640,"symbols_capped":true},"blast_radius":{"reaches":13,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":13,"module_span":5,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":5,"module_span":5,"max_ccx":38,"sum_ccx":70,"ambiguous_calls":14,"blast_reaches":13,"contract_touches":1,"conflicts":0,"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":140,"l":280,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":15, … [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":2953,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":516,"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":3032,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":516,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":140,"l":885,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":18,"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":49,"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":3097,"ord":0,"overloads":1,"amb":1,"cx":4,"ccx":3,"churn":516,"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}, +{"p":"./src/cli.h","n":"printUsageTier","scope":"rw","key":"82b7af67acdca479","id":"./src/cli.h::rw::printUsageTier","id_addressable":true,"id_collides_with":0,"l":3083,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":1,"churn":516,"tested":0}, +{"p":"./src/crossref.h","n":"writeWhereisTextCut","scope":"crossref","key":"9a3ef6403f53ce21","id":"./src/crossref.h::crossref::writeWhereisTextCut","id_addressable":true,"id_collides_with":0,"l":4542,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":3,"churn":95,"tested":0}, +… [58 more display lines; full output is 18348 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes=99 --task=x` + +*Out-of-range refusal shape for the lane count.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out) +````` + +## `./build/ripwire . --layout=Symbol` + +*CPU/GPU contract view of one struct: computed offsets/sizes/padding + mirror check.* + +**wall time: 2.43s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --layout=Lang` + +*The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated.* + +**exit code: 1** — **wall time: 4.50s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped enum's underlying type is not a byte layout) +````` + +## `./build/ripwire . --doc-drift` + +*Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp).* + +**wall time: 6.38s** + +````` + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +… [244 more display lines; full output is 24872 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --doc-drift --gateability` + +*The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* + +**wall time: 5.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [274 more display lines; full output is 26289 bytes on 1 raw line(s)] +````` + +Tail of the same output — the `` section: + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doc-drift --with-history` + +*Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* + +**wall time: 8.69s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [224 more display lines; full output is 24935 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).* + +**wall time: 2.42s** + +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:11993 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:12034 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1755 + #3 0x102e01a44 in main src/main.cpp:4177 + #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 4915 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.* + +**wall time: 1.77s** + +````` + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI"` + +*ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* + +**wall time: 1.64s** + +````` + + + + +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 + +L8 (train 29): the NEXT PAGE of a callers listing — the CLI --callers root's next=, find_referencing_symbols' next and find_symbol's calledBy_next all name this one call, so the three cannot spell tinline std::string callHierarchyPageNext( bool wantCallers, std::string_vi … [line truncated: 98 more bytes on this line] +inline std::string classNewGroupXml( const IngestResult& ing, std::span<const ClassNewRow> rows, std::string_view rootPrefix, EscapeFn ex ) + + + + + + + + + +… [67 more display lines; full output is 12092 bytes on 59 raw line(s)] +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3` + +*Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* + +**wall time: 1.96s** + +````` + 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] +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 + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +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 +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +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] +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [124 more display lines; full output is 19110 bytes on 10 raw line(s)] +````` + +## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` + +*Per-file metrics as CodeCharta cc.json.* + +**wall time: 4.87s** + +````` +(empty) +````` + +Artifact written: + +````` + 630319 /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: 10.78s** + +Input file: + +````` +for:incremental cache invalidation +callers:rankGraphTeleport +grep:DISCLOSE +lego:Vehicle +````` + +````` + + + + + + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…inline std::string spanTierMemoPath( const std::string& diskPath ) + +Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] + +P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) +struct FileHealth +inline 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 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& cac … [line truncated: 66 more bytes on this line] +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline const McpIndex& getIndex( const std::string& root ) +inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) +struct McpIndex +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 ) +static bool cachePathIsDirectory( const std::string& cachePath ) +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] +HashMap<const TSLanguage*, TSQuery*>& compiledQueryCache() +inline std::string oracleCachePath( const std::string& root, const std::string& headSha ) +… [107 more display lines; full output is 51609 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: 58.97s** + +````` + + + + + + + + + + +````` + + +--- + +# 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.* + +**wall time: 1.63s** + +````` + + + +````` + +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: 11.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --exclude=changelog.d --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +**wall time: 13.64s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=bogus --top-k=5` + +*An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf|churn|churn-decay) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=columnar` + +*Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* + +**wall time: 4.30s** + +````` + + +0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp + +0,1,1,2,3,3,3 +getIndex,rankGraph,anchoredLexicalRank,runEval,churnDecayRanking,churnRankedGraph,runDefaultMap +1184,12032,12678,171,1427,1466,1753 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 +src/mcpindex.h:1288,src/graph.h:12035,src/graph.h:12729,src/eval.h:328,src/main.cpp:1441,src/main.cpp:1480 src/main.cpp:1481 src/main.cpp:1495,src/main.cpp:1873 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +**wall time: 8.98s** + +````` + + + +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** — **wall time: 1.08s** + +````` +(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.* + +**wall time: 3.62s** + +````` +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":3015,"graph_unresolved":15989,"graph_unindexed":275,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1184","sites_at":["src/mcpindex.h:1288"]}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:12032","sites_at":["src/graph.h:12035"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:12678","sites_at":["src/graph.h:12729"]}, +{"t":"fn","n":"runEval","p":"src/eval.h:171","sites_at":["src/eval.h:328"]}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1427","sites_at":["src/main.cpp:1441"]}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1466","sites_at":["src/main.cpp:1480","src/main.cpp:1481","src/main.cpp:1495"]}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1753","sites_at":["src/main.cpp:1873"]}]} +````` + +## `./build/ripwire . --hotspots --json` + +*JSON refusal shape: an unsupported verb refuses loudly instead of silently falling back to XML.* + +**exit code: 1** — **wall time: 1.06s** + +````` +(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, --defs, and --plan-lanes which is JSON-native (e.g. ripwire