Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8d464ce
search: echo a pre-delivered payload instead of re-querying
suhaanthayyil Aug 1, 2026
5511d6f
search: merge near same-file hits into one contiguous span
suhaanthayyil Aug 1, 2026
afdb54e
search: print the matched line (focus=) in the text payload
Aug 2, 2026
afd5a1c
search: one real search per session, echo the first payload on repeats
suhaanthayyil Aug 1, 2026
6da0291
feat(search): add --full-unit-top and --edit-site-bodies editability …
suhaanthayyil Aug 3, 2026
dfb112a
feat(search): add --callee-hop, the outgoing-CALLS route to the gold …
suhaanthayyil Aug 3, 2026
8663044
fix(search): make the editability levers budget-driven and non-destru…
suhaanthayyil Aug 4, 2026
c74f647
fix(sem): scope Go receiver-method and field resolution to the call s…
suhaanthayyil Aug 4, 2026
98338c5
feat(sem): index Go interface method requirements and bind calls thro…
suhaanthayyil Aug 4, 2026
2d7b5fc
feat(sem): make C++ template specialization arguments searchable
suhaanthayyil Aug 4, 2026
ee014d3
fix(cli): answer ambiguous and misspelled symbol queries instead of r…
suhaanthayyil Aug 4, 2026
0c30774
Merge branch 'levers/graph-correctness' into levers/l1-l2-editability
suhaanthayyil Aug 4, 2026
f5c2080
fix(cli): keep fuzzy relevance order and add the answer-instead-of-re…
suhaanthayyil Aug 4, 2026
ccbce16
fix: honour the elision contract, mark degenerate impact, harden the …
suhaanthayyil Aug 4, 2026
94009ef
feat: hybrid verify ladder, mechanical low-confidence, payload diet, …
suhaanthayyil Aug 4, 2026
79efcb4
feat(verify): guard-aware derivation, honest unsatisfiable guards, PR…
suhaanthayyil Aug 4, 2026
dd10f93
feat(search): re-anchor doc-comment hits onto code and deny body slot…
suhaanthayyil Aug 4, 2026
69c2702
fix(search): never fund a re-anchored body by evicting another hit's
suhaanthayyil Aug 4, 2026
a40ba96
feat(cli): numbered navigation bodies, follow-up verbs on locators, m…
suhaanthayyil Aug 4, 2026
db24f74
feat(cli): add `entire graph verify` — an adjudicated verdict, not te…
suhaanthayyil Aug 4, 2026
19b0769
fix(sem,cli): TypeScript bodyless declarations, test-file demotion, b…
suhaanthayyil Aug 6, 2026
ecaba15
fix(search): strip URL query/fragment state from queries
suhaanthayyil Aug 6, 2026
c81ac1f
Merge main into fix/graph-retrieval-and-typescript
claude Aug 6, 2026
8a4ffb3
fix(cli): three ways search could answer with nothing, silently
claude Aug 7, 2026
800473c
fix(sem): resolve a TypeScript overload set to its implementation
Aug 7, 2026
e812688
fix(sem): keep the bare compound-v1 ID on an overloaded implementation
Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,11 @@ mise run check # fmt + vet + race tests + build
```

Contract rules that must not break: schema `1.x` is frozen and additive-only (`docs/adr/0001-ga-schema-contract.md`); the provider is **no-egress** (never add remote fetches, hosted API calls, telemetry, or runtime grammar downloads); `compound-v1` symbol IDs must stay stable across ordinary edits; unsupported/unparseable files must surface as machine-readable partial failures, never silent drops. All logic lives under `internal/` (`sem` = parsing/graph/search, `cli` = hand-rolled dispatch, `gitutil` = git subprocess); `cmd/entire-graph/main.go` is a thin entry point. The plugin manifest (`entire-plugin.yml`) registers the subcommand `graph`, so users type `entire graph ...`. This project was **previously named `entire-sem`** — do not reintroduce the old name. **Entire Brain** (`entire-brain`) is the separate downstream consumer of this provider's NDJSON — not an old name for this project.

<!-- entire-graph:begin -->
This repo has the entire-graph code graph installed. Before exploring code with
grep/find/whole-file reads, read .entire/graph-agent.md — the search-first, verify-once
doctrine for coding agents: search instead of grepping, then check the sibling sites and
compile (or run the nearest existing test) once before you finish.
@.entire/graph-agent.md
<!-- entire-graph:end -->
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
@AGENTS.md

<!-- entire-graph:begin -->
This repo has the entire-graph code graph installed. Before exploring code with
grep/find/whole-file reads, read .entire/graph-agent.md — the search-first, verify-once
doctrine for coding agents: search instead of grepping, then check the sibling sites and
compile (or run the nearest existing test) once before you finish.
@.entire/graph-agent.md
<!-- entire-graph:end -->
24 changes: 19 additions & 5 deletions internal/cli/completeness.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,25 @@ func writeScopedCompletenessBlock(
if level == "" {
level = "degraded"
}
fmt.Fprintf(out, "Completeness: %s for %s (%d of %d %s file%s failed to parse; %d total diagnostic%s in this snapshot)\n",
level, scopeLanguageLabel(scope),
scope.LanguageFailed, scope.LanguageFiles, scopeLanguageLabel(scope), pluralSuffix(scope.LanguageFiles),
len(warnings)+len(partialFailures), pluralSuffix(len(warnings)+len(partialFailures)),
)
// A FRACTION only when the denominator is real. `LanguageFiles` counts files that parsed, so a
// language whose files all failed reports 0 — and "35 of 0 files failed to parse" (three.js),
// "6 of 0" (terraform) is not a small error, it is a number that cannot be true and it discredits
// every other count on the line. When the denominator is missing or smaller than the numerator, the
// honest form is the count alone.
total := len(warnings) + len(partialFailures)
if scope.LanguageFiles <= 0 || scope.LanguageFailed > scope.LanguageFiles {
fmt.Fprintf(out, "Completeness: %s for %s (%d %s file%s failed to parse; %d total diagnostic%s in this snapshot)\n",
level, scopeLanguageLabel(scope),
scope.LanguageFailed, scopeLanguageLabel(scope), pluralSuffix(scope.LanguageFailed),
total, pluralSuffix(total),
)
} else {
fmt.Fprintf(out, "Completeness: %s for %s (%d of %d %s file%s failed to parse; %d total diagnostic%s in this snapshot)\n",
level, scopeLanguageLabel(scope),
scope.LanguageFailed, scope.LanguageFiles, scopeLanguageLabel(scope), pluralSuffix(scope.LanguageFiles),
total, pluralSuffix(total),
)
}
for _, warning := range scope.InScopeWarnings {
if warning.FilePath == "" {
fmt.Fprintf(out, "- warning %s\n", warning.Code)
Expand Down
6 changes: 5 additions & 1 deletion internal/cli/completeness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ func TestScopedCompletenessItemizesInScopeFailures(t *testing.T) {
rendered := out.String()
for _, want := range []string{
"degraded for Rust",
"5 of 2 Rust file",
// The fixture has 5 failures against 2 successfully-parsed files, so there IS no honest
// fraction: "5 of 2 ... failed to parse" is a number that cannot be true and it discredits
// every other count on the line (measured as "35 of 0" on three.js, "6 of 0" on terraform).
// The count alone is the truthful form.
"5 Rust files failed to parse",
"broken_a.rs",
"... 2 more in Rust",
"plus 273 diagnostics in other languages",
Expand Down
143 changes: 118 additions & 25 deletions internal/cli/def.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
Expand Down Expand Up @@ -50,8 +51,11 @@ const (
)

type defFlags struct {
Repo string
Symbol string
Repo string
Symbols []string
Symbol string
// From is the line a clipped body resumes at, so the "--from N" the resume note prints is real.
From int
File string
Line int
Kind string
Expand Down Expand Up @@ -118,12 +122,16 @@ type defDeclaration struct {
}

type defResponse struct {
FormatVersion int `json:"format_version"`
RepoRoot string `json:"repo_root"`
Commit string `json:"commit,omitempty"`
Tree string `json:"tree,omitempty"`
Profile string `json:"profile,omitempty"`
Query string `json:"query"`
FormatVersion int `json:"format_version"`
RepoRoot string `json:"repo_root"`
Commit string `json:"commit,omitempty"`
Tree string `json:"tree,omitempty"`
Profile string `json:"profile,omitempty"`
Query string `json:"query"`
// FuzzyMatchKind names the rung of the fuzzy ladder these declarations came from, empty on an
// exact match. See resolveFocusSymbolsOrFuzzy: `def` answers a misspelled name rather than
// returning nothing.
FuzzyMatchKind string `json:"fuzzy_match_kind,omitempty"`
Declarations []defDeclaration `json:"declarations"`
DeclarationTotal int `json:"declarations_total"`
Truncated bool `json:"truncated"`
Expand Down Expand Up @@ -166,21 +174,67 @@ func runDef(ctx context.Context, opts Options, args []string) error {
}
indexLatency := time.Since(totalStarted)
queryStarted := time.Now()
response := buildDefResponse(snapshot, flags)
response.IndexCacheHit = cacheHit
response.IndexLatencyMS = indexLatency.Milliseconds()
response.QueryLatencyMS = time.Since(queryStarted).Milliseconds()
response.TotalLatencyMS = time.Since(totalStarted).Milliseconds()
switch flags.Format {
case "json":
encoder := json.NewEncoder(opts.Stdout)
encoder.SetEscapeHTML(false)
return encoder.Encode(response)
case "text", "agent":
return writeDefText(opts.Stdout, response, flags.MaxContextBytes)
default:
return fmt.Errorf("def --format must be json, text, or agent, got %q", flags.Format)
symbols := flags.Symbols
if len(symbols) == 0 {
symbols = []string{flags.Symbol}
}
// One index build, N answers. The whole point of the multi-query form is that the second and third
// name cost a map lookup rather than another 10-second snapshot load.
for position, symbol := range symbols {
query := flags
query.Symbol = symbol
response := buildDefResponse(snapshot, query)
response.IndexCacheHit = cacheHit
response.IndexLatencyMS = indexLatency.Milliseconds()
response.QueryLatencyMS = time.Since(queryStarted).Milliseconds()
response.TotalLatencyMS = time.Since(totalStarted).Milliseconds()
switch flags.Format {
case "json":
encoder := json.NewEncoder(opts.Stdout)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(response); err != nil {
return err
}
case "text", "agent":
if position > 0 {
// A separator, because several cards in one stream have to be tellable apart.
fmt.Fprintln(opts.Stdout)
}
if len(symbols) > 1 {
fmt.Fprintf(opts.Stdout, "== %s ==\n", symbol)
}
if err := writeDefText(opts.Stdout, response, flags.MaxContextBytes); err != nil {
return err
}
writeDefBodies(opts.Stdout, response, repo, query.From)
default:
return fmt.Errorf("def --format must be json, text, or agent, got %q", flags.Format)
}
}
return nil
}

// writeDefBodies prints the SOURCE of each declaration the card describes, numbered.
//
// The card answers "what can I do with this"; agents call `def` to answer "show me the code". Measured
// on carbon: the agent got a body it could not navigate, cut it with `head -80`, lost the line it
// needed and spent 87 turns grepping instead. The card alone was never the whole answer.
func writeDefBodies(out io.Writer, response defResponse, repoRoot string, from int) {
if len(response.Declarations) == 0 || repoRoot == "" {
return
}
records := make([]sem.SymbolRecord, 0, len(response.Declarations))
for _, declaration := range response.Declarations {
start := declaration.StartLine
if from > start {
start = from
}
records = append(records, sem.SymbolRecord{
Name: defDisplayName(declaration), Kind: declaration.Kind,
FilePath: declaration.FilePath, StartLine: start, EndLine: declaration.EndLine,
})
}
writeSymbolMatchBodies(out, symbolMatchBodies(repoRoot, records, len(records)))
}

func parseDefFlags(args []string) (defFlags, error) {
Expand All @@ -207,6 +261,16 @@ func parseDefFlags(args []string) (defFlags, error) {
flags.File, err = value()
case "--kind":
flags.Kind, err = value()
// --from N resumes a body the 400-line cap clipped. It is the invocation the resume note prints,
// so the note is actionable rather than merely apologetic.
case "--from":
var raw string
if raw, err = value(); err == nil {
flags.From, err = strconv.Atoi(raw)
if err != nil || flags.From < 0 {
return flags, fmt.Errorf("def --from requires a non-negative integer, got %q", raw)
}
}
case "--format":
flags.Format, err = value()
case "--profile":
Expand Down Expand Up @@ -249,10 +313,13 @@ func parseDefFlags(args []string) (defFlags, error) {
if strings.HasPrefix(arg, "-") {
return flags, fmt.Errorf("def received unexpected argument %q", arg)
}
if flags.Symbol != "" {
return flags, fmt.Errorf("def takes one name, got %q and %q", flags.Symbol, arg)
// MULTI-QUERY. Agents batch shell calls under the prompt's batching rule — laravel chained
// three greps into one Bash call — and a tool that answers one name per invocation cannot
// compete with that. `def A B C` returns each body in sequence.
flags.Symbols = append(flags.Symbols, arg)
if flags.Symbol == "" {
flags.Symbol = arg
}
flags.Symbol = arg
}
if err != nil {
return flags, err
Expand Down Expand Up @@ -304,6 +371,7 @@ func buildDefResponse(snapshot sem.ProviderSnapshot, flags defFlags) defResponse
}
index := newDefIndex(snapshot)
matches := index.resolve(flags)
response.FuzzyMatchKind = index.fuzzyKind
groups := index.groupPartials(matches)
response.DeclarationTotal = len(groups)
if len(groups) > defDeclarationLimit {
Expand Down Expand Up @@ -333,6 +401,10 @@ type defIndex struct {
symbols []sem.SymbolRecord
filePaths []string
repoRoot string
// fuzzyKind names the rung of the fuzzy ladder that produced the matches, empty when the exact
// lookup succeeded. It is set by resolve and reported so the caller knows it did not get what it
// literally asked for.
fuzzyKind string
}

type defOwnedMember struct {
Expand Down Expand Up @@ -442,6 +514,21 @@ func (index *defIndex) resolve(flags defFlags) []sem.SymbolRecord {
break
}
}
// FIX B: every spelling of the name missed, so degrade to the fuzzy ladder rather than answering
// "(no symbol named X)". The ref is rebuilt from the caller's ORIGINAL spelling: the qualified-name
// forms above are exact-match aids and would only narrow the fuzzy search.
if len(matches) == 0 {
ref := parseSymbolRef(flags.Symbol, flags.File, flags.Line, flags.Kind, index.repoRoot, index.filePaths)
if fuzzy, tier, ok := resolveFocusSymbolsOrFuzzy(index.symbols, ref, symbolFuzzyCandidateLimit); ok {
index.fuzzyKind = tier.label()
matches = fuzzy
}
}
// A fuzzy answer keeps the relevance order resolveFocusSymbolsOrFuzzy produced; only exact matches
// are re-sorted positionally. See the same reasoning in buildNeighborResponse.
if index.fuzzyKind != "" {
return matches
}
sort.Slice(matches, func(left, right int) bool {
if matches[left].FilePath != matches[right].FilePath {
return matches[left].FilePath < matches[right].FilePath
Expand Down Expand Up @@ -712,6 +799,12 @@ func renderDefText(response defResponse, limit int) []byte {
writeNoFocusMatch(&buffer, response.Query, "", 0)
return []byte(buffer.String())
}
// A fuzzy answer says so, once, above the cards. Silently returning a different symbol's
// declaration than the one asked for would be worse than the empty answer this replaced.
if response.FuzzyMatchKind != "" {
fmt.Fprintf(&buffer, "No exact match for %q; showing the closest %d by %s match.\n",
response.Query, len(response.Declarations), response.FuzzyMatchKind)
}
for index, declaration := range response.Declarations {
if index > 0 {
buffer.WriteByte('\n')
Expand Down
Loading