Documentation
¶
Overview ¶
Package query — internal/query/expand.go implements the subgraph-construction heuristics H10-H12 (RESEARCH §C.2, D-10): the DoS-bounding stage (T-01-18) that turns explore's gathered candidate set (H3-H6, gather.go) into the bounded subgraph computeGraphRelevance (rwr.go) later ranks over. The live TS dist is no longer readable on this machine (see gather.go's package doc comment) — every constant below is cited from the frozen 01-RESEARCH.md §C.2 capture, not re-derived from a fresh source read:
- H10 Type-hierarchy expansion — context/index.js:921-955; traversal.js:332-380
- H11 BFS traversal bounds — mcp/tools.js:2422-2427
- H12 Glue-node injection — mcp/tools.js:2439-2467
A later plan (16, "wire time") composes expandTypeHierarchy, expandBFS, and expandGlueNodes into Explore()'s subgraph-gathering pipeline; this plan lands the three primitives as pure, graphstore.Reader-driven functions, independently unit-testable against a synthetic index (mirroring gather.go's/traverse.go's own fresh-per-call, no-cache discipline throughout).
Package query — internal/query/explore_gate.go implements the final file selection+ordering stage of the explore pipeline: H17 (the EXPL-03 relevance gate), H18 (the 5-tier file sort), and H19 (central-file selection). RESEARCH §C.2/§4 (cited from the frozen RESEARCH capture):
- H17 Relevance GATE — mcp/tools.js:2763-2783
- H18 5-tier file sort — mcp/tools.js:2823-2863
- H19 Central-file selection — mcp/tools.js:2716-2720
RESEARCH's single most emphasized pitfall for this stage: H17 is a 5-way boolean OR, NOT a bare `fileGraphScore >= maxGraph*0.06` threshold (D-08). A single-clause implementation under-selects files the full rule would keep (e.g. a named-by-agent file with near-zero RWR mass). Every clause below is implemented as an independently-sufficient check, preserving the full `||` chain.
Division of labor / D-02 scoping note (same precedent as scoring.go): RESEARCH's frozen citations pin H17-H19's constants and rule STRUCTURE, but the functions below accept the upstream per-file score/flag maps (plan 13's fileScores/fileGraphScore/rescued, plus a caller-supplied fileTermHits and fileNodeCounts) as parameters rather than deriving them internally — the actual wiring into Explore() is a later plan's job, mirroring expand.go's/scoring.go's own "primitives now, wiring later" discipline.
Package query — internal/query/gather.go implements the hybrid candidate-gathering heuristics H3-H6 (RESEARCH §C.2, context/index.js:449-606 [cited from the frozen 01-RESEARCH.md capture — the original source is no longer present on this machine, only its .d.ts type declarations remain, so this file works from RESEARCH's pinned constants rather than a fresh source read]): three independently- scored channels feeding explore's RWR candidate set, merged max-score-wins. This REPLACES the naive lexical matchNodes as explore's input construction (RESEARCH Pitfall 1) — a later plan (10) wires this into Explore() and extends this file with H7+ rerankers; this plan lands only H3-H6 plus the shared isTestFile path predicate H7 (plan 10) and the relevance gate (plan 14) both reuse.
Package query (this file): the SURF-06 markdown renderers for the 5 JSON-shaped MCP read tools (callers/callees/impact/search/files).
This file is strictly ADDITIVE (D-16). Every one of the corresponding Marshal*JSON helpers (traverse.go, files.go) is SHARED with the CLI --json path AND is testdata/golden's shape oracle — e.g. MarshalCallersJSON is called from both internal/cli/callers.go and internal/mcp/tools.go. Mutating one of those bodies to emit markdown would silently break the CLI contract and the behavioral golden suite simultaneously. So after this phase each helper family has exactly one caller per surface: Marshal*JSON is called ONLY by internal/cli (the --json contract and behavioral_test.go's shape oracle); Render* (this file) is called ONLY by internal/mcp (whose consumer is a language model, not a parser — nothing unmarshals MCP text content). That asymmetry is intentional, not an oversight — do not "helpfully" reunify the two families.
Package query is the read-only engine over a frozen internal/graphstore GraphStore (D-02): one Snapshot per invocation, never a write path. CLI commands and the MCP server share this single engine (D-08b) so their output shapes cannot drift into two code paths.
Package query — internal/query/rwr.go implements EXPL-02's load-bearing algorithm: computeGraphRelevance, the documented Random-Walk-with-Restart (RWR) relevance ranker (RESEARCH §3, mcp/tools.js:2321-2386 [cited from the frozen RESEARCH capture]). This file is pure — no graphstore.Reader dependency — so it is fully unit-testable on synthetic in-memory subgraphs.
Package query — internal/query/scoring.go implements the per-file scoring, hard-exclusion, and change-surface buried-rescue heuristics H14-H16 (RESEARCH §C.2, D-10): the stage that converts node-level RWR mass (plan 06, rwr.go) plus the file candidate set into the per-file "score" the relevance gate (H17, plan 14) filters/sorts on. The original source is no longer readable on this machine (see gather.go's package doc comment) — every constant below is cited from the frozen 01-RESEARCH.md §C.2 capture, not re-derived from a fresh source read:
- H14 Per-file score tiers — mcp/tools.js:2632-2647
- H15 Hard test/spec exclusion — mcp/tools.js:2652-2684
- H16 Change-surface buried-rescue — mcp/tools.js:2574-2613, 2733-2762
Division of labor / D-02 scoping note: RESEARCH's frozen citations pin H14-H16's constants and rule STRUCTURE, but not the exact upstream wiring of "which node ids are named-seed/entry/tier-seed" or "how fileTermHits is computed" — those come from earlier pipeline stages (H11's BFS roots via expand.go, H13's named-symbol seed tiers via seeding.go, and H5's per-term-hit tracking via gather.go) that a LATER wiring plan (per expand.go's own "primitives now, wiring later" precedent) composes together. This file's functions therefore accept those sets/maps as caller-supplied parameters rather than re-deriving them, keeping scoring.go a pure, Reader-driven primitive independently unit-testable against a synthetic index — mirroring expand.go's/ seeding.go's own discipline.
Package query — internal/query/seeding.go implements the named-symbol seeding heuristic H13 (RESEARCH §C.2, mcp/tools.js:2477-2562 [cited from the frozen 01-RESEARCH.md capture]): the stage that resolves the agent's named query symbols into RWR seeds and gives their files the dominant +50 score (plan 13, H14). Feeds directly off extractSymbolsFromQuery (H1, tokenize.go) and computeGraphRelevance's seedIDs restart vector (rwr.go).
H13's rule set, transcribed from the RESEARCH §C.2 row:
- tokenize the query again (H1), keep only tokens >=3 chars, capped at the first 16 (in scan order)
- resolve each token via a full-scan exact-name lookup (getNodesByName — NOT the FTS/gather channels H3-H6 use)
- <=3 defs for a name: INJECT ALL of them into the RWR seed set; the "seed tier" (the subset plan 13's +50 named-seed file score keys off) is def0 (the D-04 lowest-Id def, substituting for the documented unordered SELECT per Assumption A3) plus any OTHER co-named def whose caller count is >= 0.25*maxCallers among that name's defs
- >3 defs for a name: only the disambiguated subset is injected (and IS the seed tier, no further split) — PascalCase type tokens from the query (excluding the project name) corroborate up to 4 defs by matching a def's OWNING type's name (via the contains index, traverse.go's buildContainsIndex); if none corroborate, the single def with the greatest "body substance" wins
Divergence (D-02, no verbatim source survives for these specifics — the original source is no longer readable on this machine, see gather.go's package doc comment for the same constraint): the RESEARCH capture pins H13's constants and branch structure but not (a) the exact "body-substance" measure the documented design uses to rank a large-overload def with no corroborating type token, or (b) the exact mechanism it uses to correlate a PascalCase type token with an overloaded def. This plan's own, documented design:
- body substance = a def's own line span (EndLine-StartLine+1) — a cheap, Reader-only proxy for "how much implementation a def contains" without a second disk read (this function stays a pure graphstore.Reader-driven algorithm, mirroring rwr.go/expand.go/ gather.go's discipline)
- corroboration = a def's OWNING type (the type that "contains" it, per traverse.go's buildContainsIndex) has a Name matching one of the query's PascalCase type tokens. A def with no owning type (a free function, not a method) never corroborates via a type token — it can only win via the top-1-by-substance fallback. Deliberately does NOT also match a def's own Name against the type-token set: the resolved query token itself is frequently PascalCase-shaped (e.g. "Process"), which would otherwise trivially self-corroborate every def sharing that name and defeat the disambiguation entirely.
- project name = the caller-supplied projectName string (typically filepath.Base(repoRoot)), excluded case-insensitively from the PascalCase type-token set before corroboration runs.
Index ¶
- Constants
- Variables
- func BuildImplementsIndex(r graphstore.Reader) (map[string][]*schema.Edge, error)
- func BuildReverseAdjacency(r graphstore.Reader) (map[string][]*schema.Edge, error)
- func DenseEdgesByKind(r StatusResult) map[string]int64
- func MarshalAffectedJSON(r AffectedResult) ([]byte, error)
- func MarshalCalleesJSON(r CalleesResult) ([]byte, error)
- func MarshalCallersJSON(r CallersResult) ([]byte, error)
- func MarshalFilesJSON(r FilesResult) ([]byte, error)
- func MarshalImpactJSON(r ImpactResult) ([]byte, error)
- func MarshalQueryJSON(nodes []*schema.Node) ([]byte, error)
- func MarshalStatusJSON(r StatusResult) ([]byte, error)
- func RenderCalleesMarkdown(r CalleesResult) string
- func RenderCallersMarkdown(r CallersResult) string
- func RenderExplore(query string, fileCount, symbolCount int, groups []exploreFileGroup, ...) string
- func RenderFilesMarkdown(r FilesResult) string
- func RenderImpactMarkdown(r ImpactResult) string
- func RenderNode(n *schema.Node, calls, calledBy []*schema.Node) string
- func RenderNodeMultiDef(symbol string, matches []*schema.Node, fetch nodeSectionFetch) (string, error)
- func RenderSearchMarkdown(term string, locs []Location) string
- func RenderStatusMarkdown(r StatusResult) string
- func RenderStatusText(r StatusResult, projectPath string) string
- func ResolveCodegraphDir(start string) (string, error)
- func ValidateAffectedFiles(n int) error
- func ValidateKind(kind string) error
- func WorktreeNotice(m *gitmeta.Mismatch) string
- func WorktreeWarningBlockquote(m *gitmeta.Mismatch) string
- type AffectedResult
- type CalleesResult
- type CallersResult
- type Engine
- func (e *Engine) Affected(files []string, depth int) (AffectedResult, error)
- func (e *Engine) Callees(symbol string, limit int) (CalleesResult, error)
- func (e *Engine) Callers(symbol string, limit int) (CallersResult, error)
- func (e *Engine) Explore(query string, maxFiles int) (string, error)
- func (e *Engine) Files(opts FilesOptions) (FilesResult, error)
- func (e *Engine) Impact(symbol string, depth int) (ImpactResult, error)
- func (e *Engine) Node(symbol, file string, line *int) (string, error)
- func (e *Engine) Query(term, kind string, limit int) ([]*schema.Node, error)
- func (e *Engine) Search(term, kind string, limit int) ([]Location, error)
- func (e *Engine) Status(ctx context.Context) (StatusResult, error)
- func (e *Engine) UseDetector(d *gitmeta.CachingDetector)
- func (e *Engine) WorktreeMismatch(ctx context.Context) *gitmeta.Mismatch
- type ExpandBFSBounds
- type FileEntry
- type FileTreeNode
- type FilesOptions
- type FilesResult
- type ImpactResult
- type IndexHealth
- type Location
- type PendingChanges
- type StatusResult
Constants ¶
const ( ExpandMaxNodes = 200 ExpandTraversalDepth = 3 ExpandMinScore = 0.2 ExpandSearchLimit = 8 GlueNodeCap = 60 )
H11's explore-override bounds and H12's cap (RESEARCH §C.2, mcp/tools.js:2422-2427 / 2439-2467 [cited from the frozen RESEARCH capture]) are implemented here with the same override values. These are explore's OWN overrides of the more permissive library defaults; this file carries only the override values, since explore is the only caller these primitives currently serve.
const ( // MaxDepth bounds impact/affected BFS depth. 50 comfortably exceeds // any realistic call-chain depth in a real codebase while keeping a // worst-case traversal small relative to the ≈4k-edge golden corpus // scale this phase targets (CONTEXT D-04). MaxDepth = 50 // MaxLimit bounds how many result rows query/search/callers/callees // may return in one call. MaxLimit = 1000 // MaxFiles bounds explore's per-call file-read fan-out. MaxFiles = 1000 // MaxAffectedFiles bounds how many changed-file paths `affected // --stdin` (an explicitly untrusted input surface, SURF-04/ // T-08-05-01) may ingest before Engine.Affected ever runs its bounded // graph scan (CR-01, T-03-02-DoS). Deliberately its own constant // rather than reusing MaxFiles — MaxFiles bounds Explore's per-call // file-READ fan-out (an expensive per-file verbatim-source read); // affected's stdin path is a cheap string-dedup/BFS-seed operation // that can tolerate a much larger ceiling before becoming a DoS // concern, so the two must not be silently coupled. MaxAffectedFiles = 10000 )
Documented ceilings for the numeric flags every query command exposes (--depth/--limit/--max-files). These bound BFS/scan/allocation work before it starts (V5 Input Validation, RESEARCH Pitfall 4, T-03-02-DoS): a caller — human or an untrusted/compromised MCP client — cannot force an unbounded traversal or allocation just by passing a large number.
Variables ¶
var DefaultExploreBFSBounds = ExpandBFSBounds{ MaxNodes: ExpandMaxNodes, TraversalDepth: ExpandTraversalDepth, MinScore: ExpandMinScore, SearchLimit: ExpandSearchLimit, }
DefaultExploreBFSBounds bundles H11's four override constants above into the ExpandBFSBounds shape expandBFS consumes.
var ErrNotInitialized = errors.New("query: not initialized")
ErrNotInitialized mirrors the internal/cli sentinel of the same name (see internal/cli/root.go) but is declared locally so internal/query has no reason to import internal/cli (which would invert the intended CLI-depends-on-query dependency direction). Callers compare against this sentinel via errors.Is/errors.As, not string matching.
var RankEdges = map[string]bool{ goextract.RefKindCalls: true, goextract.RefKindReferences: true, goextract.EdgeKindExtends: true, goextract.EdgeKindImplements: true, goextract.EdgeKindOverrides: true, goextract.RefKindInstantiates: true, goextract.RefKindReturns: true, goextract.RefKindTypeOf: true, goextract.RefKindImports: true, }
RankEdges is the Go RANK_EDGES-equivalent set (RESEARCH §C.1) — plain Set membership only, undirected and UNWEIGHTED (no per-kind weights, despite the phase description's phrasing — D-09 confirmed). Sourced from goextract's shared RefKind*/EdgeKind* constants (one definition, never re-declared literals, mirroring goextract's own discipline).
Functions ¶
func BuildImplementsIndex ¶
BuildImplementsIndex builds an in-memory index of "implements" edges keyed by edge.Target (the interface node), from one full IterateEdges("") scan — mirrors BuildReverseAdjacency's shape exactly (same fresh-per-call discipline, no package-level cache/sync.Once), but is a SEPARATE, purpose-built index (RES-02/D-06): dispatch traversal is name-joined (an interface method's callers must also reach every concrete implementation's SAME-NAMED method), not identity-followed like a "calls" edge, so BuildReverseAdjacency's goextract.RefKindCalls -only filter is deliberately NOT widened to admit "implements" edges — this is new, separate traversal code (per 05-PATTERNS.md).
func BuildReverseAdjacency ¶
BuildReverseAdjacency builds an in-memory reverse-adjacency map keyed by edge.Target, from one full IterateEdges("") scan (D-04). It is filtered to goextract.RefKindCalls only — callers/impact/affected are call-graph traversals, and the golden callers.json/callees.json/ impact.json shapes contain only call targets, never contains/embeds/ imports edges, so a raw unfiltered scan would leak unrelated relationships into caller/blast-radius results.
This is built fresh inside every caller (Callers/Impact/Affected) — no package-level cache, no sync.Once (RESEARCH Pitfall 2 / T-03-04-Stale): a long-lived process (the future MCP server) must never serve a stale point-in-time reverse view across multiple calls, even though Phase 3's CLI invocations are one-scan-per-process anyway. Exported (Phase 4, 04-02) so internal/indexer.Sync() can reuse this exact scan for dependent-file detection (D-02a) — callers outside this package MUST follow the same fresh-per-call discipline: never cache the result across a Sync() invocation.
func DenseEdgesByKind ¶ added in v0.11.0
func DenseEdgesByKind(r StatusResult) map[string]int64
DenseEdgesByKind returns a NEW map carrying every RankEdges (rwr.go) member with an explicit value — r.EdgesByKind's count where present, an explicit 0 where absent — so "absent" (unmeasured) and "measured zero" are never confusable (D-04). The key set is DERIVED by ranging over RankEdges, never hand-listed: a future 10th ranked edge kind is picked up automatically instead of needing a matching edit here. Any entry in r.EdgesByKind whose key is NOT a RankEdges member (an unranked kind, e.g. "contains") is copied across unchanged, so the result is the union of RankEdges and r.EdgesByKind's keys — an unranked kind is never silently dropped. r.EdgesByKind itself is never mutated.
func MarshalAffectedJSON ¶
func MarshalAffectedJSON(r AffectedResult) ([]byte, error)
func MarshalCalleesJSON ¶
func MarshalCalleesJSON(r CalleesResult) ([]byte, error)
MarshalCalleesJSON, MarshalCallersJSON, MarshalImpactJSON, and MarshalAffectedJSON colocate --json shaping with the traversal methods that produce these results (matching search.go's MarshalQueryJSON convention, 03-03) — each result struct is already tagged to its golden shape, so marshaling is a thin passthrough.
func MarshalCallersJSON ¶
func MarshalCallersJSON(r CallersResult) ([]byte, error)
func MarshalFilesJSON ¶
func MarshalFilesJSON(r FilesResult) ([]byte, error)
MarshalFilesJSON renders a FilesResult as --json output. There is no golden fixture for files (D-07a) — this is this plan's own designed shape, marshaled as a thin passthrough matching search.go/traverse.go's Marshal* convention.
func MarshalImpactJSON ¶
func MarshalImpactJSON(r ImpactResult) ([]byte, error)
func MarshalQueryJSON ¶
MarshalQueryJSON renders nodes as the golden query --json shape: a top-level array of {"node": {...}} envelopes (D-05). Marshaling is deterministic given the same input slice — encoding/json orders struct fields by declaration, and Query/Search already return a stably ranked, tie-broken slice — so two consecutive calls for the same query produce byte-identical output (D-06).
func MarshalStatusJSON ¶
func MarshalStatusJSON(r StatusResult) ([]byte, error)
MarshalStatusJSON renders a StatusResult as --json output, matching search.go/traverse.go's Marshal* convention.
func RenderCalleesMarkdown ¶
func RenderCalleesMarkdown(r CalleesResult) string
RenderCalleesMarkdown renders a CalleesResult as markdown, mirroring RenderCallersMarkdown's shape for call targets instead of call sites.
func RenderCallersMarkdown ¶
func RenderCallersMarkdown(r CallersResult) string
RenderCallersMarkdown renders a CallersResult as markdown: a bolded header naming the symbol and caller count, then the shared location table. An empty result renders an explicit "no callers" sentence naming the symbol instead of a headerless table — a bare table header with zero rows reads as a rendering bug to a model.
func RenderExplore ¶
func RenderExplore(query string, fileCount, symbolCount int, groups []exploreFileGroup, blasts []exploreBlast, sources map[string][]byte, stale bool, skeletonFiles map[string]bool) string
RenderExplore reproduces the golden explore.json markdown shape byte-for-byte in its fixed regions (D-05a): the exploration header, the "Found N symbol(s) across M file(s)." line, the blast-radius bullets, the verbatim-source disclaimer, and one "**`path`** — sym(kind), ..." header + fenced source block per matched file, in groups' order. When stale is true (D-04a), a single bolded staleness line is prepended before the exploration header; a current graph (stale=false) prepends nothing, keeping the golden's fixed section order untouched. skeletonFiles (H20, may be nil) renders a file's SIGNATURES ONLY (renderSkeleton) instead of its full verbatim source (renderNumberedSource) — an off-spine file whose classes share a >=3-implementer supertype.
func RenderFilesMarkdown ¶
func RenderFilesMarkdown(r FilesResult) string
RenderFilesMarkdown renders a FilesResult as markdown. FilesResult is a UNION — exactly one of Files (flat format) or Tree (tree format) is populated, per Format — so, unlike the four Location-backed renderers above, this one branches on shape rather than sharing a single table.
Both the empty string and the literal "flat" value are treated as the flat branch, matching FilesOptions.Format's documented "empty means flat" default and the MCP files tool's own req.GetString("format", "") default.
Note: the documented MCP files tool defaults to the tree format; ours defaults to flat. That is a PRE-EXISTING divergence, not introduced by this plan — do not "fix" it here; it is Phase 8 SURF territory.
func RenderImpactMarkdown ¶
func RenderImpactMarkdown(r ImpactResult) string
RenderImpactMarkdown renders an ImpactResult as markdown. Unlike the other three Location-backed renderers, its header additionally carries Depth/NodeCount/EdgeCount — the scalars ImpactResult has that have no place in a per-row table — mirroring the documented bolded-key style.
func RenderNode ¶
RenderNode reproduces the golden node.json markdown shape byte-for-byte in its fixed regions (D-05b): "**name** (kind)", a blank line, "**Location:**", "**Signature:**", the fixed Trail line, "**Calls →**" (forward edges), and "**Called by ←**" (reverse edges) — each entry list comma-joined via joinNodeRefs.
func RenderNodeMultiDef ¶
func RenderNodeMultiDef(symbol string, matches []*schema.Node, fetch nodeSectionFetch) (string, error)
RenderNodeMultiDef reproduces the documented multi-def node markdown shape byte-for-byte (NODE-02, RESEARCH §8, Pitfall 4): the "**N definitions named "X"**" header line, immediately followed (single newline, NOT a blank line — Pitfall 4) by the "Returning M in full[; K more listed below] — pick the one you need (no Read required)." line, a blank line, then up to HARD_CAP full bodies (renderNodeSection) joined by "\n\n---\n\n" and capped at a BODY_BUDGET char budget (always rendering at least the first, regardless of budget). When any definitions overflow the cap or budget, a "**Other definitions**" list follows — capped at LIST_CAP entries with a trailing "+K more" line beyond that — plus a closing "Need one of these in full?" hint.
func RenderSearchMarkdown ¶
RenderSearchMarkdown renders search results as markdown. It takes the raw []Location slice (rather than a wrapper result struct) because Engine.Search returns []Location directly with no wrapper — the same reason search has no MarshalXJSON helper of its own.
func RenderStatusMarkdown ¶
func RenderStatusMarkdown(r StatusResult) string
RenderStatusMarkdown renders StatusResult in the documented MCP bolded-key bullet-list shape (D-17), built to the documented mcp/tools.js ~3890-3945 shape. Called ONLY by internal/mcp (plan 02-06) — see this file's header comment for why a second, structurally different renderer exists for the CLI (RenderStatusText).
The verbose worktree warning is embedded via WorktreeWarningBlockquote (worktree_notice.go) rather than a second inline "\n" -> "\n> " transform — one implementation, one place to be wrong (D-12).
DROPPED, and why: the documented "**Journal mode:**" line has no Pebble analog (same rationale as RenderStatusText); the documented "**Pending resolution:**" / pendingRefs>0 branch is dead code here since PendingRefs is hard-pinned to 0 (RESEARCH Pitfall 4); the documented "**Auto-sync disabled:**" (isWatcherDegraded) and per-file freshness (getPendingFiles) sections both depend on a live watcher, which is Phase 3 (WATCH-01) — not Phase 2 content, and are NOT stubbed here.
Note the documented design labels the FilesByLanguage breakdown "**Languages:**" in this form while its CLI labels the identical data "Files by Language:" — each surface keeps its own documented label.
func RenderStatusText ¶
func RenderStatusText(r StatusResult, projectPath string) string
RenderStatusText renders StatusResult in the documented CLI padded-column shape (D-09), built to the documented bin/codegraph.js ~900-985 shape. Called ONLY by internal/cli (plan 02-07) — see this file's header comment for why a second, structurally different renderer exists for MCP.
projectPath is caller-supplied rather than read from r: StatusResult's own ProjectPath field is deliberately blanked per its decision table's host-path privacy stance (status.go), but the CLI knows its own start path and passes it through here for the "Project: <path>" line — the same accepted host-path exception WorktreeMismatch already documents.
Journal: is deliberately DROPPED — no Pebble analog exists, consistent with the existing journalMode drop in StatusResult's decision table. Backend: renders r.Backend (the Go-truthful "pebble"), never a hardcoded string. The documented indexState (indexing/partial/failed) and pendingRefs>0 warning branches are deliberately NOT implemented here: Go's IndexHealth.State is only ever "complete"/"not_indexed" and PendingRefs is hard-pinned to 0, so those branches could never fire — implementing them would be dead code (RESEARCH Pitfall 4).
func ResolveCodegraphDir ¶
ResolveCodegraphDir walks filepath.Dir upward from start (D-01a, RESEARCH Pattern 4), returning the nearest ancestor directory (inclusive of start itself) that contains a .codegraph subdirectory. It returns ErrNotInitialized once the filesystem root is reached without finding one. start is resolved to an absolute path first via filepath.Abs (V5 — no unbounded/relative path is ever stat'd against unexpected cwd state).
func ValidateAffectedFiles ¶
ValidateAffectedFiles rejects a files slice longer than MaxAffectedFiles with a clear error (CR-01), mirroring validateLimit/validateMaxFiles's "reject outright rather than silently truncate" posture — a caller (or an untrusted/compromised MCP client, or a hostile CI diff) that tries to grow `affected --stdin`'s input past this ceiling gets an explicit error instead of an ever-growing, unbounded seen/files/fileSet allocation. Exported so internal/cli/affected.go's collectAffectedFiles can enforce the cap as it reads stdin, before Engine.Affected is ever called.
Unlike its unexported siblings above, this message carries no "query: " prefix: those are internal-only, whereas this one is exported precisely so the CLI can surface it verbatim under its own "affected: " prefix. Prefixing here would render as "affected: query: N files exceeds maximum M", leaking an internal package name into user-facing output.
func ValidateKind ¶
ValidateKind rejects an unknown --kind value against knownKinds before any node scan (T-03-02-Kind, V5): the empty string passes (no filter — query/search's default "match every kind" behavior), any known kind passes, and anything else returns an error naming the full allowed set so the caller can self-correct instead of silently scanning to an always-empty result.
func WorktreeNotice ¶
WorktreeNotice returns m.Notice() followed by a blank line (D-12) — the ONE uniform mechanism prefixed onto all 7 non-status read tools (explore/node/search/callers/callees/impact/files) whenever a borrowed-index mismatch is detected. status is excluded: it embeds its own verbose Warning() form instead, wrapped as a blockquote by WorktreeWarningBlockquote below. Mirrors staleBanner's exact shape (render_markdown.go) — nil-safe via Mismatch.Notice()'s own nil-receiver guard, "" when there is nothing to warn about, so every call site needs no nil guard.
A previously-proposed per-tool "_worktreeNotice" JSON-field hybrid was WITHDRAWN (CONTEXT.md D-12, corrected 2026-07-15): its premise was that a JSON contract needed protecting from a text-prefix, but MCP text content is consumed by a language model, not a parser — nothing in this repo, nor Claude Code, unmarshals it — and SURF-06 (D-16) moves every one of these tools to markdown regardless. There is one shape and one mechanism; do not reintroduce a per-tool notice mechanism.
func WorktreeWarningBlockquote ¶
WorktreeWarningBlockquote wraps m.Warning() as a markdown blockquote — "> ⚠ " prefixed onto the warning's first line, and every subsequent "\n" rewritten to "\n> " — for MCP status only (D-12), matching the documented mcp/tools.js construction. The CLI does NOT use this form: it prints the warning through its own warn-style line (plan 02-07), because the documented design ships two structurally different status renderings (D-17) and a blockquote is meaningless outside markdown.
Types ¶
type AffectedResult ¶
type AffectedResult struct {
Files []string `json:"files"`
AffectedTests []Location `json:"affectedTests"`
}
AffectedResult has no golden oracle (D-07a) — this shape is this plan's own design: the changed files that were queried, plus the impacted test locations derived at query time (D-07).
type CalleesResult ¶
CalleesResult mirrors the golden callees.json shape: {"symbol", "callees": [locations]}.
type CallersResult ¶
CallersResult mirrors the golden callers.json shape: {"symbol", "callers": [locations]}.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the read-only query engine over a single graphstore.Reader snapshot (D-02), plus (03-06) the repo root Node (file mode) and Explore confine their on-disk source reads to (D-05a — source is read fresh from disk on every call, not from the stored Node/File record).
repoRoot and startPath are NOT the same thing, and the distinction is the whole of D-14's worktree-detection bug: repoRoot is the RESOLVED INDEX root — the output of ResolveCodegraphDir's upward walk from whatever directory the caller started at, i.e. wherever the nearest ancestor .codegraph/ actually lives. startPath is where the caller ACTUALLY STOOD when they issued the query — OpenAt's start argument, absolutized. When start lives inside a linked worktree that has no .codegraph/ of its own, the upward walk resolves repoRoot to a DIFFERENT working tree (the main checkout), and worktree detection (WorktreeMismatch) is precisely the comparison between these two paths. Engines built via New/NewWithRoot carry no startPath (zero value "") and therefore have no context to detect a mismatch against — see WorktreeMismatch's degrade-safely contract.
func New ¶
func New(r graphstore.Reader) *Engine
New wraps an already-open graphstore.Reader in an Engine with no repo root configured. Most callers should use OpenAt instead; New exists so tests (and OpenAt itself, via NewWithRoot) can construct an Engine from a Reader obtained however is convenient. An Engine built via New rejects Node (file mode) and Explore's disk reads with a clear error, since it has no repo root to confine them to, and reports no worktree mismatch (no startPath, no repoRoot — see WorktreeMismatch).
func NewWithRoot ¶
func NewWithRoot(r graphstore.Reader, repoRoot string) *Engine
NewWithRoot wraps reader together with repoRoot (03-06, D-05a) — the directory Node (file mode) and Explore resolve every on-disk source read against and confine it to (path-traversal defense, T-03-06-Path). No startPath is set, so WorktreeMismatch degrades to nil (D-14) — only OpenAt supplies the caller's original starting directory.
func OpenAt ¶
OpenAt is the single read seam CLI commands and MCP tool handlers both call (D-08b): it resolves the nearest .codegraph/ at or above start (ResolveCodegraphDir), opens the GraphStore at its store subdirectory, takes a fresh Snapshot (D-02 — one snapshot per invocation, never reused across calls), and returns an Engine wrapping that snapshot plus an io.Closer that releases both the Reader and the underlying store.
The returned Engine also retains start, absolutized, as startPath (D-14) — the side of worktree detection ResolveCodegraphDir's upward walk does NOT capture (its return value becomes repoRoot, the resolved index root, which may differ from where the caller actually stood). On a filepath.Abs error, startPath is left empty rather than failing OpenAt — a status query must not die because a path could not be absolutized (WORK-03); WorktreeMismatch simply degrades to nil in that case.
The returned closer is idempotent: calling Close more than once is safe and returns nil on the second and subsequent calls, so a defer alongside an early explicit Close (or vice versa) never double-frees the underlying Pebble handles.
func (*Engine) Affected ¶
func (e *Engine) Affected(files []string, depth int) (AffectedResult, error)
Affected derives impacted test files/symbols for a set of changed files via a depth-bounded BFS over the D-04 reverse-adjacency map, bounded by clampAffectedDepth(depth) (SURF-04/D-05, CONTEXT D-05, RESEARCH Pitfall 2) — NOT the single-hop lookup this used to be. Mirrors Impact's frontier/next-frontier loop shape (traverse.go above), but with the documented test-files-as-leaves pruning rule instead of Impact's "expand everything": a dependent that passes isTestSymbol is recorded as an affected test AND is a leaf — it is never queued for further expansion, so its own dependents can never surface at any depth. A non-test dependent is queued for the next hop and is never itself recorded as an affected test. There is no golden oracle for this command (D-07a); behavior is proved structurally in traverse_test.go against seeded call chains.
RES-02 (WR-04 — 08-REVIEW.md): each frontier node's dispatch siblings (the same dispatchSiblingIDs composition Callers/Impact already apply — every OTHER concrete implementation's same-named method, reached through a shared "implements" edge) are ALSO expanded at every hop, so a changed file's dispatch-reachable test dependents are not silently excluded from affectedTests just because Affected historically only composed the direct reverse-adjacency map. When no implements edges exist in the graph (the common case), dispatch siblings are always empty and this composition is a no-op.
func (*Engine) Callees ¶
func (e *Engine) Callees(symbol string, limit int) (CalleesResult, error)
Callees returns symbol's forward call targets via a direct IterateEdges(srcID) range scan (D-04 — no reverse-adjacency scan needed for the forward direction), capped at limit (0/negative means unlimited, validated by validateLimit before any scan runs, V5).
func (*Engine) Callers ¶
func (e *Engine) Callers(symbol string, limit int) (CallersResult, error)
Callers returns symbol's reverse callers via the D-04 in-memory reverse-adjacency map (built fresh, BuildReverseAdjacency), capped at limit identically to Callees. RES-02: when symbol resolves to a method declared on a type that implements one or more interfaces, callers ALSO include callers of every OTHER implementer's same-named method — a call dispatched dynamically through the shared interface could have reached symbol, so its callers are relevant dispatch targets too (D-06's name-joined traversal, composed from BuildImplementsIndex + a "contains" index — both fresh-per-call, same discipline as BuildReverseAdjacency).
func (*Engine) Explore ¶
Explore is the flagship one-round-trip command (QRY-08, D-05a, EXPL-02): wires the full explore pipeline in the documented stage order (RESEARCH Architecture Diagram) — tokenize (H1/H2) -> hybrid gather (H3-H6) -> post-merge rerank (H7-H9) -> named-symbol seeding (H13) -> type-hierarchy expansion (H10) -> bounded BFS (H11) -> glue-node injection (H12) -> RWR graph relevance (computeGraphRelevance) -> per-file score tiers + hard exclusion + buried-rescue (H14-H16) -> central-file selection + the 5-way relevance gate + the 5-tier sort (H17-H19) -> render, with H21's adaptive output budget sizing the render. RWR output (not lexical match order) feeds groupMatchesByFile — the RESEARCH Pitfall 1 fix: this REPLACES the lexical matchNodes input construction, not a rerank layered on top of it. groupMatchesByFile/buildBlastEntry/ readSourceFile are unchanged downstream (D-05's "extend, don't replace").
"Matched" symbols shown per selected file (the header's "sym(kind), ..." list and the blast-radius bullets) are the query's actual gather+seed candidates in that file, RWR-score-ordered (not the whole bounded subgraph — showing every BFS/glue/hierarchy-expanded node would flood a file's header with symbols the query never textually or nominally matched, and would break the single-exact-match "1 symbol, 1 file" contract). A file selected purely through structural connectivity (no direct candidate landed in it) falls back to its single highest-RWR-mass subgraph node, so a structurally-selected file is never rendered with an empty symbol list.
query is rejected if empty/whitespace-only (WR-05, mirroring Query/Search) rather than falling through to a tokenizer's degenerate "matches everything" case; the tokenizers themselves also return empty for empty input (plan 03) — two layers, per the threat model's T-01-25 disposition.
func (*Engine) Files ¶
func (e *Engine) Files(opts FilesOptions) (FilesResult, error)
Files browses the indexed file structure from the graph (QRY-07): it reads e.reader.IterateFiles() only — never os.ReadDir or any other live filesystem walk — so the result reflects the frozen point-in-time graph even if the working tree has since changed underneath it. pattern narrows by glob match on the full path, filter narrows by exact Language match, dir narrows by path prefix (SURF-02, orthogonal to and composed with filter), depth bounds directory nesting, and format selects the flat-list vs. nested-tree projection. The returned set is additionally capped at MaxLimit entries (T-03-05-DoS, "cap the returned set").
func (*Engine) Impact ¶
func (e *Engine) Impact(symbol string, depth int) (ImpactResult, error)
Impact returns the depth-bounded reverse blast radius of symbol: a BFS over the D-04 reverse-adjacency map, bounded by clampDepth(depth) (T-03-04-DoS, RESEARCH Pitfall 4). NodeCount is the count of distinct visited nodes including symbol itself; EdgeCount is the count of reverse edges inspected while expanding each depth's frontier — this counting rule is cross-checked against testdata/golden/corpus/weft-go/impact.json's arithmetic in traverse_test.go's TestImpact doc comment. RES-02: at each frontier node, dispatch siblings (same composition Callers uses — every OTHER implementer's same-named method, reached via a shared "implements" edge) are ALSO expanded, so a change's blast radius includes callers reachable only through dynamic dispatch. When no implements edges exist in the graph (the common case, and every pre-existing fixture this method's tests exercise), dispatch siblings are always empty and this composition is a no-op — the arithmetic above is unchanged.
func (*Engine) Node ¶
Node renders symbol detail (QRY-02, D-05b) when symbol is non-empty, or a line-numbered verbatim file read when symbol is empty and file is given. file additionally disambiguates symbol when both are supplied. line is an optional NODE-03 narrowing hint (RESEARCH §9); a nil line with a non-empty file tries resolveNodeForDetail's existing exact-match single-winner behavior FIRST and returns immediately on success, so every pre-CR-02 exact-match caller — the CLI's `-f`/`--file` flag used without `--line`, and any existing golden fixture — gets byte-for-byte identical output (NODE-04). When symbol is given without file, or when the exact-match attempt above didn't apply/succeed, Node enumerates every exact-name definition (NODE-01) and narrows it via narrowNodeMatches (NODE-03: substring file hint + line-containment hint, never emptying the set — a pure in-memory filter, D-07, never a fresh disk read keyed on the raw hint): a single narrowed match renders via the original single-def RenderNode path unchanged (NODE-04's no-hints case is a no-op through narrowNodeMatches), while multiple narrowed matches render via NODE-02's multi-def budget/overflow path (RenderNodeMultiDef).
func (*Engine) Query ¶
Query returns full node records whose name or qualifiedName matches term (D-06), optionally filtered by kind and capped at limit. term is validated non-empty (WR-05 — an empty/whitespace-only term would otherwise match every node via lexicalMatchTier's degenerate strings.HasPrefix(field, "") == true case, a "dump the whole graph" footgun compounding CR-01), kind is validated via ValidateKind, and limit via validateLimit, all before any scan runs (V5, T-03-03-Kind, T-03-03-DoS) — an empty term, unknown kind, or out-of-range limit returns an error without touching the store.
func (*Engine) Search ¶
Search returns the same matches as Query, but projected to the lightweight Location shape (D-06) — no source body, no signature. term/kind/limit are validated identically to Query, before any scan.
func (*Engine) Status ¶
func (e *Engine) Status(ctx context.Context) (StatusResult, error)
Status reports index health/counts (QRY-09) by scanning the frozen graph: fileCount + filesByLanguage from a single IterateFiles scan, nodeCount + nodesByKind from a single IterateNodes scan, and edgesByKind from a single full edge-iteration scan (v0.11.0 Phase 1, FIXT-01) mirroring buildExpandAdjacency's established full-scan shape (internal/query/expand.go), unfiltered by RankEdges so a kind outside the 9-kind ranked set is still tallied. edgeCount itself still comes from GetMeta's indexer-stamped Meta.EdgeCount aggregate (internal/indexer/resolve.go) — a separate read from edgesByKind, not a sum of it.
A full edge scan is now UNCONDITIONAL on every status call — previously this method read only Meta.EdgeCount and never scanned edges at all. edgesByKind is deliberately derived fresh at read time on every call and never stored in Meta: D-01 scopes this phase to `status`, not to indexer/Meta changes, so a future phase MAY choose to stamp a per-kind aggregate at index time to remove this scan's cost — a real, named future optimization, not an oversight. Edge keys are `edge/<src>/<kind>/<dst>` with no leading kind segment (internal/graphstore/keys.go), so no cheaper per-kind prefix scan is available today: one full scan tallying every kind at once is the correct and only reasonable approach given the current key layout.
Every scan-derived count in the returned StatusResult (fileCount, nodeCount, nodesByKind, filesByLanguage, edgesByKind) comes from the SAME e.reader snapshot, so they are mutually consistent with each other. Meta.EdgeCount, by contrast, is a separately-stamped aggregate the indexer writes at index time — while a background re-index is in flight, it may legitimately disagree with the sum of edgesByKind. That disagreement is a true reading of an index mid-write, not a bug.
languages is derived from filesByLanguage (D-05: count > 0, sorted), not from a separate node-scan languageSet, so it reflects every file the indexer discovered and stored — including a file that yields zero extracted nodes — rather than only files with at least one resolved node. A missing Meta record (a store that exists but was never indexed) is tolerated rather than treated as an error: counts fall back to the scanned values and index.state reports "not_indexed".
ctx (WR-01) is threaded through to WorktreeMismatch, which spawns up to four git subprocesses — see WorktreeMismatch's doc comment for why this must be the caller's real, cancelable context rather than context.Background().
func (*Engine) UseDetector ¶
func (e *Engine) UseDetector(d *gitmeta.CachingDetector)
UseDetector installs a shared, caller-owned gitmeta.CachingDetector that WorktreeMismatch will route detection through instead of computing an uncached, Engine-private verdict.
This exists because internal/mcp's openEngine builds a FRESH Engine on every single tool call by design (D-02/D-08b, RESEARCH Pitfall 2) — an Engine-scoped cache alone (the mismatchOnce/mismatchCache fields below) yields ZERO cross-call benefit on the exact long-lived surface (the MCP server process) the cache exists to help, since a brand-new Engine means a brand-new sync.Once every time. internal/mcp therefore constructs ONE CachingDetector per server (BuildServer) and calls UseDetector on every Engine it opens, so all tool calls within one server's lifetime share a single git-subprocess-avoiding cache (D-13, corrected). The CLI needs no such call — it constructs at most one Engine per invocation, so the per-Engine cache alone is already free.
func (*Engine) WorktreeMismatch ¶
WorktreeMismatch returns the live worktree/index-mismatch verdict for this Engine (D-14/WORK-01): whether startPath (where the caller stood) belongs to a DIFFERENT git working tree than repoRoot (the resolved index root). Detection runs at most once per Engine (guarded by mismatchOnce) — Status() and any render path that also calls this method within one request never re-spawn git.
ctx (WR-01) is threaded all the way down to the underlying git subprocesses (up to four, gitmeta.DetectIndexMismatch's doc comment) — every MCP handler and CLI command already receives a real, cancelable context (the handler's ctx / cmd.Context()); before WR-01 this method discarded it in favor of context.Background(), so a client that disconnected or timed out still left up to ~20s of uncancellable git subprocess work running with no way to abort it.
Returns nil, without ever panicking, when this Engine has no filesystem context to check: startPath == "" or repoRoot == "" (Engines built via New/NewWithRoot — the same degrade-safely shape computeStale already uses for e.repoRoot == ""). Otherwise delegates to e.detector.Detect, which is nil-receiver-safe (falls through to an uncached gitmeta.DetectIndexMismatch when no detector was injected via UseDetector) — so no nil branch is needed here.
BL-01 note: mismatchOnce/mismatchCache latch a verdict computed under a cancelled ctx too — but that is safe HERE, unlike gitmeta.CachingDetector. internal/mcp's openEngine builds a brand-new Engine (and therefore a brand-new mismatchOnce) on every single tool call, so this per-Engine latch never outlives one request; the long-lived poisoning risk lives entirely in the server-scoped CachingDetector this method delegates to, which is why BL-01's fix (never caching a cancelled-ctx verdict) lives in gitmeta.CachingDetector.Detect, not here. The CLI is unaffected for a different reason: cmd.Context() is always context.Background() today (IN-01, uncancellable), so a cancelled ctx never reaches this method on that surface at all.
type ExpandBFSBounds ¶
ExpandBFSBounds groups H11's four explicit override bounds (RESEARCH §C.2/H11) so callers pass one value, not four positional ints/floats.
type FileEntry ¶
type FileEntry struct {
Path string `json:"path"`
Language string `json:"language"`
NodeCount int64 `json:"nodeCount"`
EdgeCount int64 `json:"edgeCount"`
}
FileEntry is one browsed file's projection: identity plus the aggregate symbol/edge counts already stored on its schema.File record (no additional graph work per entry).
type FileTreeNode ¶
type FileTreeNode struct {
Name string `json:"name"`
IsDir bool `json:"isDir"`
Path string `json:"path,omitempty"`
Language string `json:"language,omitempty"`
Children []*FileTreeNode `json:"children,omitempty"`
}
FileTreeNode is one node of the "tree" format's nested directory projection. Directory nodes carry Children and no Path/Language; file (leaf) nodes carry Path/Language and no Children.
type FilesOptions ¶
type FilesOptions struct {
// Pattern is a shell glob (path/filepath.Match semantics, matched
// against the full forward-slashed file path) that narrows the
// result set. Empty matches every file.
Pattern string
// Filter narrows results to files whose Language exactly matches.
// Empty applies no language filter.
Filter string
// Dir narrows results to files whose path starts with this prefix —
// a plain strings.HasPrefix check (or "./"+Dir), implementing the
// documented files --filter <dir> semantics exactly
// (bin/codegraph.js:1348-1354). This is
// deliberately NOT a glob despite its CLI flag's <dir> placeholder
// text — see dirPrefixMatches. Empty applies no directory filter.
// Orthogonal to and composes (AND) with Filter (the language filter,
// CONTEXT D-03 add-alongside): a file must satisfy both to appear.
Dir string
// Depth caps directory nesting: a file whose path has Depth or more
// "/" separators is excluded (Depth=1 means root-level files only,
// Depth=2 allows one level of nesting, and so on). 0 (the zero
// value) means unlimited — deliberately NOT clampDepth's "0 means a
// small BFS-safe default" convention (that convention exists to
// bound unbounded graph traversal cost; Files is a bounded scan over
// an already-enumerated file set, so "browse everything" is the
// useful default). Negative values and values above MaxDepth are
// rejected outright (validateFilesDepth) rather than silently
// clamped, matching validateLimit's "reject absurd input" pattern
// (T-03-05-DoS, reusing the existing validate-helper convention).
Depth int
// Format selects the projection: "flat" (default, or when empty)
// returns FilesResult.Files, a sorted list of FileEntry records.
// "tree" returns FilesResult.Tree, the same entries grouped into a
// nested directory structure. Any other value is rejected.
Format string
}
FilesOptions configures Engine.Files' browse of the indexed file structure (QRY-07). The zero value browses every indexed file, in the default "flat" format, with no depth limit.
type FilesResult ¶
type FilesResult struct {
Format string `json:"format"`
Files []FileEntry `json:"files,omitempty"`
Tree []*FileTreeNode `json:"tree,omitempty"`
}
FilesResult is Engine.Files' return shape: exactly one of Files (flat format) or Tree (tree format) is populated, per Format.
type ImpactResult ¶
type ImpactResult struct {
Symbol string `json:"symbol"`
Depth int `json:"depth"`
NodeCount int `json:"nodeCount"`
EdgeCount int `json:"edgeCount"`
Affected []Location `json:"affected"`
}
ImpactResult mirrors the golden impact.json shape: {"symbol","depth", "nodeCount","edgeCount","affected": [locations]}.
type IndexHealth ¶
type IndexHealth struct {
BuiltWithVersion string `json:"builtWithVersion"`
BuiltWithExtractionVersion uint32 `json:"builtWithExtractionVersion"`
CurrentExtractionVersion uint32 `json:"currentExtractionVersion"`
ReindexRecommended bool `json:"reindexRecommended"`
State string `json:"state"`
PendingRefs int `json:"pendingRefs"`
}
IndexHealth mirrors the golden's index.* shape, with the TS version/extraction fields remapped to schema.SchemaVersion-derived values (see StatusResult's mapping table).
type Location ¶
type Location struct {
Name string `json:"name"`
Kind string `json:"kind"`
FilePath string `json:"filePath"`
StartLine int32 `json:"startLine"`
}
Location is the lightweight locations-only projection Search returns (D-06) — name/kind/filePath/startLine only, no source body/signature. It is exported so 03-04's callers/callees/impact commands can reuse the same shape (callers.json/callees.json/impact.json all wrap this exact field set, per RESEARCH's Code Examples).
type PendingChanges ¶
type PendingChanges struct {
Added int `json:"added"`
Modified int `json:"modified"`
Removed int `json:"removed"`
}
PendingChanges mirrors the golden's pendingChanges shape — a Phase-4 sync concept rendered as an inert all-zero placeholder in Phase 3 (see StatusResult's mapping table).
type StatusResult ¶
type StatusResult struct {
Initialized bool `json:"initialized"`
Version string `json:"version"`
ProjectPath string `json:"projectPath"`
IndexPath string `json:"indexPath"`
FileCount int64 `json:"fileCount"`
NodeCount int64 `json:"nodeCount"`
EdgeCount int64 `json:"edgeCount"`
DbSizeBytes int64 `json:"dbSizeBytes"`
Backend string `json:"backend"`
NodesByKind map[string]int64 `json:"nodesByKind"`
EdgesByKind map[string]int64 `json:"edgesByKind"`
FilesByLanguage map[string]int64 `json:"filesByLanguage"`
Languages []string `json:"languages"`
PendingChanges PendingChanges `json:"pendingChanges"`
WorktreeMismatch *gitmeta.Mismatch `json:"worktreeMismatch"`
Stale bool `json:"stale"`
Index IndexHealth `json:"index"`
}
StatusResult mirrors testdata/golden/corpus/weft-go/status.json's shape (QRY-09), with TS-SQLite-specific keys remapped to Go/Pebble-truthful values or dropped, per CONTEXT D-05 and RESEARCH Open Question 2. This is the plan's authoritative per-key decision table:
TS key | Go/Pebble rendering | Rationale
---------------------------------|---------------------------------------------|----------
initialized | true (Status only runs on an opened Engine) | Unchanged — same concept
version | fmt.Sprintf("%d", schema.SchemaVersion) | No codegraph-go release-version concept exists yet; schema version is the closest stable Go analog
projectPath / indexPath | "" (empty string, key kept) | Engine carries no path context in its read-only Reader-only design (files_modified excludes engine.go this plan) — trivially satisfies T-03-05-Leak by having nothing host-specific to leak, while keeping the key present for output shape stability
fileCount / nodeCount / edgeCount | computed from IterateFiles/IterateNodes scans + Meta.EdgeCount | Unchanged concept, Go-sourced values
backend | "pebble" (a literal Pebble identifier) | D-05's explicit example remapping
journalMode | dropped (key omitted) | No Pebble user-facing WAL/journal-mode analog (RESEARCH Open Question 2); D-05 permits dropping keys with no Go analog
nodesByKind / languages | computed via a full IterateNodes() scan | D-03's IterateNodes; reflects whichever LanguageSpecs are registered AND have discoverable files in the repo (a Go-only repo reads languages:["go"]; a repo with both Go and Python source, e.g. the weft corpus, reads languages:["go","python"] once Phase 5's Python extraction lands)
pendingChanges | {added:0,modified:0,removed:0} | Phase-4 sync concept; the added/modified/removed COUNT breakdown remains an inert placeholder — computing it would require re-running Sync's diff at Status()-time (out of scope, RESEARCH A2). The plain existence of pending changes is now live via the new top-level `stale` field below (D-04a)
worktreeMismatch | live *gitmeta.Mismatch, {worktreeRoot,indexRoot} object or null | D-14/WORK-01 — computed via Engine.WorktreeMismatch(), which runs gitmeta.DetectIndexMismatch's 4-gate cascade against Engine.startPath (where the caller stood) vs repoRoot (the resolved index root). ★ Deliberate, scoped exception to this table's own projectPath/indexPath privacy stance below: those two keys are blanked to avoid leaking host-local absolute paths through the MCP surface, but the mismatch warning is USELESS without naming the two trees involved, and TS interpolates the identical raw paths (T-02-14, accepted) — so this key intentionally carries absolute host paths, but ONLY when a mismatch is genuinely detected; a clean tree still leaks nothing (nil)
stale | live bool (D-04a) | true when `.codegraph/.sync-pending` exists (watcher/daemon signal) OR — no-daemon fallback — the newest on-disk source-file mtime is newer than Meta.last_sync_unix_ms; this is the field that makes the sync-pending concept real this phase, see computeStale
index.builtWithVersion | fmt.Sprintf("%d", schema.SchemaVersion) | Same Go analog as top-level version — no separate release/build-version concept
index.builtWithExtractionVersion | uint32(schema.SchemaVersion) | Go has one "extraction version" concept: the schema version stamped by NewMeta
index.currentExtractionVersion | uint32(schema.SchemaVersion) | This build's own SchemaVersion constant
index.reindexRecommended | !schema.IsCurrentSchemaVersion(meta) | Derived, not a literal placeholder — true when the stored Meta predates this build's schema version
index.state | "complete" if Meta exists, else "not_indexed" | Best-effort Go analog of TS's index lifecycle state
index.pendingRefs | 0 (always) | Phase 2 resolves all refs at index time (no unresolved-ref persistence in Go v1); inert placeholder matching the golden's own steady-state 0
dbSizeBytes | filepath.WalkDir byte sum over .codegraph/store/ | D-07 — Pebble has no single-file page-count analog to SQLite; a recursive byte sum over the store dir (SSTables+WAL+MANIFEST) is the honest Go-truthful reading. Best-effort: an Engine with no repoRoot (New, not OpenAt) or an unreadable/missing store dir degrades to 0 rather than failing Status(). Reverses the golden-corpus strip on the Go side only — see D-08 and testdata/golden/README.md's volatile-fields table
filesByLanguage | map[string]int64, now emitted (key unsuppressed) | D-05 originally suppressed this key from --json to avoid a Go-vs-TS shape divergence — TS's own --json derives `languages` from this map and discards the counts. That Compatibility constraint was formally retired 2026-08-13 (engram record gw79qy2a9z): TS parity is no longer owed on --json shape, so v0.11.0 Phase 1 (FIXT-01) un-suppresses this key in the same diff as the new edge tally below. Still computed in the existing IterateFiles() scan by reading fileIt.File().Language — no new scan needed
edgesByKind | map[string]int64, new field, emitted | v0.11.0 Phase 1 (FIXT-01) — a read-time-derived, per-edge-kind tally from one full edge scan, mirroring nodesByKind/buildExpandAdjacency's established full-scan shape (internal/query/expand.go). Unfiltered by RankEdges, so a kind outside the 9-kind ranked set (e.g. "contains") is still tallied rather than silently discarded. Sparse: a kind with zero observed edges is absent from the map, never present with value 0. Deliberately NOT stored in Meta — see Status()'s doc comment
(no lastIndexed / *_at keys) | omitted entirely | Volatile fields per testdata/golden/README.md's stripping rules — never rendered (dbSizeBytes above is the one documented exception, D-08)