diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index ee309c7a5..3ba12823b 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -408,6 +408,7 @@ ack complexity 1e9c684ddd70ed1f 27 cid=f3502fc85213b238 by=src/* lane F (F-05/F- ack complexity 205bde7df6b15388 26 cid=a790a90cc086ddef by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack complexity 20b8113908960e36 52 cid=8333a4060e6ba0f6 now=52 was=48 p=src/verbs_navigate.h:2095 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity 218041bccb277b92 76 cid=69bc85c464d51fa1 lane B1 cap disclosure: the census IS the +3. The raw-token window moved out of the while condition so the scan reaches the end of the task text and counts every qualifying token, which is what makes mention_tokens_total= an exact number instead of a bare 'something was dropped'. The kept set is unchanged and the scan is bounded by one sentence of task text. +ack complexity 21969a79132df738 89 cid=3aba7ead4fee5208 now=89 was=85 p=src/lsp.h:943 task C runLsp: the textDocument/definition arm destructures the new LspDefinitionAnswer and writes a non-empty d.notice ahead of the answer (arm 19) — that notice branch is the +4cx; the concat-to-frame shape argument belongs to the duplication rows at p=src/lsp.h:147 (the two notice builders), not to the dispatcher ack complexity 231d5965c5d8503b 52 cid=0ecc0b97fecbcc72 P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). ack complexity 240fa4dc89f6442d 23 cid=ca5968a9693b8433 churn=self on the three symbols that ARE the ack-ledger format. P1.4 adds one optional named token (by=) to that format, so these are exactly the sites a format change must touch. ack complexity 2434bf5574dc575b 16 cid=e3c081715b00762c now=16 was=0 p=src/quality.h:5813 lane/ack-provenance-backfill: the legacy-ack backfill (clone kinds reconstructed from the current tree, marked prov=recon), its refresh/unverified freshness rule, and qackconcurrencycheck arm 9 — plus this lane's own minor footprint on the ack reader/writer it extends @@ -482,6 +483,7 @@ ack complexity 96fdcdff2f0ff0f7 28 cid=95538ee0cd9b8cae round ec5e3c3..HEAD, lan ack complexity 97a23cbeae7feb30 39 wave merge 2026-08-20 (harvestexec): collectFacts +12 LOC / +2 cx, from lane/resolver-precision. The executable part is ONE guard — 'if role == RefRole::Type: continue' — and 8 of the 12 added lines are the comment that makes it honest (the rest are the if, its braces and the continue; 'the other eleven lines' was this ack's own overstatement, corrected 2026-08-20), because a silent skip here is indistinguishable from an oversight. Two reasons are recorded in it: a member declaration 'Shared m_a;' already contributes a site through its HAS-A compose reference over the SAME bytes, so admitting the type mention too would count one thing a reader must read twice; and the coupling/cohesion thresholds this lens feeds were calibrated over the pre-Type reference stream (docs/EVALS.md section 9), so widening its input from a lane registered to change the USE-SITE index would invalidate a calibrated instrument as a side effect. That makes the growth a scoping decision with its own follow-up round, not debt: deleting the comment would shrink the number and destroy the only record of why the lens holds still. ack complexity 97e844e27c205f75 73 cid=2ba2aaf4fee7426c M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. ack complexity 983814f2b5912a90 17 cid=1ea6676dedf249db at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack +ack complexity 9c7ec267aa320b9f 25 cid=d850116150e347ac now=25 was=24 p=src/lsp.h:863 task C lspHover: the +1cx is the new if( defs.size() > 1 ) branch — it appends the one N-definitions sentence to the hover markdown beside the def block it qualifies (arm 19); extracting that sentence would couple hover's disclosure to the notice senders' concat-to-frame shape, which is the argument the duplication rows at p=src/lsp.h:147 carry (lspWorkspaceCapNotice/lspMultiDefNotice build frames, lspHover builds none) ack complexity 9ebbafddddd086b4 97 cid=5538119bcafa2857 now=97 was=95 p=src/mcpverbs.h:3460 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: F5 sibling: packConnect grows by the over_ceiling decision and its re-pricing — three statements beside the est_tokens it must agree with. Splitting them out would separate the label from the number it is about, which is the defect being fixed. ack complexity a3a317502ee383bc 18 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack complexity a4a8c133e40fe0ac 28 cid=0dfc15f8d6edef3d H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. @@ -679,7 +681,9 @@ ack duplication 8d47d68ea8798deb 29 now=29 was=0 p=src/prcontext.h:499 PR 423 hu ack duplication 8fff3a3ea6d42578 75 by=src/verbs_report.h now=75 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/verbs_report.h): extentSuspectCountsByFile and recall.h docFileMask are the one-pass-over-ing.symbols per-fileId tally idiom (lintrules.h dependencyCapableMask is a third), with different element types and questions: a uint32 count of flagged definitions per file vs a char mask of files holding Markdown symbols. Same class as ack 689e8ff37b877fd0: a filter-and-tally template parameterised by a lambda would read worse than either five-line loop. ack duplication 91876bb9e456fd42 30 now=30 was=0 prov=recon wave-3 close: re-grouping artifact — hasAnyExt/isValidSeverity/isMcpProtocolVersionSupported/isDecisionShaped/hasNode did not change; their clone group lost the isShellBuiltinName member and the remaining pre-existing pairs re-emerge as new pairs vs HEAD ack duplication 9497791b092d0735 19 now=19 was=0 prov=recon M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. +ack duplication 956e5716c55ebb17 31 now=31 was=0 p=src/lsp.h:147 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack duplication 96ccf33fadeecaee 18 now=18 was=0 prov=recon incidental shape similarity of small string/kind predicates across unrelated domains; sharing a helper would couple them (#235 Java kind predicates vs flipimpact/ingest_metrics) +ack duplication 975b7085738b5997 31 now=31 was=0 p=src/lsp.h:147 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack duplication 978255b9ac3b2028 37 by=src/* now=37 was=0 prov=recon member-variable round (card A3), side-table rule: isMemberAccessSite is the ONE definition of the member-access predicate contextratio.h / nonlocalstate.h / graph.h share; the 37-token pairs with editCheckImplicitReceiver and eligibleForJoin are the two-conjunct-bool-predicate SHAPE, not shared logic ack duplication 979a4b8f7a8177ba 20 the three sidecar read seams keep one O_NOFOLLOW shape by design, each with its own once-per-site DISCLOSE (pathguard.h round 3) ack duplication 9a01b02dda7e280e 28 now=28 was=0 prov=recon 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit @@ -746,6 +750,7 @@ ack duplication ce3ae91a3d24ac8c 64 now=64 was=0 prov=recon timsort vendoring: e ack duplication cf6c83caaec68565 19 now=19 was=0 prov=recon taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff ack duplication d00e8847a6532b4d 18 now=18 was=0 p=test/receiverevidencefix/superroot/cs/App/Panel.cs:13 lane call-result-typing (train 29) arm (L) fixtures: tiny call-then-member-call fixture functions (18-23 tokens) that share a shape with older receiver-evidence fixtures by design — each pairs a local with a same-named method on two classes; no shared logic to extract (test fixtures are never built) ack duplication d02609215b4dde37 338 P3.2 plan-lint mechanical footprint: printUsage grows with the new help block; glyphName clones accessshape::shapeName (an enum-to-string switch idiom, the exact false-positive class P2.1's clone-idiom demotion targets, not yet landed); gitBlameLineSha clones quality.h::gitBlameRangeHasWindowCommit's blame-porcelain invocation shape — a shared-invocation extraction was tried and reverted as net-worse (it only moved the cost onto that untouched function's own churn), see planlint.h's own comment on the decision +ack duplication d02fc7c439904e54 41 now=41 was=0 p=src/lsp.h:147 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack duplication d1754de2cfeae4a5 27 now=27 was=0 prov=recon facet=threshold-ladder lane fix/cpp-macro-member-reparse-2026-09-11: test/extentfix/leak.cpp and test/extentfix/control/leak.cpp are the extent detector's designed fixture PAIR (the control is the leak with semicolons added), so their free functions are byte-identical by construction; the pair only re-keyed because the leak's macro moved to lowercase (the member-macro re-parse repairs the ALL-CAPS spelling, so the detector needs a shape that still derails) and the leak copies are filed under IndiaError:: ack duplication d2187166e5746ef0 23 now=23 was=0 p=test/kotlinfix/JavaBridge.java:13 lane call-result-typing (train 29) arm (L) fixtures: tiny call-then-member-call fixture functions (18-23 tokens) that share a shape with older receiver-evidence fixtures by design — each pairs a local with a same-named method on two classes; no shared logic to extract (test fixtures are never built) ack duplication d343b392d3ab9ecb 114 now=114 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -1597,6 +1602,7 @@ ack verbosity 1e9c684ddd70ed1f 80 cid=f3502fc85213b238 by=src/* lane F (F-05/F-0 ack verbosity 20b8113908960e36 98 cid=c93e456ce00ceaec capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 20e7cca8cfd03e6c 66 cid=6d56fab80a7c07a2 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack verbosity 218041bccb277b92 104 cid=69bc85c464d51fa1 lane B1 cap disclosure: the census IS the +3. The raw-token window moved out of the while condition so the scan reaches the end of the task text and counts every qualifying token, which is what makes mention_tokens_total= an exact number instead of a bare 'something was dropped'. The kept set is unchanged and the scan is bounded by one sentence of task text. +ack verbosity 21969a79132df738 135 cid=3aba7ead4fee5208 now=135 was=133 p=src/lsp.h:943 task C runLsp: the textDocument/definition arm destructures the new LspDefinitionAnswer and writes a non-empty d.notice ahead of the answer (arm 19) — those two statements are the +2 LOC; the concat-to-frame shape argument belongs to the duplication rows at p=src/lsp.h:147 (the two notice builders), not to the dispatcher ack verbosity 225d0a144176c808 115 cid=fdf6f7041c20fcbe OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack verbosity 24637f7c69207cb9 1135 cid=b7513601dadd2b3e t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign ack verbosity 28449092af4cecb7 74 cid=ca3960652d2d85b9 by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). diff --git a/changelog.d/432-name-tower.md b/changelog.d/432-name-tower.md new file mode 100644 index 000000000..f4740a6aa --- /dev/null +++ b/changelog.d/432-name-tower.md @@ -0,0 +1,6 @@ +### Changed — one order for every multi-definition name answer, and `--lsp` says when a name has several definitions (#432, thanks @mpapis) + +- When a bare name has several definitions, every verb now orders them the same way: the most specific kind first (a method before a variable before a doc heading or config key; a constructor ranks just below its own class), then the leading definition's language, then crawl order. Single-pick verbs (`--around`, `--lego`, `--connect`'s preferred definition, `--impact`'s focus, `--owners`, `--edit-check`) answer at the front of that order; union verbs (`--callers`, `--uses`, `--impact`'s `of_def=` rows, `--lsp` definition/workspace-symbol lists) keep every definition and only reorder it. Train 33's code-first rule and the C/C++ declaration-to-body rule still apply on top. `defs=` still says how many definitions the name has, so a pick among several stays disclosed (thanks @mpapis, #432). +- `--lego` reads the order by role: it asks for a type, so a class, struct, interface or named type all share one rung. On that rung a type with at least one implementor comes first, then a type whose method contract is read (`` rows, no `not-extracted-for-lang` caveat; today that is C++ and Objective-C, the languages whose `` rows are read), then the order above. So `--lego=Shape` answers the C++ `Shape` with its contract, not an Elixir module that has none, and a Kotlin `interface` (tagged as a class) that has an implementor is not passed over for a same-named Java one that has none (diagnosis @mpapis, #432). +- `--lsp` definition sends one `window/logMessage` before the list, and hover adds one sentence, when the name has more than one definition. This is a count of same-named definitions, prototypes included. It does not mean the call graph split a call between them. +- The map is unchanged: the order applies at query time only, so call-edge candidates, `kParserVer` and caches do not move. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a7aa2b8c7..b1a80ce75 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -747,9 +747,11 @@ definitions and says how it got there. Input spellings: a bare name, `file:name` the canonical id the tool prints, `@FILE:LINE`, a `*` glob, and the changed symbols of a diff. All of them go through `resolveAllByNameQualified` (`graph.h`) or one of its tiers. The diff-hunk producer (`diffhunks.h`, `situ.h`) maps hunks to the symbols whose bodies they touch, and falls back to the whole file when no body holds a hunk -(contributed by @llvm-x86 in #423). Matches come back in index order (ascending id); a verb that needs ONE -focus takes it from `resolveFocus` (`graph.h`): a code definition before a docs heading or a config key, the lowest id -among those, and a bodyless C/C++ declaration yields to the same-scope definition with a body. A seed resolver never +(contributed by @llvm-x86 in #423). Several definitions with one name are ordered by one total order, the name tower +(`applyNameTower`, `kindSpecificity` in `model.h`; contributed by @mpapis in #432). A verb that needs ONE focus takes +it from `resolveFocus` (`graph.h`; `--lego` and the MCP `lego` verb take theirs from `resolveLegoFocus`: the +role order, then `focusOfOrderedMatches`): the tower front, a code definition before a docs heading or a config +key, and a bodyless C/C++ declaration yields to the same-scope definition with a body. A seed resolver never picks silently. `defs=`, `unproven_defs=`, `from_defs=`/`to_defs=` and the `` table disclose the candidates. The rule: **any spelling the output prints is a spelling the input accepts.** @@ -782,7 +784,7 @@ cut something is printed (`capped=`, `total=`, `next=`). It is never reported as **One coupling, stated on purpose.** When a name has several definitions, the *pick* can depend on the walk: the right `Loop` is the one that reaches the other endpoint. The seed layer therefore returns the ordered candidates. A walk that must choose uses the one shared terminal scorer (`scoreTerminalCandidates`, `graph.h`), -with `resolveFocus`'s order as the tie-break, and it prints the alternatives it did not take. +with the tower as the tie-break, and it prints the alternatives it did not take. **Adding to a layer.** - A new spelling goes in the seed resolver. The verbs pick it up from there. diff --git a/src/compactlegend.h b/src/compactlegend.h index 527f47895..f7f156c05 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -120,14 +120,14 @@ inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { "edit-check", "edit-check", "sym='s contract NOW vs HEAD: status=unchanged|new-symbol|contract-change; callers" }, // safe-delete (2026-09-12, the fourth sweep): radius_tested=/radius_untested= ride every answer and read here, by the same // isTestedByReach lens as --impact's pair (verbs_navigate.h runSafeDelete), over impact_reaches= instead of reaches=. The - // sweep's last pass read the rest of that root against runSafeDelete: t= and p= are defs[0]'s, the lowest id + // sweep's last pass read the rest of that root against runSafeDelete: t= and p= are defs[0]'s, the tower-front match // resolveAllByNameQualified returns. That is a MATCH, not always a definition (the sweep's design review): on a // header-qualified file:name selector the decl→def widening keeps the declarations beside the definitions it adds - // (graph.h declToDefFollowThrough), and a declaration can hold the lowest id, so the reading names the unit defs= counts. + // (graph.h declToDefFollowThrough), and a declaration can hold the front, so the reading names the unit defs= counts. // ambiguous_callers= counts callers whose g.ambOut is non-zero; dead_code_candidate=1 needs defs=1, no 1-hop caller, // deadCodeEligibleKind (SymKind::Function with a body, not in a .h/.hpp/.hh/.hxx path) and a whole-word `static` inside the // signature span (quality.h sourceHasStaticToken). - { "safe-delete", "safe-delete", "can sym= go, a READ never a verdict: callers= impact_reaches= uses= tested_self= risk=; ; radius_tested= non-tests of impact_reaches= an indexed test reaches, radius_untested= the rest; t= p= its lowest-id match's kind and file:line; defs= matched; ambiguous_callers= callers with a call split over several defs; dead_code_candidate=1 only at defs=1 with no caller, for a t=fn with a body, outside .h/.hpp/.hh/.hxx, whose signature spells static (0 never means in use)" }, + { "safe-delete", "safe-delete", "can sym= go, a READ never a verdict: callers= impact_reaches= uses= tested_self= risk=; ; radius_tested= non-tests of impact_reaches= an indexed test reaches, radius_untested= the rest; t= p= its tower-front match's kind and file:line; defs= matched; ambiguous_callers= callers with a call split over several defs; dead_code_candidate=1 only at defs=1 with no caller, for a t=fn with a body, outside .h/.hpp/.hh/.hxx, whose signature spells static (0 never means in use)" }, { "quality-delta","quality-delta","only what the change made WORSE vs baseline=: regressions= minor= gating=; , acked" }, { "test-gate", "test-gate", "tests + untested blast radius ; exit 4 while either exists" }, { "affected", "affected", "test files that transitively reach the changed files/symbols: in evidence order (order= partners=); seeded_by= the reading taken" }, @@ -466,7 +466,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // defs= names resolveFocus's pick (graph.h), and the body preference there holds only IN THE DECLARATION'S SCOPE: a pure // virtual beside another class's override keeps the focus. Unconditional, this reading described that answer wrongly // (test/decltodefcheck.sh E3g, which also holds the full --around and --connect readings to the same condition). - { "defs", "defs=N: of= names N defs; lowest-id walked, code first, a C/C++ body in the same scope over its declaration", true, "r" }, + { "defs", "defs=N: of= names N defs; most-specific kind walked first, its language ahead, id last, code first, a C/C++ body in the same scope over its declaration", true, "r" }, // The map HEADER's absent-at-zero gauges: `" ); // 0.6.6: the CLI --owners qualification, on the same element in the same slot (before at=). @@ -3975,7 +3977,7 @@ inline constexpr char kConnectHeader[] = " graph_ambiguous=/graph_unresolved= are the whole graph's resolver gauge (calls split over several defs / calls" " whose in-repo defs were all language-filtered), the map header's ambiguous=/unresolved=." " defs= on a terminal row = that NAME has N definitions; every one is scored and the one reaching the most other" - " terminals within radius= (fewest hops) is used, else the lowest id (a C/C++ declaration without a body yields to the" + " terminals within radius= (fewest hops) is used, else the tower front (most specific kind, leading language, lowest id; a C/C++ declaration without a body yields to the" " definition of its scope); ambiguous_terminal= on the root names terminals whose pick tied with another equally-joining" " definition. Qualify with file:name to pick another. Steiner rows never carry it." " connects= on a Steiner row = how many DISTINCT symbols that intermediary joins (its callers plus its" @@ -5002,12 +5004,12 @@ inline EditCheckReply editCheckText( const std::string& root, const std::string& return EditCheckReply{ {}, "new_body " + std::string( mcpedit::kBinaryPayloadRefusal ) }; } const rw::editpreview::Outcome preview = - rw::editpreview::run( ing, g, root, kDefaultMaxFileBytes, {}, true, symbol, groups[0].lowestNode, newBody, nullptr, + rw::editpreview::run( ing, g, root, kDefaultMaxFileBytes, {}, true, symbol, groups[0].focusNode, newBody, nullptr, pg.limit, pg.offset ); return preview.ok ? EditCheckReply{ preview.xml, {} } : EditCheckReply{ {}, preview.message }; } - return EditCheckReply{ editCheckBundleText( ing, g, root, kDefaultMaxFileBytes, {}, groups[0].lowestNode, + return EditCheckReply{ editCheckBundleText( ing, g, root, kDefaultMaxFileBytes, {}, groups[0].focusNode, /*ni=*/nullptr, /*preview=*/false, // lane lean2b (card #9): the CLI's verdict-only fold — a clean contract at HEAD with nothing // flagged answers the verdict + rows_next= (the MCP call, limit=N, that lists the callers), callers= kept @@ -5348,7 +5350,7 @@ inline FetchOutcome fetchBody( const std::string& root, const std::string& handl RedactCounts* redact = nullptr ); // R2c (the 2026-08-12 usage mine): serve fetch_body for a bare symbol NAME through the SAME lookup path -// find_symbol uses (resolveAllByNameQualified → the lowest-id pick; resolveFocus's C/C++ body preference is NOT applied), then +// find_symbol uses (resolveAllByNameQualified → the tower-front pick; resolveFocus's C/C++ body preference is NOT applied), then // RECURSE into fetchBody with the freshly-minted real handle — every staleness/overload guarantee applies // unchanged, and the result teaches the handle for next time. Disclosed: resolved_from_name always; // name_defs/other_defs (file:line + handle, capped) when the name has several DISTINCT defs — the honest diff --git a/src/model.h b/src/model.h index 36817ea3b..f8e6fba5c 100644 --- a/src/model.h +++ b/src/model.h @@ -122,6 +122,53 @@ inline bool isStructOrNamedType( SymKind k ) noexcept return k == SymKind::Struct || k >= SymKind::NamedType; } +// The specificity ladder, hoisted out of dedupRawDefs (ingest_model.h) by RIPWIRE-RUBY task C so the +// resolver's tower (graph.h's name matchers) and the def dedup order the SAME way from ONE table — higher = +// more specific / preferred when two matches collide on one name byte. A table, not a switch (§3's +// table-over-switch rule; --quality-delta scored the hoisted switch as a clone of the SymKind readers). +// Values are load-bearing: the dedup's total order and the picker's Method > Var > Section answer sequence +// are byte-pinned (rubyattrs/rubyschema checks, lspcheck 18). Index order is the SymKind enumerator order +// above; the size assert pins the length, so appending a kind without arming it is a build error, not a +// silent 0. The three keyed asserts below pin the enumerator ORDER at three rungs — Method at 1, Section at 6, FuncType at +// 13, each together with its table weight — so moving the top rung, the floor or the tail without its row is a build error. +// They do not pin the other eleven: a swap of two unanchored enumerators (Var with Interface) leaves every guard true while +// the position-indexed table reweights them, which is what the byte-pinned answers above are for. DISTINCT from gitmine.h's +// §H6 "specificity-ordered mapper", which orders file PATHS on a different axis; nothing there reads or shares this ladder. +inline constexpr std::array kKindSpecificityTable = { + /* Function = */ 2, // a bare function definition + /* Method = */ 5, // most specific: a behaviour owned by a type + /* Class = */ 3, + /* Struct = */ 3, + /* Interface = */ 4, + /* Var = */ 1, + /* Section = */ 0, // doc headings, JSON/YAML keys, schema columns: never beat a code kind on one name byte + /* Macro = */ 0, // a #define comes from its own single capture, same value as Section/Other + /* Field = */ 1, // never collides with Var in practice: the static/non-static gates are complementary + /* Other = */ 0, + /* ModuleScope = */ 0, // minted after the dedup pass runs; it can never reach it + /* NamedType = */ 1, // Go's type_spec fires the generic definedtype AND functype on the same name byte for + /* Alias = */ 1, // `type F func(...)`: the form-specific kind must win — FuncType below — and an alias is + // its own node (type_alias) that never collides + /* FuncType = */ 2, +}; +static_assert( kKindSpecificityTable.size() == kSymKindCount, + "kKindSpecificityTable: one weight per SymKind, in enum order" ); +// The size assert arms an APPENDED kind; these arm a REORDERED enumerator, which no length can see. House pattern +// (serialize.h pins int(Lang::Unknown) == 12; the NamedType..FuncType assert above pins a range): ordinal and rung +// are asserted TOGETHER, keyed by enumerator. Three anchors — the top of the ladder, its floor, its tail — so a reorder +// that moves one of THOSE without its table row trips one of them; a permutation confined to the eleven unanchored +// enumerators trips none, and arming one of those is a fourth keyed assert, not a stronger claim about these three. +static_assert( unsigned( SymKind::Method ) == 1 && kKindSpecificityTable[ unsigned( SymKind::Method ) ] == 5, + "SymKind::Method is enumerator 1 with weight 5 — the tower's top rung; move the enumerator and the table row together" ); +static_assert( unsigned( SymKind::Section ) == 6 && kKindSpecificityTable[ unsigned( SymKind::Section ) ] == 0, + "SymKind::Section is enumerator 6 with weight 0 — headings, keys and schema columns never beat a code kind on one name byte" ); +static_assert( unsigned( SymKind::FuncType ) == 13 && kKindSpecificityTable[ unsigned( SymKind::FuncType ) ] == 2, + "SymKind::FuncType is enumerator 13 (the last) with weight 2 — the Go func-type rung over NamedType/Alias" ); +inline int kindSpecificity( SymKind k ) noexcept +{ + return enumTableAt( kKindSpecificityTable, k, 0 ); // a byte past the enum (a corrupt cache byte is VALIDATEd on read) +} + // The last segment of a scope (Symbol::scope) as written ("outer::Scanner" or "pkg.Outer" -> the innermost owner): the spelling the selector // resolves as Owner.name (graph.h scopeSuffixMatches). inline std::string_view scopeLastSegment( std::string_view sc ) noexcept diff --git a/src/serialize.h b/src/serialize.h index 79140ab53..ee42c64e6 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -3148,9 +3148,9 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto { legend += ""; legend += seedBiteLegend( ann.seed ); // C2: charged only on a run whose root carries a bite attribute (the at= rule) // H1: charged only on a run whose root carries unproven_defs= — the same rule, and part of the head est_tokens prices. @@ -9166,6 +9166,82 @@ inline const char* legoContractCaveat( bool isContractExtracted, bool isTargeted return ( isTargeted && !isContractExtracted ) ? "\" methods=\"0\" caveat=\"not-extracted-for-lang" : ""; } +// --lego's ROLE order over the tower (graph.h applyNameTower), view-local: the answer to "--lego=NAME" is a TYPE with +// something to show, so every type kind (Class/Struct/Interface/named type) shares ONE rung, and on it the keys are, +// in order: at least one implementor (the rows); a method contract that is extracted (legoMethodContractSound — +// no not-extracted caveat, its rows are read; today a C++ / Objective-C definition, decided by the language alone, +// so it must NOT outrank an implementor: a data-only C++ class would beat a Rust trait that has implementors, +// rv-pr432-delta B1); then the tower's own key — kind rank (an Interface above a +// Class), the leading symbol's language, id ASC. Every other rung keeps the tower rank (a Method above the types, a +// constructor just below them). Measured (diagnosis @mpapis, #432): `--lego=Shape` on this repository answered an +// Elixir `shape.ex` (Interface, methods="0" caveat) over the C++ `Shape` whose contract is extracted +// (a9disclosurecheck §A9.4); `--lego=Marker` answered the Java `interface Marker` (no implementor) over the Kotlin +// one the grammar tags as a class, which `Tagged` implements (kotlincheck §13). A Rust trait still leads a same-named +// struct (nushell FromValue, PluginCommand): its implementors keep it ahead, and so does the kind rank. +// Where every candidate has implementors (§A9.4 Shape: C++ 4, Elixir 2, Rust 2) the extracted contract settles it. +inline std::vector legoRoleOrder( const IngestResult& ing, const std::vector>& implementors, std::vector ids ) +{ + if( ids.size() < 2 ) + { + return ids; + } + struct RoleKey + { + int rung; + bool hasImplementor; + bool isContractExtracted; + int towerRank; + NodeId id; + }; + const std::vector towerRanks = nameTowerRanks( ing, ids ); + const int typeRung = 2 * kindSpecificity( SymKind::Interface ); + std::vector keys; + keys.reserve( ids.size() ); + for( std::size_t i = 0; i < ids.size(); ++i ) + { + const NodeId id = ids[ i ]; + const Symbol& s = ing.symbols[ id ]; + const bool isType = namespaceCompatible( RefRole::Type, s.kind ); // graph.h's one statement of "a type definition" + keys.push_back( RoleKey{ isType ? typeRung : towerRanks[ i ], isType && id < implementors.size() && !implementors[ id ].empty(), + isType && legoMethodContractSound( s.lang ), towerRanks[ i ], id } ); + } + const auto ahead = []( const RoleKey& a, const RoleKey& b ) noexcept + { + return std::tuple( a.rung, a.hasImplementor, a.isContractExtracted, a.towerRank ) + > std::tuple( b.rung, b.hasImplementor, b.isContractExtracted, b.towerRank ); + }; + const RoleKey reference = *std::min_element( keys.begin(), keys.end(), [ & ]( const RoleKey& a, const RoleKey& b ) + { return ahead( a, b ) || ( !ahead( b, a ) && a.id < b.id ); } ); + const Lang referenceLang = ing.symbols[ reference.id ].lang; + std::sort( keys.begin(), keys.end(), [ & ]( const RoleKey& a, const RoleKey& b ) noexcept + { + if( ahead( a, b ) || ahead( b, a ) ) + { + return ahead( a, b ); + } + const bool refA = ing.symbols[ a.id ].lang == referenceLang, refB = ing.symbols[ b.id ].lang == referenceLang; + if( refA != refB ) + { + return refA; + } + return a.id < b.id; + } ); + for( std::size_t i = 0; i < ids.size(); ++i ) + { + ids[ i ] = keys[ i ].id; + } + return ids; +} + +// --lego's single pick (CLI --lego=TYPE and the MCP lego verb): resolveFocus's pick (graph.h focusOfOrderedMatches: +// code first, a bodyless C/C++ front yields to its same-scope body) over legoRoleOrder instead of the bare tower. +inline NodeId resolveLegoFocus( const IngestResult& ing, const std::vector>& implementors, std::string_view spec, + std::size_t* unprovenDefCountOut ) +{ + std::vector matches = resolveAllByNameQualified( ing, spec, unprovenDefCountOut ); // the tower order, every tier + return focusOfOrderedMatches( ing, legoRoleOrder( ing, implementors, std::move( matches ) ) ); +} + // §P3 SCOPE (bundle embeddings only). packLego's ranked mode treats "has implementors" as "is an interface", // so scoping THAT map is the filter: this returns a view of `implementors` in which every interface the task // did not reach has an EMPTY implementor list — packLego then ranks and emits only what is left, and emits no diff --git a/src/verbs_for.h b/src/verbs_for.h index f26c0ba0e..8a9d54f2a 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -4247,7 +4247,7 @@ std::optional runTargetedViews( const MainDispatch& d ) // not tie to the file it named. implementors= is read off the one node picked here, so unreported, a drop reached // the reader as implementors="0" about a forward declaration. std::size_t legoUnprovenDefs = 0; - const NodeId focus = resolveFocus( ing, cfg.legoType, &legoUnprovenDefs ); + const NodeId focus = resolveLegoFocus( ing, g.implementors, cfg.legoType, &legoUnprovenDefs ); if( focus == kNoNode ) { // §B4.2: one shared refusal — a non-defining `file:name` says WHICH files define the type and diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 47258a20f..3aae43c00 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -1463,9 +1463,9 @@ std::optional runSafeDelete( const MainDispatch& d ) { rw::emitTo( stdout, "", rw::declinedLiteralCallsLegend( true ) ); } - const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified walks ascending id — defs[0] is the - // lowest, same convention --impact/--uses/--callers's of=/defs= - // pairing relies on for orientation when defs > 1. + const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified's name tier arrives tower-ordered — defs[0] is the + // tower front (kind, leading language, lowest id), the same orientation + // convention --impact/--uses/--callers's of=/defs= pairing relies on when defs > 1. const PageWindow cw = pageWindow( callerIds.size(), effectiveRowCap( cfg.pageLimit, 40 ), cfg.pageOffset ); ASSUME( cw.begin == sdLw.begin && cw.end == sdLw.end, "pageWindow is pure: the rows' window is the legend's (sdSites is indexed by it)" ); char cab[ kPageDisclosureCap ]; diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 0976e7ec0..0e7c90e6d 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -2987,13 +2987,13 @@ std::optional runQualityViews( const MainDispatch& d ) // grammar --for/--expand already use (editCheckBundleText's `ni` parameter defaults to nullptr, so the MCP // call site is untouched and stays notes-inert for now). // -// §A6a — an AMBIGUOUS bare name is REFUSED, not silently narrowed. resolveFocus() answers "the lowest-id +// §A6a — an AMBIGUOUS bare name is REFUSED, not silently narrowed. resolveFocus() answers "the tower-front // definition with this name", which is the right answer for --around's ego graph and the WRONG one here: this // verb's whole value is "did I break a contract?", and answered about a definition the agent never edited it // returns status="unchanged" — reassurance for the wrong symbol. Its siblings can union (--callers reports // defs="3" and lists the union); a CONTRACT cannot be unioned, so the only honest answers are one definition // or a refusal that says how to name one. The resolver moves to resolveAllByNameQualified (every match, not -// the lowest id) — byte-identical on any selector that matched exactly one definition site, and it accepts a +// one tower pick) — byte-identical on any selector that matched exactly one definition site, and it accepts a // canonical id too, which is the spelling the refusal has to offer when a file alone cannot separate two // scopes in one file. std::optional runEditCheck( const MainDispatch& d ) @@ -3031,7 +3031,7 @@ std::optional runEditCheck( const MainDispatch& d ) editCheckAmbiguousMessage( cfg.editCheckSym, groups, "--edit-check=", matches.size() ).c_str() ); return 1; } - const NodeId focus = groups[0].lowestNode; + const NodeId focus = groups[0].focusNode; // card A1 — the PRE-APPLY fork. Everything above (resolution, the not-found refusal, the §A6a ambiguity // refusal) is shared verbatim, because a preview that resolved its target differently from the post-hoc diff --git a/src/verbs_report.h b/src/verbs_report.h index 65eaf90ce..9ce2a2917 100644 --- a/src/verbs_report.h +++ b/src/verbs_report.h @@ -1767,8 +1767,8 @@ std::optional runMaintenanceViews( const MainDispatch& d ) return 1; } // §B11.3's SHAPE, found by that item's sweep and closed here: this is a fold that reports a scalar. - // `defs[0]` is ONE of N definitions of the name — the lowest node id — and the report then covers - // that definition's file alone under files="1", which reads as "this symbol lives in one file" + // `defs[0]` is ONE of N definitions of the name — the tower front (kind, leading language, then the lowest id) — and + // the report then covers that definition's file alone under files="1", which reads as "this symbol lives in one file" // while --callers/--uses/--impact/--mentions on the same name all disclose defs="3". The pick is a // reasonable default (it matches --around/--lego's resolveFocus), but it was invisible. onlyFileId = ing.symbols[ defs[0] ].fileId; @@ -1837,7 +1837,7 @@ std::optional runMaintenanceViews( const MainDispatch& d ) "files= means two different things by DEPTH here and is deliberately not renamed: on the ROOT it is how " "many files were ANALYSED; on the fold it is how many of them collapsed into that one row. " "With a SYM, of= echoes it and defs= is how many DEFINITIONS that name has: this report covers the file " - "holding the FIRST of them (lowest node id), so defs= above 1 means " + "holding the TOWER FRONT of them (most specific kind, leading language, then lowest node id), so defs= above 1 means " "the other definitions' files were NOT analysed. Qualify with file:name to choose one. An @FILE:LINE " "seed rebinds to the innermost definition enclosing that line (sym= names it) and covers exactly that " "definition's file -->{}{}{}{}", diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 059a60e0e..0023b5f11 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -642,6 +642,23 @@ probeFor() # A name with several definitions now serves lean (no ride-along map), so the whole-file answer this probe reaches carries # topk_default="0" (+112 B reading) and map_next= (+56 B reading) on its root, both present-only (compactlegend.h). The # (U-twin-expand) arm below keeps the OLD 360 B pin for everything else in that legend. +# RE-PINNED 2026-10-21 (this review-fix round, but the BYTES are the reviewed commit's): ripwire.around/v1 770 -> 830 +# (measured 812, the --around=distance probe; the table held 760). e5d39f70 "say tower front where the code now means it" +# rewrote the compact defs= READING from "lowest-id walked, code first, a C/C++ body in the same scope over its declaration" +# (107 B) to "most-specific kind walked first, its language ahead, id last, code first, a C/C++ body in the same scope over its +# declaration" (151 B, +44 B) and left this row where it was — splice that pre-e5d39f70 term back into THIS build's answer and +# the same probe measures 768 B, inside the old 770, which localises the whole breach to that one reading. The (U) arm was +# therefore already red on the branch before this round. This round's own half of the same fix — the FULL --around seed clause, +# re-worded to the tower-front pick — costs (U) nothing: the compact dialect drops that clause, so no full-legend phrase is +# billed here. No (U-twin): a twin holds an OLD PATH that still exists, and here the same probe in the same dialect simply +# speaks the pick correctly, so the row is the whole story. +# RE-PINNED 2026-10-21 (this review-fix round, the BYTES are the reviewed branch's): ripwire.safe-delete/v1 measured +# 828 -> 830 (the --safe-delete=distance probe), pin 840 UNCHANGED. e5d39f70 re-worded that verb's own compact purpose +# clause (compactlegend.h:130, "t= p= its lowest-id match's kind and file:line" -> "its tower-front match's ...", 9 B -> +# 11 B, +2 B), a clause every compact --safe-delete answer emits, and left this row at 828. Nothing goes red: +# round-up-to-10(830) + 10 is still 840, and the (U-twin) cut holds its own reading — so this is the attribution the +# table's rule above asks for (re-pin in the commit that adds the bytes, with the bytes attributed there), the same one +# the around/v1 row carries. # RE-PINNED 2026-10-10 (lane lean-trio, idea #11): ripwire.edit-check/v1 610 -> 650 (measured 636, the --edit-check=distance # probe; 595 on main 0852bc0f). The probe's contract is unchanged with nothing flagged, so the answer is now the verdict # line (no rows), and the compact legend reads its two new facts: rows_next= (the --limit call listing the callers) and @@ -704,12 +721,12 @@ ripwire.grep/v1 1432 1420 ripwire.match/v1 370 355 ripwire.lego/v1 290 275 ripwire.exemplar/v1 440 422 -ripwire.around/v1 770 760 +ripwire.around/v1 830 812 ripwire.callers/v1 450 438 ripwire.callees/v1 500 484 ripwire.uses/v1 510 500 ripwire.batch/v1 250 238 -ripwire.safe-delete/v1 840 828 +ripwire.safe-delete/v1 840 830 ripwire.at/v1 180 161 ripwire.from-trace/v1 1300 1281 ripwire.plan-lint/v1 570 551 @@ -2017,9 +2034,10 @@ for v in "" "--query=distance" "--rank-by=hub" "--around=total_area" "--impact=d for spec in pr_iters pr_converged "#declined" "#external" "#locality_pinned" "#extent_suspect_syms" "#macro_blanked_files" \ "#ignored_files" "#ignored_dirs" "#max_tokens" "#fit_bytes" "#over_ceiling" r:defs r:rank_by r:window; do attr="${spec#*:}"; attr="${attr#\#}" - # Since the fourth sweep (2026-09-12) --impact's purpose line reads its OWN defs=, so the map root's defs= reading is - # found by its opener rather than its attribute name, the way (D17)/(D23) find theirs. - if [ "$spec" = r:defs ]; then isRead="$( ca mentions "$TMP/d12.c" "the lowest-id one was walked" )"; else isRead="$( ca defines "$TMP/d12.c" "$attr" )"; fi + # Since the fourth sweep (2026-09-12) --impact's purpose line reads its OWN defs=, so the map root's defs= reading is found by its opener rather + # than its attribute name, the way (D17)/(D23) find theirs. THE NEEDLE IS THE COMPACT TERM'S OPENER (compactlegend.h's defs row) — these documents + # are read under --legend=compact, where the full clause is never printed, so a full-legend phrase here would silently match nothing. + if [ "$spec" = r:defs ]; then isRead="$( ca mentions "$TMP/d12.c" "most-specific kind walked first" )"; else isRead="$( ca defines "$TMP/d12.c" "$attr" )"; fi if [ "$isRead" = 1 ] && [ "$( ca carries "$TMP/d12.c" "$spec" )" != 1 ]; then no "(D12) ${v:-the flagless map}: the compact legend defines $attr= but the document carries no $spec — a reading of a field that is not there" d12bad=1 diff --git a/test/decltodefcheck.sh b/test/decltodefcheck.sh index 8f4598853..787f5cbda 100755 --- a/test/decltodefcheck.sh +++ b/test/decltodefcheck.sh @@ -88,10 +88,11 @@ # written; a bare name and a refusal that dropped nothing print no such line. # (E3a..E3f) THE FOCUS PICK. --lego, --connect and --around take ONE node from a match set (resolveFocus), and it was the # lowest id — with the header sorting first, the DECLARATION, even where no definition was dropped: a bare name and a -# fully proven file:name answered the same zero as the dropping selector. A bodyless C/C++ lowest id now yields to the -# lowest-id bodied C/C++ match of the same scope: --around reaches the caller (E3a), --connect joins it and names the -# definition's file on its terminal row (E3b), --lego counts the implementor (E3c), and among several definitions the -# lowest id wins (E3d). Kept as they were: a set of declarations only (the dropping selector still carries its +# fully proven file:name answered the same zero as the dropping selector. A bodyless C/C++ front now yields to the +# tower-front bodied definition of the same scope: --around reaches the caller (E3a), --connect joins it and names +# the definition's file on its terminal row (E3b), --lego counts the implementor (E3c), and among several +# definitions the tower front wins (E3d). Kept as they were: a set of declarations only (the dropping +# selector still carries its # residue). A TypeScript overload signature beside its implementation (E3e) and a C++ pure virtual beside an override # of another class (E3f) keep resolveFocus's pick, but since 0.6.6 --connect searches from the definition that JOINS # the other terminal (graph.h joinTerminalPicks), so its terminal row there names the implementation / the override. @@ -1176,7 +1177,7 @@ for pair in "e2_us_cmp.xml:uses" "e2_mn_cmp.xml:mentions" "e2_vf_cmp.xml:verify" done # ── (E2n..E2t) THE SAME RESIDUE ON --edit-check, --lego, --connect, --around AND --slice ─────────────────────────────── -# Five more readers of the same resolver (--lego, --connect and --around through resolveFocus, its lowest-id projection) +# Five more readers of the same resolver (--lego, --connect and --around through resolveFocus, its tower-front projection) # answered the drop with a zero and nothing beside it. The one a reader acts on first is --edit-check's: incompatible="0" # about the declaration, while the definition the selector dropped carries the caller that no longer binds. # Same four questions as (E2e), through two readers that find a root wherever it sits and read its zero off whichever @@ -1736,7 +1737,7 @@ done e3Present "(E3c) kept: --lego=a_fwd.h:Shape drops the definition" "$TMP/e3_lg_drop.xml" lego 1 iface implementors 0 echo -echo "=== (E3d) among several definitions, the lowest id is chosen ===" +echo "=== (E3d) among several definitions, the tower front is chosen ===" run "$TMP/free" --connect=helper,unrelatedCaller >"$TMP/e3_cn_free.xml" e3Pick "(E3d) --connect=helper,unrelatedCaller over api.h, other.cpp, third.cpp" "$TMP/e3_cn_free.xml" helper other.cpp:2 1 @@ -1913,8 +1914,8 @@ printf ' block now carries p= on / (identity parity with # the standalone verb) and is SCOPED to the task's resolved surface. Assertion 2 below was updated: it @@ -227,6 +228,42 @@ else no "MCP lego verb: NoSuchType should error with a not-found message, got: $MCPNF" fi +# ── 15) rv-pr432-delta B1: implementors outrank "contract extracted" on --lego's type rung ─────────────────────── +# Kept in a throwaway corpus, not test/legofix (other gates pin that tree's bytes). A Rust `trait Render` with two +# implementors and a same-named, data-only C++ class (no methods, no implementors) in a vendored header: the +# extracted-contract key depends on the LANGUAGE only (C++/ObjC), so it must sort BELOW hasImplementor, or the +# empty C++ answer would beat the trait. Red before the key swap, green after. +XC="$TMP/xlangpick"; mkdir -p "$XC/src" "$XC/ext" +cat > "$XC/src/render.rs" <<'RS' +pub trait Render { + fn render(&self) -> String; +} +pub struct Button; +impl Render for Button { + fn render(&self) -> String { String::from("b") } +} +pub struct Label; +impl Render for Label { + fn render(&self) -> String { String::from("l") } +} +RS +cat > "$XC/ext/render.hpp" <<'HPP' +namespace vendor { +class Render { + public: + int frame; +}; +} +HPP +"$BIN" "$XC" --no-cache --lego=Render 2>/dev/null > "$TMP/lego_xpick" +XP="$( cat "$TMP/lego_xpick" )" +if printf '%s' "$XP" | grep -q ']*p="src/render.rs"[^>]*implementors="2"' \ + && printf '%s' "$XP" | grep -q '1 defs carries ONE sentence — N definitions, counts are +# floors, the resolver splits — in the hover markdown and in one window/logMessage ahead of every +# definition answer; absent on single-def names, whose hover must ANSWER before the silence is credited +# (20) Used-at parity (task C): hover Used-at for the colliding name answers every CLI --uses call row, with the +# kLspHoverUseCap=8 tier premise pinned first — past the cap a missing link is the disclosed truncation, not a defect +# (21) C/C++ decl/def no-regression (task C): on a tree whose HEADER sorts first — so the prototype carries the LOWER +# crawl-order id — the single-pick --around still walks the bodyful definition (the resolveFocus refinement +# composes with the tower sort); textDocument/definition is pinned beside it as the whole-list answer it is +# (22) determinism x2 on the new collision dialog (house pattern) +# (23) the known gap stays COHERENT: --around's CLI row shows the tower front AND its defs= count, so +# edge-vs-display disagreement is visible on the row, never hidden +# (24) a constructor never outranks its class (maintainer, #432 review): Java `public Shape() {}` inside class Shape +# and C++ `Widget::Widget()` beside a header-first class Widget — --lego names the class and its implementor, +# --around focuses the class row; a near-miss Java method named like ANOTHER class keeps the Method rung +# (25) a C prototype beside its body is counted in the (19) sentence but never described as a resolver split +# (26) the tower's tiers in ORDER: kind before language — a Python class (rung 3) sits between a Ruby method (5) +# and a Ruby attr (1) although the leading language is Ruby (arms 18-23 are single-language and cannot see a +# swap of the two tiers) # # Usage: RIPWIRE_BIN=build/ripwire bash test/lspcheck.sh | bash test/lspcheck.sh path/to/ripwire @@ -51,7 +76,7 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } -WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +WORK="$( cd "$( mktemp -d )" && pwd -P )"; trap 'rm -rf "$WORK"' EXIT mkdir -p "$WORK/src" # C++ fixture — line numbers below are load-bearing (the arms address them): @@ -341,5 +366,272 @@ noticeLine="$( grep -n '"method":"window/logMessage"' "$WORK/r17" | cut -d: -f1 && [ -n "$noticeLine" ] && [ "$noticeLine" -lt "$resp30Line" ] \ && ok "(17) workspace/symbol: 25 matches → 20 rows after one window/logMessage 'first 20 of 25'; 10 matches (capfn10-19) → 10 rows, no notice" \ || { no "(17) cap disclosure wrong: rows30=$rows30 rows31=$rows31 notices=$notices"; head -c 500 "$WORK/r17"; } +# ── (18) picker order: the tower sorts every multi-def answer, byte-pinned on the collision fixture ─── +cp -r "$ROOT/test/rubyschemafix" "$WORK/collide" +uriColl="file://$WORK/collide" +uriCA="file://$WORK/collide/consumer_ambiguous.rb" +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"$uriColl\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"$uriCA\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/all_four.rb\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":40,\"method\":\"workspace/symbol\",\"params\":{\"query\":\"name\"}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":41,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"$uriCA\"},\"position\":{\"line\":7,\"character\":10}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"$uriCA\"},\"position\":{\"line\":7,\"character\":10}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":43,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/all_four.rb\"},\"position\":{\"line\":9,\"character\":8}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d18" +"$BIN" --lsp < "$WORK/d18" > "$WORK/raw18" 2>/dev/null +decodeFrames "$WORK/raw18" > "$WORK/r18" 2>/dev/null +head40="$( grep '"id":40,' "$WORK/r18" )"; head41="$( grep '"id":41,' "$WORK/r18" )"; R42="$( grep '"id":42,' "$WORK/r18" )"; R43="$( grep '"id":43,' "$WORK/r18" )" +p_m="$( printf '%s' "$head40" | grep -bo 'all_four.rb","range":{"start":{"line":9' | cut -d: -f1 | head -1 )" +p_v="$( printf '%s' "$head40" | grep -bo 'all_four.rb","range":{"start":{"line":7' | cut -d: -f1 | head -1 )" +p_c="$( printf '%s' "$head40" | grep -bo 'schema.rb","range":{"start":{"line":14' | cut -d: -f1 | head -1 )" +p_y="$( printf '%s' "$head40" | grep -bo 'spike_names.yml","range":{"start":{"line":3' | cut -d: -f1 | head -1 )" +# The def-front clause pins the FIRST row by OFFSET, like the ws/symbol rows above: a Location row is ~130 bytes, so the +# old "first 260 bytes contain a "line":9 field" window also matched the SECOND row — a Var-first order still printed PASS. +q_m="$( printf '%s' "$head41" | grep -bo 'all_four.rb","range":{"start":{"line":9' | cut -d: -f1 | head -1 )" +q_v="$( printf '%s' "$head41" | grep -bo 'all_four.rb","range":{"start":{"line":7' | cut -d: -f1 | head -1 )" +# The hover-front clause is pinned by offset the same way. lspAppendHoverDefs renders up to kLspHoverDefCap=8 of the 19 defs, +# so the old membership grep matched the Method block wherever it sat: on the pre-tower binary it is the SECOND block of the +# hover (a Var leads it) and that clause still printed PASS. The FIRST def block is the one a reader reads first, so the +# Method block's offset must EQUAL the offset of the first "**** `" marker, not merely exist somewhere in the answer. +h_first="$( printf '%s' "$R43" | grep -bo '\*\*[a-z]*\*\* `' | cut -d: -f1 | head -1 )" +h_m="$( printf '%s' "$R43" | grep -bo '\*\*method\*\* `AllFour::name`' | cut -d: -f1 | head -1 )" +[ -n "$p_m" ] && [ -n "$p_v" ] && [ -n "$p_c" ] && [ -n "$p_y" ] \ + && [ "$p_m" -lt "$p_v" ] && [ "$p_v" -lt "$p_c" ] && [ "$p_c" -lt "$p_y" ] \ + && [ -n "$q_m" ] && [ -n "$q_v" ] && [ "$q_m" -lt "$q_v" ] \ + && [ -n "$h_first" ] && [ -n "$h_m" ] && [ "$h_m" = "$h_first" ] \ + && ok "(18) picker order: Method before Var before column-Section before yaml-Section on ws/symbol, def-front and hover-front" \ + || { no "(18) tower order wrong: m=$p_m v=$p_v c=$p_c y=$p_y d_m=$q_m d_v=$q_v h_m=$h_m h_first=$h_first"; printf '%s\n' "$head40" | head -c 400; printf '%s\n' "$head41" | head -c 240; } + +# ── (19) the ONE multi-def disclosure sentence, hover + definition logMessage, absent on single-def ─── +printf '%s' "$R42" | grep -q '19 definitions for .name. — counts are floors; a call may bind to any one of them' \ + && ok "(19) hover discloses the def count + may-bind sentence on the colliding name" \ + || { no "(19) hover disclosure missing"; printf '%s\n' "$R42" | head -c 400; } +n19="$( grep -c 'window/logMessage.*19 definitions for .name. — counts are floors; a call may bind to any one of them' "$WORK/r18" )" +l41="$( grep -n '"id":41,' "$WORK/r18" | cut -d: -f1 )"; l19="$( grep -n 'window/logMessage.*19 definitions' "$WORK/r18" | head -1 | cut -d: -f1 )" +[ "$n19" = 1 ] && [ -n "$l19" ] && [ -n "$l41" ] && [ "$l19" -lt "$l41" ] \ + && ok "(19) the definition answer carries ONE window/logMessage disclosure AHEAD of it" \ + || { no "(19) definition logMessage wrong: n=$n19 noticeLine=$l19 respLine=$l41"; } +# Premise (fix round 4): credit the silence only to an ANSWERING hover. When the id-5 row was absent (dialog +# truncated, decodeFrames threw) or its result null (lspHover regressed), the old bare `grep -q` failed and the || +# branch printed PASS — the one clause of the round that could not tell "no disclosure, because the name has one +# def" from "no answer at all". R 5 reads $WORK/resp, the geometry dialog: the single-def tree this control is about. +if ! printf '%s' "$R5" | grep -q '"result":{'; then + no "(19) premise broken — the id-5 hover did not answer (row absent or result null), so its silence proves nothing: $( printf '%s' "$R5" | head -c 160 )" +elif printf '%s' "$R5" | grep -q 'definitions for .scale.'; then + no "(19) single-def name carries a multi-def disclosure" +else + ok "(19) scale (single def) carries no disclosure (premise held: the id-5 hover answered)" +fi + +# ── (20) Used-at parity: hover answers every CLI --uses call row for the colliding name ──────────────── +UR="$( cd "$WORK/collide" && "$BIN" --uses=name . 2>/dev/null | grep -oE ' "$WORK/collide/click_single.rb" <<'EOF' +# C task arm 18b: consumer of the UNIQUE column event_id -> schema.rb line 29 (0-based 28) +Spike::IdRenamed.new.event_id +EOF +cat > "$WORK/collide/click_split.rb" <<'EOF' +# C task arm 18b: consumer of a COLUMN-ONLY repeated name (timestamps, no code def) -> schema row first +Spike::ColumnYaml.new.created_at +EOF +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"$uriColl\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_single.rb\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_split.rb\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":44,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_single.rb\"},\"position\":{\"line\":1,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":45,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_split.rb\"},\"position\":{\"line\":1,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":46,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_split.rb\"},\"position\":{\"line\":1,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d18b" +"$BIN" --lsp < "$WORK/d18b" > "$WORK/raw18b" 2>/dev/null +decodeFrames "$WORK/raw18b" > "$WORK/r18b" 2>/dev/null +head44="$( grep '"id":44,' "$WORK/r18b" | head -1 )"; head45="$( grep '"id":45,' "$WORK/r18b" | head -1 )"; R46="$( grep '"id":46,' "$WORK/r18b" )" +# Both clauses pin the FIRST Location row by offset, like arm (18)'s def-front sibling. The old 340-byte window spanned the +# WHOLE id-45 answer (two ~130-byte rows plus a 35-byte prefix = ~300 bytes), so it proved the schema.rb:75 row was PRESENT, +# never that it led: with the t.timestamps-minted row ahead of it the needle still fell inside the window and printed PASS. +# r1_4x is the start of the answer's first row, r2_4x the start of its second (empty when the answer holds one row); the +# needle must lie inside the first row, and for the split name ahead of the other schema row. +r1_44="$( printf '%s' "$head44" | grep -bo '{"uri":"' | cut -d: -f1 | head -1 )"; r2_44="$( printf '%s' "$head44" | grep -bo '},{"uri":"' | cut -d: -f1 | head -1 )" +r1_45="$( printf '%s' "$head45" | grep -bo '{"uri":"' | cut -d: -f1 | head -1 )"; r2_45="$( printf '%s' "$head45" | grep -bo '},{"uri":"' | cut -d: -f1 | head -1 )" +o28="$( printf '%s' "$head44" | grep -bo 'schema\.rb","range":{"start":{"line":28' | cut -d: -f1 | head -1 )" +o75="$( printf '%s' "$head45" | grep -bo 'schema\.rb","range":{"start":{"line":75' | cut -d: -f1 | head -1 )" +o82="$( printf '%s' "$head45" | grep -bo 'schema\.rb","range":{"start":{"line":82' | cut -d: -f1 | head -1 )" +[ -n "$o28" ] && [ -n "$r1_44" ] && [ "$o28" -ge "$r1_44" ] && { [ -z "$r2_44" ] || [ "$o28" -lt "$r2_44" ]; } \ + && [ -n "$o75" ] && [ -n "$r1_45" ] && [ -n "$r2_45" ] && [ "$o75" -ge "$r1_45" ] && [ "$o75" -lt "$r2_45" ] \ + && [ -n "$o82" ] && [ "$o75" -lt "$o82" ] \ + && printf '%s' "$R46" | grep -q 'definitions for .created_at.' \ + && ok "(18b) column clicks land on schema.rb — unique event_id -> its row; column-only created_at -> schema first with the N-definitions disclosure" \ + || { no "(18b) schema-column click wrong: e44 needle=$o28 in[$r1_44,${r2_44:-end}] | s45 needle=$o75 in[$r1_45,${r2_45:-end}] other=$o82"; printf '%s\n' "$head44" | head -c 240; printf '%s\n' "$head45" | head -c 240; } + +# ── (21) C/C++ decl/def: the LOWER-id header prototype yields to the bodyful def on the SINGLE-PICK verb ───── +# WHY THE FIXTURE IS NAMED quad.h + z_quad.cpp: ids follow SORTED crawl order, and "quad.h" sorts before +# "z_quad.cpp", so the bodyless prototype carries the LOWER NodeId and the tower's id-ASC tie leads with it. That is +# the only order in which resolveFocus's same-scope body preference (graph.h) decides the answer: delete the yield and +# the focus stays quad.h's declaration, which carries no call or extends edges — the walk would then name the +# declaration's own row and never reach use_quad. textDocument/definition cannot show that pick at all (lsp.h +# lspDefinition answers the WHOLE def list via resolveAllByName), so it is pinned here beside the pick, as the +# no-regression answer it actually is, and the pick itself is read off a single-pick verb. +mkdir -p "$WORK/cppdd" +cat > "$WORK/cppdd/quad.h" <<'EOF' +// decl/def fixture — the HEADER sorts FIRST, so this prototype carries the lower crawl-order id +int quad( int f ); +EOF +cat > "$WORK/cppdd/z_quad.cpp" <<'EOF' +int quad( int f ) +{ + return f * 4; +} + +int use_quad() +{ + return quad( 2 ); +} +EOF +uriCpp="file://$WORK/cppdd" +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"$uriCpp\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/cppdd/z_quad.cpp\",\"languageId\":\"cpp\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":50,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/cppdd/z_quad.cpp\"},\"position\":{\"line\":7,\"character\":11}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d21" +"$BIN" --lsp < "$WORK/d21" > "$WORK/raw21" 2>/dev/null +decodeFrames "$WORK/raw21" > "$WORK/r21" 2>/dev/null +resp50="$( grep '"id":50,' "$WORK/r21" | head -1 )" +# the whole-list answer: BOTH definitions are named, the body among them (the list is not a pick — do not read order here) +printf '%s' "$resp50" | grep -qF "\"uri\":\"file://$WORK/cppdd/quad.h\",\"range\":{\"start\":{\"line\":1" \ + && printf '%s' "$resp50" | grep -qF "\"uri\":\"file://$WORK/cppdd/z_quad.cpp\",\"range\":{\"start\":{\"line\":0" \ + || { no "(21) the def list no longer names both the prototype (quad.h:2) and the body (z_quad.cpp:1)"; printf '%s\n' "$resp50" | head -c 300; } +ar21="$( cd "$WORK/cppdd" && "$BIN" --around=quad . 2>/dev/null )" +printf '%s' "$ar21" | grep -q ']*defs="2"' \ + || no "(21) premise broken — --around=quad's root carries no defs=\"2\", so the name has no pick for the refinement to make: $( printf '%s' "$ar21" | grep -m1 -o ']*>' | head -c 160 )" +w21="$( printf '%s' "$ar21" | grep -cF '' )" +[ "$w21" = 1 ] && [ "$c21" = 1 ] && [ "$n21" = 0 ] \ + && ok "(21) single-pick decl/def refinement composes with the sort: --around walks the bodyful .cpp def and reaches its caller, never the lower-id header prototype" \ + || { no "(21) the pick did not move to the body — the same-scope yield is what does that here (bodyRow=$w21 caller=$c21 declRow=$n21)"; printf '%s\n' "$ar21" | grep -o ']*>\|' | head -4; } + +# ── (23) known-gap coherence: the CLI row shows the tower front AND its defs= count ─────────────────── +c23="$( cd "$WORK/collide" && "$BIN" --around=name . 2>/dev/null | grep -m1 -o '/dev/null | grep -m1 -o 'defs="[0-9]*"' )" +[ -n "$c23" ] && [ "$d23" = 'defs="19"' ] \ + && ok "(23) --around names the tower front (t=method AllFour::name) while disclosing defs=\"19\" — the edge-vs-display gap is visible on the row" \ + || { no "(23) known-gap coherence broken: focus='$c23' count='$d23'"; } + +# ── (24) a constructor never outranks its class ────────────────────────────────────────────────────── +mkdir -p "$WORK/ctorj" "$WORK/ctorc" +cat > "$WORK/ctorj/Shape.java" <<'EOF' +public class Shape { + public Shape() {} + public void area() {} +} +class Sq extends Shape { public void area() {} } +class Maker { public Sq Sq() { return null; } } +EOF +cat > "$WORK/ctorc/a_widget.h" <<'EOF' +class Widget { +public: + Widget(); + virtual void draw() = 0; +}; +EOF +cat > "$WORK/ctorc/widget.cpp" <<'EOF' +#include "a_widget.h" +Widget::Widget() {} +class Button : public Widget { public: void draw() override {} }; +EOF +lj24="$( cd "$WORK/ctorj" && "$BIN" --lego=Shape . 2>/dev/null )" +aj24="$( cd "$WORK/ctorj" && "$BIN" --around=Shape . 2>/dev/null )" +lc24="$( cd "$WORK/ctorc" && "$BIN" --lego=Widget . 2>/dev/null )" +ac24="$( cd "$WORK/ctorc" && "$BIN" --around=Widget . 2>/dev/null )" +nm24="$( cd "$WORK/ctorj" && "$BIN" --around=Sq . 2>/dev/null )" +printf '%s' "$lj24" | grep -q ']*defs="2" implementors="1"' && printf '%s' "$lj24" | grep -qF ']*>' | head -1; } +printf '%s' "$aj24" | grep -qF ']*>' | head -1; } +printf '%s' "$lc24" | grep -q ']*implementors="1"' && printf '%s' "$lc24" | grep -qF ']*>' | head -1; } +printf '%s' "$ac24" | grep -qF ']*>' | head -1; } +printf '%s' "$nm24" | grep -qF ']*>' | head -1; } + +# ── (25) a prototype beside its body is COUNTED, never called a split ────────────────────────────── +# The graph binds helper(2) to the body alone (no amb=), so the sentence may count the prototype but must not say the +# resolver split the call: "a call may bind to any one of them" is the claim the mechanism supports. +mkdir -p "$WORK/protodef" +printf 'int helper(int x);\n' > "$WORK/protodef/h.h" +printf '#include "h.h"\nint helper(int x) { return x + 1; }\nint top(void) { return helper(2); }\n' > "$WORK/protodef/h.c" +uriPD="file://$WORK/protodef/h.c" +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"file://$WORK/protodef\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":51,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"$uriPD\"},\"position\":{\"line\":2,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":52,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"$uriPD\"},\"position\":{\"line\":2,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d25" +( cd "$WORK/protodef" && "$BIN" --lsp < "$WORK/d25" > "$WORK/raw25" 2>/dev/null ) +decodeFrames "$WORK/raw25" > "$WORK/r25" 2>/dev/null +h25="$( grep '"id":52,' "$WORK/r25" )" +n25="$( grep -c 'window/logMessage.*2 definitions for .helper.' "$WORK/r25" )" +printf '%s' "$h25" | grep -q '2 definitions for .helper.' && [ "$n25" = 1 ] \ + || no "(25) premise broken — the prototype+body pair no longer reads as 2 definitions (hover/logMessage=$n25)" +grep -q 'splits over' "$WORK/r25" \ + && no "(25) the disclosure says the resolver SPLIT a call it bound to the body alone" \ + || ok "(25) prototype+body: counted as 2 definitions, no split claimed (the call binds to the body)" + +# ── (26) kind tier before language tier ────────────────────────────────────────────────────────────── +mkdir -p "$WORK/mixlang" +printf 'class AllFour\n attr_reader :name\nend\n' > "$WORK/mixlang/a.rb" +printf 'class name:\n pass\n' > "$WORK/mixlang/b.py" +printf 'class Other\n def name\n 1\n end\nend\nOther.new.name\n' > "$WORK/mixlang/c.rb" +o26="$( cd "$WORK/mixlang" && "$BIN" --impact=name . 2>/dev/null | grep -o ']*p="[^"]*"' | tr '\n' ' ' )" +case "$o26" in + *'ix="1" t="method" sc="Other" p="c.rb:2"'*'ix="2" t="cls" sc="name" p="b.py:1"'*'ix="3" t="var" sc="AllFour" p="a.rb:2"'*) + ok "(26) kind before language: Ruby method, then the Python class, then the Ruby attr" ;; + *) no "(26) tier order wrong (a language-first comparator puts the Ruby attr ahead of the Python class): $o26" ;; +esac + +# ── (22) determinism x2 on the collision dialog (house pattern) ─────────────────────────────────────── +"$BIN" --lsp < "$WORK/d18" > "$WORK/raw22a" 2>/dev/null +"$BIN" --lsp < "$WORK/d18" > "$WORK/raw22b" 2>/dev/null +cmp -s "$WORK/raw22a" "$WORK/raw22b" && [ -s "$WORK/raw22a" ] \ + && ok "(22) collision dialog byte-identical across two runs" \ + || { no "(22) collision dialogs differ"; ls -l "$WORK/raw22a" "$WORK/raw22b"; } + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/mcphandlecheck.sh b/test/mcphandlecheck.sh index 2a715a2f2..f424ee16a 100755 --- a/test/mcphandlecheck.sh +++ b/test/mcphandlecheck.sh @@ -187,7 +187,7 @@ echo echo "=== 6b. R2c (the 2026-08-12 usage mine): a bare symbol NAME is accepted where a handle is expected ===" # fetch_body's strict handle parse stays (steps 5/6 above are untouched contracts), but a string that is # not even handle-SHAPED is now tried as a symbol name through the same lookup path find_symbol uses -# (resolveAllByNameQualified, lowest-id pick), DISCLOSED via resolved_from_name + the REAL handle in the +# (resolveAllByNameQualified, the tower-front pick), DISCLOSED via resolved_from_name + the REAL handle in the # result — so the one-shot answer also teaches the handle for next time. A "sym#..."-prefixed string is # NEVER treated as a name (a corrupt real handle must keep the malformed refusal — step 6's guard). @@ -241,7 +241,7 @@ grep -q GUARD_OK "$TMP/symgarbchk" \ && ok "(6b-3) 'sym#'-prefixed garbage keeps the malformed-handle refusal (no name fallback)" \ || no "(6b-3) $(cat "$TMP/symgarbchk")" -# (6b-4) [red] same-named defs in DIFFERENT files: serve the lowest-id pick, DISCLOSE the others +# (6b-4) [red] same-named defs in DIFFERENT files: serve the tower-front pick, DISCLOSE the others # (name_defs + other_defs with file:line + each one's handle — the honest sibling of step 2b'\''s # overload note, for the name path). A second def is planted in the PRIVATE corpus copy. printf 'int dupTwinFn( int x ) { return x + 1; }\n' > "$CORPUS/core/twin_a.cpp" @@ -261,7 +261,7 @@ okO = len(others) == 1 and str(others[0].get("handle","")).startswith("sym#") print("DUP_OK" if (okN and okO) else "DUP_BAD:name_defs=%s others=%s" % (j.get("name_defs"), json.dumps(others)[:160])) ' "$TMP/dupname" > "$TMP/dupchk" grep -q DUP_OK "$TMP/dupchk" \ - && ok "(6b-4) [red] same-named defs: lowest-id pick served, name_defs + other_defs disclosed" \ + && ok "(6b-4) [red] same-named defs: tower-front pick served, name_defs + other_defs disclosed" \ || no "(6b-4) [red] $(cat "$TMP/dupchk")" # (6b-5) determinism: the by-name fetch is byte-identical across two server processes. Two FRESH calls diff --git a/test/ownerscheck.sh b/test/ownerscheck.sh index 3effe3f47..5b77998e0 100755 --- a/test/ownerscheck.sh +++ b/test/ownerscheck.sh @@ -212,10 +212,11 @@ OUT_B="$( "$BIN" "$REPO" --owners --no-cache 2>/dev/null )" # ── §B11.3-class: --owners=SYM IS A FOLD, and the fold is now disclosed ─────────────────────────────────── # Found by §B11.3's own sweep ("a verb that folds a group and reports a scalar as if the group were one -# thing"). --owners=SYM resolves the name, takes defs[0] — ONE of N, the lowest node id — and reports that -# definition's file under files="1", which reads as "this symbol lives in one file", while --callers/--uses/ -# --impact/--mentions on the SAME name all disclose defs="3". of=/defs= make the pick legible; the pick -# itself is unchanged (it is resolveFocus' rule, and narrowing is what file:name is for). +# thing"). --owners=SYM resolves the name, takes defs[0] — ONE of N, the tower front (kind, leading language, +# then the lowest id) — and reports that definition's file under files="1", which reads as "this symbol lives in +# one file", while --callers/--uses/--impact/--mentions on the SAME name all disclose defs="3". of=/defs= make +# the pick legible; the pick itself is still resolveFocus' rule — it moved with the tower. Narrowing is what +# file:name is for. if command -v git >/dev/null 2>&1; then OF="$( mktemp -d )"; mkdir -p "$OF/a" "$OF/b" printf 'int twinned( int q ) { return q; }\n' > "$OF/a/one.h" diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 17d07a5c0..ba1891a70 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -12,7 +12,7 @@ clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c4 help 0 ecc9e1ea480e502fb228fb129f1309e37e30918d8baf3f36e9b9f01be0277d59 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 0cca7164a48d6543694bf07b4252209558d189771d50e913e46783bb98ae4bed e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -around 0 827b569b350640938954e34497f0430f757b4baac8f0daa2194306113cb46a36 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +around 0 47510713fadffc2c6f8745bc4dbb895bedf00062bd27deb026709c72605f0d61 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 uses 0 5442767b638c8cac9eb0327cbfd5fde73f92fde5b85e2a995670a3982d903711 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 path 0 976a050ece8954e7e0b245df5d61b693bb10dec1f3867918ec519c72f83f4aee e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connect 0 6ceb99d6c5ecb9f57a1b7426da2ed2118705cb7cc6b3d0aa4456b84c0ff715f4 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -34,7 +34,7 @@ uses_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 96c grep_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 278827e2a7740aeeefa653b87ec47f74c98ec97a1758ce2e5426855a845a80c4 expand_missing 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 10f2dda880ec34c8d5a3f5bc50bd1bf50d0abe20c308de11ca8a16714f87b496 callers_missing 1 39a4cf4b2702863152edcc6c270255bac015a17d25e834dbdff181d809181326 0770e05552f4baf1c25d213e35c5f273c6f341e3c518e8233a83ff849abcd1c9 -safe_delete 0 e3535cdf695ad1d3e9ffbec44d529478059317b226fb60b2dfffaebc90b44218 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +safe_delete 0 16990750bd7c003d73fa9c4ebb85ca8a9a3e6ffe8bf8f9987ed47f5459232403 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 98a50bcc6b32183d7967ee6af8f9219f083c00bde882f3aff4ebe1f39f4166e4 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855