fix(cli): three ways search could answer with nothing, silently (supersedes #80) - #82
Merged
Merged
Conversation
Measured over 178 gated benchmark pairs, graph calls per session are invariant (1.00/1.03/1.00 across baseline bands <10/10-19/>=20) while displaced greps are not (1.35/2.16/3.74), so the call is a fixed toll with variable repayment: the no-op cohort (n=43) costs 1.165 CI [1.073,1.264] at +1.21 turns, the displacing cohort (n=135) costs 0.938 CI [0.889,0.990] at -2.07 turns, corr(turn delta, log ratio) = +0.735. No static payload feature predicts a no-op call (AUC 0.553-0.690 at a 28% base rate), so selective skipping is not implementable and pre-delivery must be universal. ENTIRE_GRAPH_PRESEARCH (alias EG_PRESEARCH) names a file holding the payload computed before the agent started; search echoes it byte-for-byte and returns before the profile, repo, cache and index are touched. An unreadable path is an error rather than a silent live query.
Two printed bodies of one file with a small hole between them are one region as far as the reader is concerned, and the hole is what it spends a turn closing. Measured across 113 Opus search payloads: 45 (40%) carry at least two ranked hits in one file within 40 lines (R30Oeg 12/25, R30OegBEST 12/30, R30OegV9 9/29, C15opeg_r1 12/29). Read side: of 53 answered pre-edit Read calls only 41 are ranged, and of those only 22 (53.7%) overlap the payload snippet at all, with the payload covering a mean 18.2% of the requested span - so nominal 62.0% read coverage is worth ~33%. Suite level: the tool makes +19.6% MORE Read calls than the no-tool baseline while total tool calls fall 14.5% (n=55 paired instances). Call by call: fluentd-3328 had gold at rank 1 (in_tail.rb:395-425, score 135.8, complete-symbol, "Top hit is exact") and still cost 1.64x, because ranks 1/2/3 were three disjoint spans of one file (349-360, 395-425, 427-443) and turn 2 was `sed -n '200,270p'; sed -n '330,470p'` - 6.9 kB re-reading a contiguous superset of what it already had. pylint-7080 turn 2 re-fetches rank 2 verbatim with `cat`. Near hits are now folded into one span carrying the verbatim text of the whole range, headed `path:START-END [contains ranks i,j,k - contiguous, nothing elided]`, because the reason the agent re-reads is that it cannot tell whether the hole hides something. Filling gaps adds bytes, and under a fixed ceiling added bytes evict the last ranked hit, so three bounds are enforced: a 40-line gap cap, an 80-line span cap, and a cost check of the whole prospective ranking against --max-context-bytes before each merge is committed - a bridge that would not fit simply does not happen. Only complete-symbol results take part (the one signal every renderer prints in full, so a merged header can never name a range whose text is absent), and merging never reorders: a run's survivor is its best-ranked member, so the rank at which a file first appears can only improve. (cherry picked from commit daa9b95d7911f35abbd0edfefac14bbea5cbed47)
Ported from eg-consolidated 024f25a. FocusLine is computed for every hit and has always been in the JSON; the text payload printed only the snippet range, so a hit on a 27-line method said 'the answer is somewhere in 135-161'. Asked what one change to the returned payload would let it finish in the fewest calls, a Sonnet agent that had just fixed apache/lucene-13170 with this tool named exactly this -- its only non-essential call was a Read to pin down line 151. Re-reading a file the payload already printed is 10.1% of post-payload tool calls across 30 measured sessions. Header, not an inline marker on the snippet line: agents copy snippet text verbatim as an edit's old_string anchor, so decorating a body line would break the anchor. Entire-Checkpoint: 2893461866fb
Adds EG_SEARCH_SESSION (the per-task state file the one-shot CLI needs to count its own calls) and EG_MAX_SEARCHES (default 1, 0 disables). Past the cap, search returns the first call's payload verbatim under a one-line header instead of running a new query. Measured: sessions with >=4 graph calls (n=16) cost 1.148 of the no-tool baseline against 0.975 for one-call sessions (n=70), diff +0.173, bootstrap CI [+0.019,+0.324]. Same gradient per config: 1.00 calls/session -> -12.7%, 1.17 -> -0.7%, 1.63 -> +5.1%, 4.45 -> worst retrieval of the set. (cherry picked from commit 823cc5e43e7e30bb121e0502c27d4f5e50f3e3b4)
…levers
Editability — the share of needed edits whose replaced text is present
VERBATIM in the payload — is a different measurement from recall, and the
two diverge. On the R30PUB benchmark payloads the gold file is ranked for 8
of 12 gold files while the gold LINES are printed for none of them.
The misses are not budget misses. They are the three conditions the
opportunistic body upgrade applies:
- searchEnclosableSymbolKind excludes containers, so a hit inside a class
body, a constant table or a type declaration resolves to no callable and
keeps a six-line window;
- defaultSearchEnclosureMaxLines (160) drops a longer callable back to a
window, which is the case where reading the file back costs the most;
- allocateSearchSnippets only evaluates demotion cuts down to
minInt(headRanks, len(results)), so at --top-k 5 with headRanks 5 the
only cut it can consider is "demote nothing" and the entire body budget
is the 10 kB growth allowance.
--full-unit-top N bypasses all three for the first N ranks: the unit is
resolved as the callable first and then as any enclosing symbol, bounded
only by a 400-line safety cap, and the allocator seats it before it prices
anything else, demoting the tail to locators to pay for it.
--max-context-bytes still wins — when the ceiling cannot hold the unit with
the whole tail reduced to locators, forced ranks hand their ranked snippets
back deepest-first, so rank 1 yields last. N >= 2 reaches rank 2 only while
its score is within 15% of rank 1's. A clipped unit reports
"elided lines X-Y (unit continues)" on its own line after the body, never
inline: body text is copied verbatim as an Edit anchor.
--edit-site-bodies gives the SAME-CONCEPT LITERAL block's EDIT sites their
source — the enclosing unit when resolvable, else +/-12 lines — capped at 60
lines and 3 kB per site and 8 sites, with the block's cap raised to 12 kB
while bodies are attached. CONSUMER and DOC sites stay file:line-only: a
consumer is listed precisely so an agent can decide not to open it.
Also fixes two cases where the renderer discarded source the allocator had
already paid for: a head-window result past the second rank was printed as a
bare locator (measured on fmtlib__fmt-2457, 1,709 B seated and thrown away),
and a forced unit could reach a rank below that tier.
Both flags default to off. Verified on eight benchmark instances that the
default payload is byte-identical to the previous binary; with the flags on,
verbatim source in the payload rises 21.2 kB -> 34.0 kB (top 2) and gold
files present as a locator with no source at all go 2 -> 0.
JSON additions are additive only: SearchResult.unit_start_line/unit_end_line
and SearchLiteralHit.body plus its line ranges.
…unit
With the unit levers on, the payload's remaining editability misses are not
truncated units — they are the WRONG unit of the right file. Measured across
eight R30PUB instances: 8 of 12 code-gold files ranked, 0 carried the gold
hunk verbatim, and in every ranked case the printed unit and the gold unit
were different callables of the same file. The commonest shape is a thin
public entry point ranked while the gold sits in the helper it calls:
redis__redis-10095 ranked src/t_list.c:577-581 lpopCommand (3 lines)
gold src/t_list.c:501 in popGenericCommand (489-535)
edge lpopCommand --CALLS--> popGenericCommand
One outgoing CALLS edge away, in the same file, already in the graph.
search_related.go excludes outgoing CALLS deliberately (:41-43) on the
grounds that such cases "are already reached as co-members of the same
unit". They are not: searchRelatedUnitCoMembers requires an identical
ContainerID and returns nothing at all once the unit holds more than
searchRelatedUnitMemberLimit (12) members, and src/t_list.c has ~40
top-level functions, so that route switches off entirely and no route in
the block can produce the site. The exclusion's reasoning still holds for
the four-slot block it was written about, so this is a separate gated route
and the related-sites rotation is untouched.
--callee-hop resolves the head's outgoing CALLS and admits up to three
callees as candidate fix sites, ordered by same-file then query overlap then
edge confidence. It fires for rank 1, plus rank 2 under the same
searchFullUnitGapRatio test --full-unit-top uses, and fires regardless of the
anchor's unit size — which is where the co-member limit turns off. Admission
requires a resolved same-repo callable whose file passes the ranker's own
file-class and test-artifact priors, and a cross-file callee must have a
query word in its NAME: without that the first cross-file hop on
redis__redis-10095 was zcalloc. Same-file callees are admitted
unconditionally, because popGenericCommand shares no word with an issue
phrased "LPOP ... null array reply".
Bodies follow the other levers rather than inventing a third policy: source
when --full-unit-top or --edit-site-bodies is on, a declaration locator
otherwise. Funding order is head, then callee bodies paid for by TERSIFYING
the tail (never dropping it, so no file loses its only mention), then fewer
callees; --max-context-bytes is exact at every step. Entries carry a
callee-hop signal and stats.callee_hop_sites so payloads stay attributable,
and print no ranked score because they were admitted by an edge.
Measured on the same eight instances: strict gold-hunk-verbatim goes 0/12 to
1/12 (redis__redis-10095 now carries the whole popGenericCommand body
including the exact replaced line), the hop fires on 3 of 8, and the default
payload is byte-identical to 6da0291 on all eight.
…ctive A 50-instance replay found --full-unit-top net-NEGATIVE: gold coverage in the losing cohort fell 28% -> 16% against the control. Per-instance forensics found three independent defects, all in funding rather than retrieval. 1. DESTRUCTIVE FUNDING. The forced pass demoted the tail to locators before the non-forced head was funded, so a lower rank that had been carrying a complete gold-covering body lost it. On redis__redis-11734 the control gave rank 4 the whole bitposCommand (src/bitops.c:882-1008, complete-symbol) and the forced run emitted `4. src/bitops.c:882 bitposCommand` with no source, losing three gold hunks to a 61-line header window at rank 2. Forced units are now seated ON TOP OF the allocation the same inputs produce with the flag off. That control plan is a floor: a forced unit may only spend budget the control left unused, plus the snippet bytes of ranks the text renderer prints as bare locators anyway. Nothing a reader would have seen is ever reduced. When the whole unit will not fit, the widest clipped form that does is seated, and only above searchForcedUnitMinLines; otherwise the pre-existing allocation stands. 2. UNSPENT BUDGET. Independently, 0 of 50 replayed payloads used even half the 24 kB ceiling (median 4,557 B), and redis__redis-11734 rendered a 127-line body as a 34 B locator with 18 kB free: --max-snippet-lines was acting as a ceiling on expansion instead of a floor on context. Under any payload-shape lever the allocator now plans an enclosure for EVERY ranked hit and spends the remaining ceiling in rank order, so a ranked hit never renders as a bare locator while its unit (or a >= 20-line window) would still fit. The pool is the caller's own --top-k, not a hidden second pool: gold units that sit at rank 6-8 are reached by raising --top-k, which keeps one visible contract. 3. HOP PRIORITY. --callee-hop entries were seated as full bodies and evicted ranked bodies outright. They are now the lowest-priority claim in the payload: they spend only what no ranked hit claimed, and they degrade themselves — full body, then a >= 20-line clip, then a locator — rather than reduce a ranked hit. The related-sites block gets the same guard: its own docstring says it reclaims redundant tail LOCATORS, but it was offering body-carrying results for displacement, which is how bitposCommand's body was dropped to seat four caller locators. Also size-aware admission for the container bypass. A non-callable unit is forced only under searchForcedContainerMaxLines: that keeps the win it was built for (rubocop-13680's 5-line config/default.yml section) and refuses the loss (docusaurus-9183's DocusaurusConfig expanding 6 -> 273 lines). Head windows over declaration-only regions cap at searchDeclarationWindowLines, since a prototype block has no body a wider window would explain. Measured on 10 instances at 24576 B, gold replaced-lines present verbatim: control 9/15, +L1/L2 11/15, +hop 12/15, +hop at --top-k 12 14/15. Every rank present without flags is still present under every flag combination, no payload exceeds the ceiling, no symbol-less bare locator appears while half the budget is free, and the default payload is byte-identical to 6da0291.
…ite's package A Go module routinely declares one type name per sibling package — prometheus/prometheus has 14 `Discovery` types with a `refresh` method, one per discovery/<provider>/. Same-name candidate selection preferred a same-file declaration and then took the first match in index order, so `d.refresh(ctx)` in discovery/puppetdb/puppetdb_test.go bound to discovery/azure's Discovery.refresh and no traversal from the test reached the code under test. The same first-match rule sent NewDiscovery's `conf.Port`/`conf.RefreshInterval` reads to azure's SDConfig. Rank candidates by path proximity instead: same file, then same directory (a Go package IS a directory), then the nearest shared module-path prefix. Ties keep input order, so an unscoped ambiguity resolves exactly as before.
…ugh them tree-sitter's Go interface body produced no member symbols, so an interface was a dead end in the call graph: hashicorp/terraform's communicator.Communicator had ZERO incoming CALLS even though every provisioner drives its remote work through it, and `comm.Disconnect()` in runScripts bound to nothing. Three parts: - emit a method symbol per interface method requirement (`method_elem`, and `method_spec` for older grammars), qualified under its interface; - type a receiver whose declared type is package-qualified by an in-module import (`comm communicator.Communicator`) — parameterVarTypes requires a bare capitalised type name, so an interface-typed parameter previously carried no receiver type at all and fell through to the globally-unique-name tier, which cannot fire when the interface and its implementations share a method name; - carry a call that landed on a requirement through to the concrete methods that satisfy it. Go satisfaction is implicit, so it is recovered structurally: a type qualifies when it declares a method for every requirement. A one-requirement interface only carries through when the called name is unique in the workspace (`Close`/`String`/`Error` are everywhere), and past 8 satisfying types the interface edge stands alone. The carry-through runs once over the finished relation list rather than at each typed-receiver tier, so a call reached through a parameter, a returned type or a constructor chain is treated identically. All edges are additive; none are rewritten.
A C++ specialization is named after the PRIMARY template, not after what it specializes: fmtlib/fmt's `template <typename Char, typename... T> struct formatter<tuple_join_view<Char, T...>, Char>` is indexed with the qualified name `formatter` — the most generic name in the repository, shared by dozens of declarations — so no name signal could ever rank it. Its signature carried tuple_join_view but only earned the 1.5x signature tier. Add the leading specialization argument, and the compound `Name<Argument>` form, as searchable ALIASES on the specialization and on its members. Aliases are the existing additive mechanism and already score as an exact name match, so the symbol keeps its name, its qualified name and — decisively — its compound-v1 ID, which is derived from the qualified name; renaming to `formatter<tuple_join_view>` would have re-keyed every specialization in every indexed repository. Only the leading argument is used. Partial specializations discriminate on it by convention and carry the primary template's own parameters (`Char`, an `enable_if_t` guard) in the trailing positions, so aliasing the rest would bind a declaration to its template parameters.
…efusing
Session-level forensics of 19 paid sessions found that the tool's most
expensive failures were refusals to answer a live query, not wrong answers.
Two messages account for both measured blow-ups, and in each case the agent
paid to do by hand what the tool had declined to do.
FIX A — "Ambiguous symbol %q matched N definitions; rerun with the selector
printed beside the one you mean." Measured +$2.22 on projectlombok__lombok-3486,
78 pre-edit operations spent manually disambiguating two same-named methods
whose locations the tool had already printed. Ambiguity means the graph found
MORE than it was asked for, which is information rather than an error, so the
listing now says "%q matches N definitions; all are listed, the first 2 with
source", keeps every locator and its narrowing selector, and appends a compact
body (<= 40 lines, elision noted) for the top matches. The rerun instruction is
gone, and both ambiguity tests now assert its ABSENCE.
FIX B — "No symbols matched %q" / "(no symbol named X)". Measured +$1.23 on
phpoffice__phpspreadsheet-3570: both live queries came back empty and shell
greps went 18 -> 39, because the query spelled the name in a different but
obvious way. An exact miss now degrades down a fuzzy ladder ordered by how much
it assumes — case-insensitive, then separator-insensitive (flattenSingleValue ==
flatten_single_value), then identifier-token subset in either direction, then
substring — and returns the best 3 candidates with locators plus a body for the
first. Every fuzzy answer is labelled with the rung that produced it, so a
caller is never misled into thinking it got what it asked for.
Both live on the one resolver and the two renderers that def, neighbors and
impact share (resolveFocusSymbolsOrFuzzy, writeDisambiguationListing,
writeFuzzyMatchListing), so no verb can drift back to refusing, and callers/
callees inherit the fuzzy focus resolution for free.
The substring rung requires 8 characters. def has a standing invariant that a
short fragment must never resolve to a longer name ("dele" is not "deletion")
and that invariant is right; the rungs above already cover every spelling case
this fix was built for, so nothing real depends on relaxing it.
Responses gain fuzzy_match, fuzzy_match_kind and match_bodies (additive).
…fuse tests The fuzzy resolver sorted candidates by how well each matched, and then both buildNeighborResponse and defIndex.resolve re-sorted them by file path — which buried the correct answer. On phpspreadsheet-3570 a query for flatten_single_value listed a `Single` class sharing one token above Functions.flattenSingleValue, and printed that class's body. File/line order is right for an EXACT match, where several definitions of one name are equally valid answers and a stable positional order is the honest presentation. For a fuzzy answer the order IS the answer, so it is now preserved. Verified on the real checkout: the method comes back first, with its body. Tests cover both fixes end to end — every spelling of the ladder, the standing def invariant that a four-letter fragment resolves to nothing, --file/--kind narrowing surviving a fuzzy name, the tokenizer across camel/snake/kebab/::/. spellings and capital runs, and both renderers asserting the absence of the rerun instruction. Also pins that pure case difference is already an exact match (resolveFocusSymbols compares with EqualFold), so the case rung is not mistaken for the thing that makes case work.
…VERIFY line Three tool-side fixes from session forensics. 1. ELISION CONTRACT (correctness). On briannesbitt__carbon-2752 the payload ranked Comparison.php #1 tagged full-unit,complete-symbol while the printed body elided lines 630-1125 — and the focus line, the region the edit had to land in, was 989. The signal was false, which is worse than a small body: an agent that trusts it edits the wrong place, and one that does not trust it re-reads the file, so the bytes bought nothing either way. The cause was the anchor. A clip anchored at the unit's START shows the declaration and drops whatever the hit was about, which for a 1,200-line unit is everything that matters. clipSearchUnitToCap now always centres on the focus, a clipped forced unit gets a focus window of +/-60 rather than a 400-line anchored slab, and widenSearchResultToEnclosure withholds BOTH complete-body signals whenever the printed span does not contain the focus — tested against the incoming focus, before the clamp rewrites it and hides the violation. The invariant is enforced at the one place signals are attached, so no caller can reintroduce it. 2. DEGENERATE IMPACT. An anchor with no relations still printed a focus line, a blast radius reading all zeros and one empty header per relation kind, which looks like an answer and is replayed on every later turn. Measured over 8 sessions: all 3 retrieval-miss instances produced exactly that (carbon-2752 isLongYear 0/0, prometheus Config.ScrapeConfigs 0/0/0, three.js build-only) and 0 of the 5 payload-hit instances did. Such a result now emits one machine-readable line, "IMPACT DEGENERATE: <anchor> has <reason>", for either of two reasons: no callers, callees or type consumers, or every relation (or, for an unresolved anchor, every definition) landing in built or vendored output. Co-change and sibling counts deliberately do not rescue it — they are heuristics, and a payload carrying only those has still said nothing about what the change breaks. The verdict is computed once on the finished response so text and JSON (degenerate, degenerate_reason) cannot disagree. 3. VERIFY CONTRACT. The emitted command now carries what to do with it: run it once after editing; if it fails, fix the code and re-run the same command; do not build an alternative harness, hand-assemble a classpath, or re-verify by reverting and reapplying. Each clause is a measured churn mode — post-edit verify churn cost +$4.11 across three sessions, and on gson-1014 a hand-built javac/JUnit classpath was 55.6% of the session's output tokens. When VERIFY is emitted is unchanged; only the contract text is added, and its cap rises from 320 to 640 bytes to hold it. Default no-flags search payload differs from 6da0291 by exactly the two added VERIFY contract lines on all eight replay instances, and by nothing else.
…block suppression Turn-level forensics of 12 real sessions: payloads got zero references, the VERIFY command was unrunnable or non-covering in 12/12 and chasing it cost ~35% of turns, impact asserted authority on the wrong symbol in 11/12, and payloads were 45-75% noise. F1 VERIFY. Kept our narrow->suite ladder, the contract note and the cap; added a third rung (single-file parse check: php -l, ruby -c, node --check, python -m py_compile) and a fourth (a residual floor line, so absence is never silent). Cherry-picked from the sibling branch: missing-runner detection, which ANNOTATES and never suppresses; the cost exclusion that keeps the note from deleting the block through a byte budget; the recovered-test-name family, so Go/Cargo narrow to a case name the test file actually declares; and CMake, which is why C and C++ had no tier at all. Universality: depth 8 -> 32 (monorepo test trees are 9-14 segments deep and the walk terminating early is indistinguishable from "no build system"); PREFIX affixes (test_*.py, test_*.rb, Test<Stem>) and test/<->spec/ swaps in both directions; suite-Node reads devDependencies like the narrow tier already did. Every command reports its tier on the evidence line and in stats.verify_tier, and --verify-prefix decorates after any `cd &&` while "VERIFY: " stays byte-identical at line start. VERIFY is promoted out of the droppable suffix set: it now rides in the agent-format prefix, yields only to a ranked location, and gets first refusal on leftover bytes when it has to. F3 Suppression. The degenerate-impact gate counts co-change as actionable (superseding round 4, which excluded it). The SAME-CONCEPT LITERAL block is emitted only when at least one EDIT site is LIVE CODE — nushell's three EDIT sites were commented-out code, so the block sent the agent to patch a comment. Completeness prints a fraction only when the denominator is real: "35 of 0 files failed to parse" (three.js) and "6 of 0" (terraform) are numbers that cannot be true and they discredit every other count on the line. F4 LOW CONFIDENCE now also fires mechanically on a near-exact top1/top2 tie (lombok-3486 shipped clean at delta 0.0100 and cost $2.22) and on a top hit whose own content is a doc comment, an example-usage list or a deprecated forwarder. The tie test is ABSOLUTE, not relative: read as 5% it fired on 56 of 56 calibration queries whose target provably exists, and a marker that always fires teaches the reader to ignore it. F5 Payload diet: the default text payload prints at most three full bodies and renders the rest as locators. Bodies past the third were never referenced in any of the 12 sessions. Ranks the caller forced with --full-unit-top or --callee-hop are exempt, and the diet writes the locator itself because writeTextSearchResult must keep refusing to collapse source the allocator paid for.
…E-FIX status line F4 GUARD-AWARE DERIVATION. fmt-2457's covering test `ranges_test.join_tuple` lives inside `#ifdef FMT_RANGES_TEST_ENABLE_JOIN`, so a correctly shaped `ctest -R ranges-test` compiles the case OUT and passes while exercising nothing. A command that silently verifies nothing is worse than the suite command it displaced, because it reports success. The guard is now discovered by a line scan (`#ifdef X`, `#if defined(X)`, `#[cfg(feature = "x")]`) that tracks balanced `#endif`, so a guard that closed before the test is never attributed to it, and Rust attributes only bind to an adjacent item. A C/C++ macro guard is satisfied from the CMake configure step (-DCMAKE_CXX_FLAGS=-D<MACRO>); a Rust feature guard becomes `--features x` but only when the manifest declares it, because an undeclared feature is a hard cargo error. Where the guard cannot be satisfied the command still ships and the derivation SAYS so, and either way the guard is printed as its own `guarded by:` line. The satisfaction rule is deliberately strict: a guard is only acted on when the test declaration was identified UNIQUELY. `TEST(ranges_test, join_tuple)` and `TEST(ranges_test, c_style_array)` both contain the suite name verbatim, so a payload whose symbol is the suite matched every case in the file — and an early version defined FMT_RANGES_TEST_ENABLE_C_STYLE_ARRAY for a case guarded by ENABLE_JOIN. Defining the wrong macro is the worst outcome available: it claims coverage the command does not have. Ambiguous now means unsatisfiable, reported. Test-line resolution also only considers DECLARATION-shaped lines (gtest/catch macros, Go/Rust functions, Python/Ruby methods, JS specs) and ranks them by how much of the name they spell out, instead of taking the first substring match. F3-RENDER. --verify-prefix-status <text> renders one `PRE-FIX:` line under the command, verbatim, capped at 200 bytes, newlines collapsed so a wrapper cannot inject a second line that parses as a VERIFY line. The binary deliberately does not interpret the text: the harness validates the pristine tree, and a status the tool invented would be a claim about a run it never made. The command cap rises 640 -> 768: the guard clause plus its provenance is a fixed addition, and at 640 it degraded the derivation away on exactly the instance the guard work was for. The define is on the command line either way; the derivation is what lets a reader judge it.
…s to low-value paths A ranked hit can match entirely inside prose. On preactjs__preact-3010 the gold file arrived as `src/diff/children.js:19`, a bodyless locator sitting in the JSDoc block of `diffChildren`, while the three body slots went to `benches/src/keyed-children/index.js` and `karma.conf.js`. The payload had located the right file and then described it by quoting four lines of `@param` text. Two changes, both default-on and both about RENDERING rather than ranking: - A hit whose focus line is a comment is re-anchored to the first code line that comment documents (the declaration below it, or a nearby enclosing unit) before enclosure planning runs. From there it is an ordinary code hit: the planner looks for a callable at the code line, the hit competes for a body slot at its code location, and the SAME-CONCEPT LITERAL block mines program text instead of `@param` prose. The matched line stays visible as `focus=19->26`, so the move is never silent. - Bodies from benchmark, demo, example and build-config paths yield their slot to real source, and charge neither a body slot nor a rank-tier position — the same reason a sectioned-away non-code hit does not charge its rank. A payload whose only program text IS the benchmark still prints it. Measured over 20 instances at the benchmark's own flags, 14 payloads are byte-identical and all 6 that move are correct comment-to-code re-anchors.
The re-anchor could take source away instead of adding it. On vuejs__core-11870 `arrayInstrumentations.ts:10->12` gained a complete body, became the third body in rank order, and pushed `runtime-core/src/helpers/renderList.ts:54-107` — 54 lines of the production helper the issue is about — out of the payload as a bare locator. Same invariant seatForcedSearchUnits enforces for forced units, now applied to re-anchored hits on both currencies: - Bytes: the re-anchored enclosures are planned, priced, and accepted only if the resulting allocation still shows every other rank everything the control allocation (the same plan with those enclosures blanked) showed it. - Slots: the text renderer counts the ordinary hits' demand on the three-body diet first; a re-anchor-gained body takes what is left. A denied hit keeps its re-anchored `focus=`, which is the larger part of the win and costs nothing. "Re-anchor-gained" is decided by that control comparison, not by `CommentFocusLine > 0`: most re-anchored hits already carried a body, and treating those as debtors turned two 30-line method bodies into locators on fluent__fluentd-4655 and facebook__docusaurus-9183. Verified over 30 replay instances: zero bodies lost or narrowed except the intended low-value-path demotions.
…ulti-query def
Transcript forensics of 12 sessions: the tool's rare real invocations were
fumbled by output ergonomics rather than by wrong answers.
1. NUMBERED, NEVER-TRUNCATED NAVIGATION BODIES. def/callers/neighbors bodies now
print each line in a right-aligned line-number gutter (" 989→ …"), and the
per-body cap rises 40 -> 400 lines with an actionable note when it binds:
"…continues to line N — rerun with --from M", plus a new `def --from N` so the
note is real. Measured on carbon: the agent called def, got an unnumbered
body, piped it through `head -80` — which cut AT the bug line — and abandoned
the tool for 87 turns of grep. A navigation answer that stops mid-symbol is
worse than no answer because it looks complete.
The SEARCH payload's bodies stay UNNUMBERED. That is not an inconsistency: an
agent copies those verbatim as an Edit anchor and a gutter breaks the anchor,
while a navigation answer wants coordinates. Two outputs, two rules, both
pinned by test.
`def` also now prints the SOURCE of each declaration under its card. The card
answers "what can I do with this"; agents call def to see the code, and the
card alone was never the whole answer.
2. FOLLOW-UP VERB ON BODYLESS HITS. Every search hit rendered without a body
gains " [body: def <symbol>]". Measured: redis's agent hand-sed-ranged
exactly such a locator and fmt's agent blind-Read a 200-line window off
another — both had the location and neither knew the tool could hand them the
body. A hit with no symbol name gets no suffix, because naming a verb that
cannot be run is worse than silence.
3. MULTI-QUERY def. `def A B C` answers each name in sequence under a `== name ==`
separator, on ONE index build. Agents batch shell calls under the prompt's
batching rule (laravel chained three greps into one Bash call) and a one-name
tool cannot compete with that.
Byte cost of the locator suffix, measured on the default payload: 0 B on
redis-10095 (no bodyless named hits), 50 B over 2 hits on fmt-2457, 48 B over 2 on
preact-3010 — ~24 B per affected hit and nothing on hits that carry a body. The
default search payload is otherwise unchanged.
…st output
Post-last-edit confirmation is 5.8 messages per session, the largest asymmetric
pot at 5.0 messages, and every prior attempt to shrink it freed budget the agent
immediately re-spent on more verification (transfer efficiency 0.2-0.35). A
cheaper way to run tests therefore cannot close the phase; only a verdict with
nothing left to re-spend on can, and that needs three properties raw test output
does not have.
A DELTA, not a state. `--record-baseline` captures {test id -> pass|fail|error}
on the pristine tree; `--pre-edit-baseline` diffs against it afterwards. That
turns "3 tests fail" — which invites a run to find out whether they failed
before — into three named classes, one of which is PRE-EXISTING FAILURES,
labelled as not caused by the change. Without that class a caller reads a red
suite and investigates a failure that predates the edit.
A VERDICT, not evidence. One line: PASS on zero regressions and at least one
newly-passing test, REGRESSION with every id listed (the ids are the actionable
part), or NO EFFECT. Regressions outrank fixes, because a green test going red is
the fact worth acting on.
NO RAW OUTPUT, EVER. Nothing the runner printed reaches the caller — forwarding
even an excerpt reopens the loop the verb exists to close. Ids are forwarded,
text is not, and setup output is discarded before parsing so an install log
cannot contribute test ids.
Parsers (table-driven, one function each, tested against captured samples in
testdata/verify): pytest, jest/vitest, cargo test, go test, phpunit, rspec,
minitest, maven/gradle surefire, ctest. A parser that is not confident returns
NOTHING rather than a partial set — a half-read set would manufacture verdicts
out of its own parse failures, since a test missing from one side of a diff reads
as a regression. An unrecognised format degrades to an exit-code-only verdict and
says that it is coarse.
Bounds: id lists cap at 20 with a count; the whole verdict caps at --max-bytes
(2048 default), enforced from the end so the VERDICT clause and its count always
survive — and when even that clause is too wide its id list yields, never the cap.
Timeouts mirror the harness's ecosystem split (900s compiled, 300s interpreted).
The verb writes nothing and mutates nothing beyond what the caller's own command
does.
FAIRNESS: this is tool capability — it runs a supplied command and adjudicates
the result. The "verification is complete" sentence is a statement about the DATA
(a zero-regression, at-least-one-fix delta is by construction complete), not
advice about how to behave, so the control arm's harness-side stub can print the
same wording.
…are-locator windows Three independent retrieval/extraction fixes, each with a regression test proven to fail without the change. All measured on rep30 (SWE-bench Multilingual) with a separate --cache-dir per binary -- the index cache is tree-keyed, not binary-keyed, so a shared cache silently returns the first build's results. 1. TYPESCRIPT BODYLESS DECLARATIONS (parser.go, +54) `entityFromNode` gated `function_signature` and `method_signature` to Dart, on the grounds that TypeScript uses those nodes for interface members. That is only half true: tree-sitter-typescript emits `function_signature` for EVERY bodyless function declaration -- overload signatures, `declare function`, ambients, `.d.ts` -- and `method_signature` for class-body overloads as well as interface members. All were silently dropped. vue's `renderList.ts` (five overloads + one implementation) exposed 1 symbol covering only the implementation's lines; lines 10-53, the entire typed surface, were unindexed. After: 6 symbols. Narrowly gated. `method_signature` becomes a symbol ONLY inside a `class_body` (`typeScriptClassBodySignature`). Interface and type-literal members stay inventory-only on purpose: they declare a contract, not a definition, and emitting them makes every `x.m()` call name-resolvable to a bodyless interface member. Not hypothetical -- extending this to interface_body/object_type members fabricates a CALLS edge in TestTypeScriptNamespaceCallSkipsParameterReceiverRoots, where a parameter named `B` shadows a namespace and `B.parse()` then binds to `Client.parse` instead of resolving to nothing. Attribution check: symbol output is BYTE-IDENTICAL to the pre-fix binary on eight non-TS repositories (django, axum, gson, carbon, bat, caddy, fmt, rubocop). 2. TEST-FILE DEMOTION (search_testrank.go, search_confidence.go) 3 of the 10 most expensive rep30 instances had a TEST FILE as the rank-1 hit (bat -> tests/integration_tests.rs, hugo-12204 -> render_hook_integration_test.go, fmt -> test/ranges-test.cc). When the payload points at a test instead of the code, the injected location is wrong AND the derived VERIFY command scopes to the wrong place. Demotion, not exclusion -- tests stay available, they just stop taking rank 1. Measured n=30: rank-1-is-a-test 3 -> 1, gold rank unchanged 13/30, VERIFY commands changed 0/30, file lists moved on 2/30. Also carries an independent real bug fix: the confidence gap was computed signed rather than absolute. 3. BARE-LOCATOR WINDOWS (search_locator_window.go, cli/search.go) A hit with no resolvable symbol shipped as a naked `path:line` with no body and no name, so the agent had to spend a turn opening the file to find out what it was. 19 of 30 payloads contained at least one. After: 1. Text-renderer only -- ranking provably untouched, top-5 byte-identical on 30/30. Cost: +1.83% payload bytes. VERIFICATION - `go test ./internal/sem/ ./internal/cli/` green (sem 52.4s, cli 4.8s). - Each fix has a regression test verified to FAIL with the production file stashed and PASS restored. - Collateral: non-JS/TS instances re-searched, top-5 unchanged. NOT INCLUDED, deliberately. A fourth fix (same-basename path prior) was held: it buys one gold@5 but DOWNGRADES a correctly-scoped VERIFY on preact from a narrow `npx mocha test/browser/render.test.js` to a whole-suite `npm test`, taking the correctly-scoped subset 7 -> 6. A fifth (coverage-mirror route) was dropped on its own author's no-ship: VERIFY coverage 17/30 -> 17/30, and 2 of its 3 new covering-test entries named the wrong test. NO TOKEN CLAIM is attached to any of these. Three are payload-quality changes and ten such levers have already failed as causal token levers on this benchmark; these are correctness fixes, measured on retrieval metrics only. Entire-Checkpoint: 3da7aa60f961
An issue body's reproduction link carries the web application's own state, and
that state mints tokens which outrank the words the caller actually wrote.
?file=/index.js the codesandbox / StackBlitz "open this file" parameter.
Tokenizes to `index.js`, earns a code-like weight AND a path
signal on EVERY index.js in the repository.
#fragment anchor state; never a name in the tree.
Scheme, host and path are KEPT: a GitHub blob URL names a real in-repo path and
that is genuine signal. lucene's and sklearn's issue URLs do exactly this.
MEASURED, n=30 rep30, one canonical query recipe, SEPARATE --cache-dir per binary
(the index cache is tree-keyed, not binary-keyed), determinism control 30/30
byte-identical:
gold found in the delivered payload 23/30 -> 24/30
JS/TS stratum 3/4 -> 4/4
vuejs-11870 position 5 -> 2
preact-3010 absent -> 5
non-JS/TS, all 26 instances: ZERO gold ranks moved
Rejected alternatives, both measured:
Stripping the WHOLE URL scores better on the benchmark (mean rank 2.04 vs 2.25,
gold@1 +2) but is a product regression: its fallback only fires when ZERO terms
survive, so a query naming a genuine code URL literal
("where do we hardcode https://api.example.com/v1/ingest") returns NO RESULTS.
It wins the benchmark partly by deleting real evidence. Not shipped.
A declaration-class prior was also rejected: no coverage gain, it cancels part
of this fix (preact's reachable gold IS a .d.ts, which it demotes out of the
payload), it was measured on the wrong base commit, and its constant was
calibrated from gold-patch content -- and still failed its own purpose.
NO TOKEN CLAIM. This is retrieval hygiene with a reproduced mechanism, measured on
retrieval metrics only. It does NOT demonstrably fix the JS/TS token deficit:
preact's recovered file is src/index.d.ts (a one-line `| bigint` union change)
while the behavioural site src/diff/children.js remains unreachable -- `grep -rni
bigint` over the repo returns 0 hits, because the gold patch introduces the word.
Eleven-plus retrieval/payload metrics have already failed to convert into token
savings on this benchmark; treat this as correctness, not a lever.
Entire-Checkpoint: 60e2a884c54c
The branch forked at 1d7e3eb, before #75 (tree-sitter parameter names), #76 (language capability from behaviour) and #78 (grouped help) landed, so it could no longer merge. One content conflict, in internal/cli/root.go: this branch had appended `verify` and `def` to the old flat help string while main replaced that string with renderRootHelp() over the commandDocs registry in help.go. Resolved to main's renderer. That resolution exposed a real gap rather than closing one. `verify` was dispatched by Run with no help entry of any kind, and main's TestRegistryMatchesDispatch — which did not exist when `verify` was written — is what catches it. So `verify` gains a commandDoc (usage, the baseline workflow, the parser list, the no-raw-output contract) and joins the dispatch list the test reads. Nothing else conflicted; main's parameter-name and capability work merges cleanly against this branch's search and parser changes. go test ./... green: sem 53.4s, cli 5.9s, gitutil 5.0s, bench, filedigest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Zhmyygmjj71qqfXinsFur
Each of these turns a benchmark cell into a cell that never reaches the graph, and none of them is visible from outside: the exit code, the harness and the transcript all record a healthy call. A cell whose search verb stops answering does not measure a weaker graph arm, it measures the baseline arm with extra steps -- so a savings number from such a run is not low, it is void. All three reproduce; all three now fail loudly. 1. EMPTY PRE-DELIVERED PAYLOAD (search.go) `EG_PRESEARCH` pointing at a zero-byte file wrote nothing to stdout and exited 0. A missing path already errored; an empty one did not. Whatever produced the empty file -- a failed pre-computation, a truncated write, an unset variable expanding to nothing -- is a caller bug, and the only safe report is a loud one. before: exit 0, 0 bytes after: exit 1, names the variable and the file 2. A SESSION ECHO ANSWERING FOR ANOTHER REPOSITORY (search_session.go) `EG_SEARCH_SESSION` is scoped to a task BY THE CALLER, and nothing checked that claim. A harness reusing one path across a run handed every instance after the first the FIRST instance's payload, verbatim, under a header saying its question was not run -- naming files absent from the tree the agent is looking at. On a multilingual suite those are different repositories in different languages. An agent reading that concludes the tool is broken and stops calling it, for the rest of the run. The state now carries the repo and HEAD tree hash it answered for, and an echo only fires while those still match. HEAD's tree is the right key: it is what the record cache already keys on, it is stable across a task because agents edit the working tree without committing, and it differs across instances. A file from another tree is replaced rather than counted into, so the cap re-arms for the task that owns it now. Every failure degrades toward running a real search; an unscoped payload from an older state file is treated as a mismatch, not a wildcard. Cost: one repo resolution and two `git rev-parse` calls before the echo. Against replaying another repository's answer for a whole run, that is not a close trade. 3. A FLAG SET BUILT FOR A NEWER BINARY (help.go, preflight.go) `search received unexpected arguments: --callee-hop` describes the symptom, not the cause. A harness driving newer flags gets exit 1 and an empty payload on the agent's first mandated action, in every session. Flag-shaped arguments now name the version and point at --help; positional arguments keep the plain wording, since a typo is not a stale deploy. That still fails at the first call of every instance, which is the worst time to find out, so `doctor --assert "<command line>"` (repeatable) parses a command line against this binary and exits non-zero if it would be rejected -- without running anything. It calls each command's real parser rather than a list of flag names, so it cannot drift from what the command accepts, which is the drift being guarded against. VERIFICATION - go test ./... green (sem 53.6s, cli 5.4s); gofmt, go vet clean. - Each regression test verified in both directions -- production change stashed, test fails; restored, test passes. - Re-ran all three reproductions against the built binary: empty payload exits 1 with 0 bytes; a session file reused across two repositories now answers laravel from laravel instead of replaying the other tree's payload; the preflight exits 1 before any instance runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Zhmyygmjj71qqfXinsFur
This was referenced Aug 7, 2026
`impact --profile fast` reported a function with a live caller as having none.
Once every bodyless TypeScript declaration became a symbol, an ordinary
`import { renderList }` + `renderList(...)` matched three candidates in
resolveImportedCallTargets. That fanout is downgraded to confidence 0.62 /
resolution "name_only" with reason "ambiguous imported call", and the fast
profile keeps only exact/package/import_resolved edges
(shallowCallRelationRetained) — so it then dropped all three:
entire-graph impact --symbol renderList --file src/renderList.ts --line 3 --profile fast
before: IMPACT DEGENERATE: renderList has no callers, callees or type consumers
after: Blast radius: 1 caller (1 direct, 0 transitive)
- useList (src/caller.ts:3)
A false negative asserted as fact is worse than a low-confidence answer.
An overload set is not ambiguity: several signatures plus the one
implementation they declare are ONE call target. Entity/SymbolRecord now carry
a private `bodyless` flag — set where the grammar guarantees no body
(TypeScript function_signature, class-body method_signature), never for a Dart
declaration head whose body is a sibling node — and
bodylessOverloadImplementation collapses a fanout that is one implementation
plus its own bodyless declarations in the same file under the same qualified
name, keeping the confidence and reason the call resolved with (0.86,
import_resolved). Genuine ambiguity still downgrades exactly as before: two
real definitions, candidates across files or qualified names, or a set with no
implementation at all are all refused.
The flag is private, so the frozen 1.x provider schema and every compound-v1
symbol ID are unchanged. It is carried through the search snapshot cache as
BodylessSymbolIDs for the same reason LocalSymbolIDs is: the selective
derivation reruns call resolution over cached symbols, so without the sidecar
the same query would answer "1 caller" cold and "no callers" warm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2aWV3w9YndiyPDeifxfD4
Emitting a symbol for every bodyless TypeScript declaration pushed the shared
base ID over entitySymbols' `> 1` disambiguation trigger, so the branch fired
for the IMPLEMENTATION too and its published ID silently moved:
entire-graph symbols --repo . --cache-dir <fresh> --format ndjson
f6783ca (main): local/repro:TypeScript:src/renderList.ts:function:renderList
8a4ffb3: local/repro:TypeScript:src/renderList.ts:function:renderList#sig:93265eb7179ac6f2
Reproduced on all three profiles with a pristine 8a4ffb3 build, a repository
neither binary had seen, and a separate --cache-dir per binary. `.d.ts` and
class-overload variants churn the same way.
compound-v1 IDs are a contract: stable across ordinary edits, with
stable_id_version as the only signal when that changes. It still reads
compound-v1, so a consumer holding a pinned ID breaks with no version signal —
the failure this repo already documents for Swift extensions at
parser.go's swiftExtensionDeclaration note. Adding a signature above a
function is an ordinary edit.
Counting is now split: a definition collides only with other definitions, so
overload signatures can no longer rename the implementation. A bodyless
declaration yields the bare ID whenever anything else shares the name, and
keeps it when it is the only declaration of that name — a lone ambient
`declare function` gets the same ID it always had, so the fix introduces no
churn of its own. Two real definitions of one name still both disambiguate.
stable_id_version is deliberately not bumped: the point is that IDs do not
change.
NOTE FOR REVIEW: a review pass reported this regression as already fixed at
8a4ffb3, having measured both binaries as byte-identical. That measurement was
a cache artifact — the index cache is tree-keyed, not binary-keyed, so without
a per-binary --cache-dir whichever build runs FIRST answers for both. Running
main first yields the bare ID from both; running 8a4ffb3 first yields the
suffixed ID from both. 19b0769 is the commit that ADDED bodyless declarations,
not one that excludes them, and no exclusion code exists at 8a4ffb3
(`git show 8a4ffb3:internal/sem/provider.go | rg bodyless` is empty).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2aWV3w9YndiyPDeifxfD4
suhaanthayyil
added a commit
that referenced
this pull request
Aug 7, 2026
Resolves 9 conflicts across the code #82 changed, keeping both sides. #82's correctness fixes preserved verbatim: - the overload-set -> implementation collapse in call resolution (bodylessOverloadImplementation, SymbolRecord.bodyless) auto-merged intact - entitySymbols' definitionCounts/declarationCounts split, so an overloaded implementation keeps its bare compound-v1 ID - the EG_PRESEARCH zero-byte payload guard in internal/cli/search.go #84's features preserved: compact NDJSON snapshots, snapshot-query, explain, prose/session parent passages, benchmark phase/RSS accounting. Resolutions: - internal/sem/provider.go: kept #84's parallel file pipeline and re-applied #82's applyCppSpecializationAliases call inside the reducer. - internal/sem/search_verify.go: taken from main. #82 reimplemented the same ten declarations #84 added (8 byte-identical modulo comments) and explicitly documents superseding the sibling early return. #84's Go "test name read from <file>" provenance note is re-applied on top. - internal/cli/search.go, internal/sem/search.go, search_blocks.go, search_enclosure.go, help_test.go: unions -- both sides' fields, blocks and functions kept; the locator line now carries #84's symbol span and #82's [body: def NAME] follow-up. - search_cache_test.go: both tests kept as separate functions. - search_verify_test.go: unioned; two #84 assertions rewritten to #82's newer contract (the residual VERIFY floor replaced silence; the CMake suite tier is evidence-derived, so the test now pins that no target is invented).
suhaanthayyil
added a commit
that referenced
this pull request
Aug 7, 2026
…ts (#84) Merged after resolving 9 conflicts against #82 (main @ db14216). The rule applied throughout was "keep both sides' intent" — #82's correctness fixes verbatim, #84's features intact. ## What was resolved, and how **`internal/sem/provider.go`** — #84 moved the parse loop into a worker pipeline (`processProviderFile` / `runProviderFilePipeline`); main had added `applyCppSpecializationAliases` inside the old inline loop. Kept #84's pipeline and re-applied that call in the reducer, in main's original position (after the registration-alias pass, before emit). #82's three provider fixes — `SymbolRecord.bodyless`, the `definitionCounts`/`declarationCounts` split in `entitySymbols`, and `bodylessOverloadImplementation` in `resolveImportedCallTargets` — auto-merged and are byte-identical to main. **`internal/sem/search_verify.go`** — taken from main wholesale. #82 and #84 had independently written the same ten declarations (`searchVerifyRecoveredTestName`, `searchVerifyNameWords`, `searchVerifyGoTestNames`, `searchVerifyRustTestNames`, `searchVerifyBuildChecks`, `deriveSearchVerifyBuildCheck`, `deriveSearchVerifyCMake`, `searchVerifyRunner`, `searchVerifyRunnerMissing`, `searchVerifyLaunchedTool`); eight are byte-identical modulo comments, and for the two that differ main is a strict superset — its `deriveSearchVerifyBuildCheck` carries an in-code note explaining that it deliberately removed the sibling implementation's `subject.testPath != ""` early return as "one of the four measured causes of a non-derivable VERIFY". #84 introduced no declaration in this file that main lacks. Its one behaviour main did not carry — the Go derivation's `+ test name read from <file>` provenance note — is re-applied on top. Two #84 behaviours here are deliberately dropped as superseded by #82, both because #82's newer measurement inverted them: the phpunit narrow-tier suppression (main's `RunnerMissing` flag detects the same missing `vendor/bin/phpunit` and annotates instead of suppressing) and the build-check early return above. **`internal/cli/search.go`** — two unions. The bodyless-locator line now carries #84's symbol span *and* #82's `[body: def NAME]` follow-up (`"%d. %s:%d %s%s%s\n"`). The bodied result now emits #82's unit-elision note *and* #84's `writeTextSearchPassages`. #82's `EG_PRESEARCH` zero-byte guard in `echoPresearchPayload` was untouched by the merge and is intact. **`internal/sem/search.go`** — three unions: `SearchOptions` keeps #84's `VerifyExplainCommand` alongside main's `FullUnitTop`/`VerifyPrefix`/`VerifyPreFixStatus`/`CalleeHop`/`EditSiteBodies`; `SearchResult` keeps #84's `Passages` alongside main's `MergedRanks`/`UnitStartLine`/`UnitEndLine`/`CommentFocusLine`/`BodyFromReanchor`; stats keep both `VerifyExplainSuffixBytes` and `VerifyTier`. The allocator call takes main's signature (`seated, enclosures, plainEnclosures, …`) with #84's `resolvedSearchSnippetGrowth` and `resolvedSearchHeadWindowLines` resolvers. **`internal/sem/search_blocks.go`** — the verify-block cap is now main's `searchLiteralClusterCap(...)` for the literal cluster *and* #84's `searchVerifyCommandMaxBytes + stats.VerifyExplainSuffixBytes` for the verify command. **`internal/sem/search_enclosure.go`** — both sides added a function at the same offset; both kept (`largestFittingSearchHeadWindow`, `removeSearchSignal`). **`internal/cli/help_test.go`** — `dispatchCommands` is the union: #84's `snapshot-query` and `explain` plus #82's `verify`, matching root.go's switch. #82's `TestUnknownFlagNamesTheVersion` kept. **`internal/sem/search_cache_test.go`** — the two sides' tests had been spliced into one another; split back into `TestPreindexForceRebuildsDespiteValidCache` (#84) and `TestSelectiveFastSearchSnapshotPreservesCachedBodylessDeclarations` (#82). **`internal/sem/search_verify_test.go`** — unioned. Two #84 assertions were rewritten because #82 deliberately superseded the contract they pinned: `buildSearchVerifyCommand` no longer returns nil (the residual floor replaced silence, per main's own `TestBuildSearchVerifyCommandFallsBackToTheResidualFloor`), and main's CMake *suite* tier is an evidence-derived answer rather than a guess. The test now pins what it was actually protecting — that no target the sources never declared is manufactured. Also fixed one non-conflicting compile break the merge created: `search_enclosure_test.go` still called `allocateSearchSnippets` with #84's arity. ## Verification that #82's fixes survived Both run against a fresh `--cache-dir`, because the index cache is tree-keyed rather than binary-keyed and reusing one replays whichever binary ran first. Bare compound-v1 ID on an overloaded implementation — `entire-graph symbols` over a TS file with two overload signatures above one implementation: ``` local/tsrepo:TypeScript:src/renderList.ts:function:renderList#sig:df6c45cae9f06c95 | line 1 local/tsrepo:TypeScript:src/renderList.ts:function:renderList#sig:6d4ec245fbebb1db | line 2 local/tsrepo:TypeScript:src/renderList.ts:function:renderList | line 3 ``` The implementation (line 3) keeps the bare ID; only the bodyless declarations take `#sig:`. Overload-set collapse in `impact --profile fast`: ``` $ entire-graph impact --symbol renderList --file src/renderList.ts --line 3 --profile fast Impact: renderList (src/renderList.ts:3) def=3 span=3-5 [function] Blast radius: 1 caller (1 direct, 0 transitive), 0 callees, ... Callers (1 direct, 0 transitive; who breaks if behavior changes): - useList (src/caller.ts:3) ``` Reports the caller, not `IMPACT DEGENERATE`. As a negative control the pre-merge #84 head, built and run with its own fresh cache dir, emits one `renderList` symbol and 2 callers (`useList` plus a spurious `renderList.ts`) — so the check is live rather than a cache replay. `stable_id_version` still reports `compound-v1`, `SchemaVersion` is unchanged at `1.1` (const block byte-identical to main), no network imports were added, and `snapshot-query` round-trips the overload symbols out of a compact snapshot with the same IDs. ## Gate `mise run check` exit 0 — fmt, vet, `-race`, statusline 151/151, build. Full `go test ./...`: 2508 passed, 0 failed. CI green on the merge head across lint, build, test and statusline on ubuntu/macos/windows. README benchmark numbers from #84 were not re-run and are out of scope here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://entire.io/gh/entireio/entire-graph/trails/35
Supersedes #80, which is unmergeable — it forked at
1d7e3eb, before #75, #76 and #78 landed. This branch is #80's 22 commits merged onto currentmain, plus the fixes below.Why this first
Each failure here turns a benchmark cell into a cell that never reaches the graph, and none is visible from outside — the exit code, the harness and the transcript all record a healthy call. A cell whose search verb stops answering does not measure a weaker graph arm; it measures the baseline arm with extra steps. A savings number from such a run is not low, it is void.
All three reproduce against built binaries. All three now fail loudly.
1. Empty pre-delivered payload —
search.goEG_PRESEARCHpointing at a zero-byte file wrote nothing to stdout and exited 0. A missing path already errored; an empty one did not. Whatever produced the empty file — a failed pre-computation, a truncated write, an unset variable expanding to nothing — is a caller bug, and the only safe report is a loud one.2. A session echo answering for another repository —
search_session.goEG_SEARCH_SESSIONis scoped to a task by the caller, and nothing checked that claim. A harness reusing one path across a run handed every instance after the first the first instance's payload, verbatim, under a header saying its question was not run — naming files absent from the tree the agent is looking at. On a multilingual suite those are different repositories in different languages. An agent reading that concludes the tool is broken and stops calling it, for the rest of the run.The state now carries the repo and HEAD tree hash it answered for, and an echo only fires while those still match. HEAD's tree is the right key: it is what the record cache already keys on, it is stable across a task (agents edit the working tree without committing), and it differs across instances. A file from another tree is replaced rather than counted into, so the cap re-arms for the task that owns it now. Every failure degrades toward running a real search, and an unscoped payload from an older state file is treated as a mismatch rather than a wildcard.
Cost is one repo resolution and two
git rev-parsecalls before the echo — set against replaying another repository's answer for a whole run.Verified end to end: one session file across two repositories now answers laravel from
HasRelationships.phpinstead of replaying the other tree's TypeScript payload.3. A flag set built for a newer binary —
help.go,preflight.gosearch received unexpected arguments: --callee-hopdescribes the symptom, not the cause. A harness driving newer flags gets exit 1 and an empty payload on the agent's first mandated action, in every session. Flag-shaped arguments now name the version and point at--help; positional arguments keep the plain wording, since a typo is not a stale deploy.That still fails at the first call of every instance, which is the worst time to find out — so
doctor --assert "<command line>"(repeatable) parses a command line against this binary and exits non-zero if it would be rejected, without running anything: no repo read, no index build, no writes.entire graph doctor --assert "search --profile full --top-k 10 --format text"It calls each command's real parser rather than a list of flag names, so it cannot drift from what the command accepts — which is the drift being guarded against. Where a parser also enforces required flags, the preflight enforces them too; assert the command line you actually intend to run.
Also in this branch
The merge to
main. One content conflict,internal/cli/root.go: this branch had appendedverifyanddefto the old flat help string whilemainreplaced that string withrenderRootHelp()over thecommandDocsregistry. Resolved tomain's renderer — which exposed thatverifywas dispatched with no help entry at all, caught bymain'sTestRegistryMatchesDispatch(written afterverify). It now has acommandDocand is in the dispatch list.Verification
go test ./...green — sem 53.6s, cli 5.4s, gitutil 3.3s, bench, filedigest.gofmtandgo vetclean.alpha.pyreturned for the other repository).No token claim
These are correctness fixes. What they buy is that a measured cell is measuring something — they do not themselves make the graph cheaper, and no savings number is attached to them.
Generated by Claude Code