graph

package
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: Apache-2.0 Imports: 35 Imported by: 1

Documentation

Overview

Package graph — store.go defines the pluggable storage interface for persisting and restoring projected graphs to/from remote backends.

The unit of persistence is a SQLite .db file (produced by SQLiteWriter or mache build). This keeps the abstraction simple: backends just need to store and retrieve opaque blobs keyed by repo identity + generation.

Implementations: local disk (default), Cloudflare R2, GCS, S3.

Index

Constants

View Source
const (
	ArenaHeaderSize = 4096
	ArenaMagic      = 0x4C455930
	// ArenaVersion is the current wire-format. v1 arenas inferred payload
	// length from the SQLite header (db page_count × page_size); v2
	// records DataSize explicitly so readers stay format-agnostic.
	ArenaVersion = 2
)
View Source
const (
	NodeKindFile = 0
	NodeKindDir  = 1
)

NodeKindFile and NodeKindDir are the integer values stored in the nodes table's `kind` column (the wire format produced by mache build / leyline parse). They are NOT a substitute for *Node.Mode at runtime — Mode uses fs.FileMode and carries the IsDir() predicate. The kind constants are used by ingestion writers when populating the column, by readers when interpreting it, and as a comment marker next to bare-int SQL literals like `kind = 1` (SQL can't reference Go constants directly).

These values are part of the on-disk schema and must not change.

Bead mache-e7e9e5.

View Source
const (
	SchemaDotJSON  = "_schema.json"
	DiagnosticsDir = "_diagnostics"
	ContextFile    = "context"
	LocationFile   = "location"
	PromptFile     = "PROMPT.txt"
	CallersDir     = "callers"
	CalleesDir     = "callees"
	DiagLastWrite  = "last-write-status"
	DiagASTErrors  = "ast-errors"
	DiagLint       = "lint"
)

Well-known virtual directory and file names.

View Source
const DefaultMaxFanIn = 100

DefaultMaxFanIn is the per-token fan-in above which a token is treated as a hub and excluded from the co-reference projection.

The projection connects every PAIR of nodes sharing a token, so a token referenced by K nodes costs K(K-1)/2 edges. Measured on mache itself: 12,275 tokens produce 84,813,280 edge insertions, and the single token `t` (the ubiquitous *testing.T receiver, K=9,903) accounts for 49M of them — 58% of the total. The top ten tokens — t, err, NoError, require.NoError, Equal, len — are 86%.

Cost is only half the reason. Those hubs also DESTROY the answer: `t` joins 9,903 nodes into a near-complete subgraph, so Louvain collapses everything that touches *testing.T into one blob. A token that nearly everything references is not evidence of shared purpose; it is the co-occurrence equivalent of a stopword.

100 keeps 12,091 of 12,275 tokens (98.5%) and removes 99.2% of the edge work. The threshold is a heuristic, exposed as a parameter — it is not a constant anyone should have to edit source to change, which is exactly the mistake the old refs-count cap made.

View Source
const DefaultMaxPairs = 5_000_000

DefaultMaxPairs bounds the projection's total edge insertions — Σ K(K-1)/2 over surviving tokens — as a backstop behind the fan-in cap.

Fan-in pruning removes the DOMINANT term, not the whole cost. With a cap of 100, a pathological repo where every one of 12,275 tokens sits exactly at the cap would still project 60.8M pairs; mache measures 681K only because real fan-in is heavily skewed. A budget keyed on the quantity that actually costs is the guard the old refs-count cap was reaching for and missed by ~7000×.

5,000,000 is ~7x mache's own measured cost, so a repo an order of magnitude larger still runs unclamped.

View Source
const FileLevelSentinelPrefix = "_file_level:"

FileLevelSentinelPrefix marks a node_refs row that records a FILE-level reference rather than a reference from a construct.

The engine emits these for patterns like cobra's `RunE: handler`, where a token is referenced at file scope with no enclosing function. dead_code needs them: its alive-set check is token-only, and without the sentinel every handler assigned that way looks dead. Attributing the ref to some arbitrary function in the file instead would be worse — it made every function in a cobra file appear to call the callback (mache-02r9).

So the row is correct. What it is NOT is user content: `_file_level:` ids name no construct anyone can open, refactor, or reason about, and they carry an ABSOLUTE path, so they are not even portable. They are bookkeeping, and they belong to the producer side of the schema.

View Source
const ProjectionSchemaVersionKey = "projection_schema_version"

ProjectionSchemaVersionKey is the `_meta` row ley-line-open stamps with the shape of the projection it wrote.

Variables

View Source
var ErrActNotSupported = errors.New("act not supported by this graph")

ErrActNotSupported is returned by Graph implementations that do not support actions.

View Source
var ErrNotFound = errors.New("node not found")
View Source
var ErrStalePropsSchema = errors.New("nodes table predates the props column")

ErrStalePropsSchema is returned for a nodes table mache itself wrote before the props column existed. Exposed so callers can distinguish "rebuild this" from a genuine open failure.

Functions

func AggregateRefs added in v0.22.0

func AggregateRefs(refs map[string][]string, g Granularity, scope string) map[string][]string

AggregateRefs rewrites refs (token → node ids) to the requested granularity and applies an optional path-prefix scope.

Scope BEFORE aggregation, on the raw id: scoping to "cmd/" must keep cmd/serve.go's nodes whether the caller asked for construct, file, or dir cells.

Aggregation DEDUPES within a token: a token referenced 40 times inside one file is ONE occurrence of that file, not 40 — otherwise buildProjection would inflate pair weights quadratically for chatty files, and the hub problem this pipeline just removed at token level would reappear at cell level. Order is preserved first-seen so identical input keeps producing an identical projection (mache-ff7e31).

A token whose occurrences all collapse into a single cell contributes no pairs and is dropped by buildProjection's len<2 skip — correctly: a symbol used in only one file says nothing about which files belong together.

func ColumnExists added in v0.22.0

func ColumnExists(db *sql.DB, table, col string) bool

ColumnExists reports whether table has a column named col. Readers use it to stay compatible with nodes tables written before a column was added (e.g. `context`, mache-b8fe72) or produced by leyline, and writers use it to decide whether an ALTER is needed.

Deliberately pragma_table_xinfo, NOT pragma_table_info: table_info omits GENERATED columns entirely, so it answers "missing" for a column that is present, readable, indexed, and returned by SELECT *. ley-line-open already ships one (`source_blobs.byte_len` STORED) and projection-v4 makes `nodes.parent_id` another (mache-bc6ca3). Under table_info a reader would silently downgrade to a compatibility path for a column it can read, and a writer would emit an ALTER that fails with "duplicate column name". xinfo reports the same rows as info plus generated and hidden ones, so no existing probe changes its answer.

func ConnectedComponents added in v0.22.0

func ConnectedComponents(refs map[string][]string) [][]string

ConnectedComponents finds connected components in the refs graph projection. Simpler than Louvain — useful as a baseline or when modularity optimization is overkill (e.g., disconnected subgraphs).

func CreateArena added in v0.22.0

func CreateArena(dbPath, arenaPath string) error

CreateArena creates a fresh double-buffered arena file from a .db file. Layout: [Header (4KB)] [Buffer0 = dbBytes] [Buffer1 = zeros] Buffer size is the .db file size rounded up to the next 4KB page boundary.

func DecodeProps added in v0.22.0

func DecodeProps(raw []byte) map[string]json.RawMessage

DecodeProps unmarshals a serialized Properties blob as read from SQLite. Returns nil when raw is empty or is not a property map — a node carrying inline content rather than properties decodes to nil, which is expected.

Every SQL read path goes through this rather than unmarshaling inline, so the on-disk encoding stays knowable from this file alone.

func DefTokenFor added in v0.22.0

func DefTokenFor(g Graph, nodeID string) string

DefTokenFor returns the symbol a node defines.

Callers that need "what is this node called?" must NOT reach for filepath.Base(nodeID). That works only on the standalone mache schema, where a node ID ends in the symbol. On a leyline projection — mache's primary backend — IDs end in a tree-sitter construct kind plus an ordinal:

a.go/function_declaration_0    not    a.go/Alpha
pkg/type_spec                  not    pkg/Greeter

Measured over a real projection of this repo: of 10,409 rows in node_defs, ZERO have a node_id ending in their own token. So the path-segment guess is not merely lossy there, it is never right (mache-cb4369).

That mattered because it did not fail closed. GetCallers("function_declaration") matches that literal string wherever it appears, and leyline indexes markdown too — so get_impact answered with code spans from docs/ARCHITECTURE.md presented as call sites. Wrong answers, not empty ones.

node_defs is the authority: it maps a node to the token that node declares, asked through the graph's DefTokener so this file names none of the producer's columns (projection-v6 renamed them, mache-90cb9f).

Every surface that names a construct's callers asks this: MCP's list_directory, get_impact and get_dataflow, and the mount's callers/. The filepath.Base fallback is kept for the standalone mache schema (no node_defs table, or a backend with no SQL handle at all), where it IS the symbol — and only for that case.

func ExportSQLite added in v0.4.0

func ExportSQLite(store *MemoryStore, dbPath string) error

ExportSQLite writes all nodes from a MemoryStore to a SQLite database. Creates the nodes table if it doesn't exist. Existing entries are overwritten. The resulting file uses the standard mache nodes table schema.

func ExtractActiveDB added in v0.22.0

func ExtractActiveDB(arenaPath string) (string, error)

ExtractActiveDB extracts the active SQLite database from the arena to a temp file. Returns the path to the temp file.

func FilterTestRefs added in v0.22.0

func FilterTestRefs(refs map[string][]string) map[string][]string

FilterTestRefs returns a copy of refs with test-related content removed. Filters both test nodes (IDs containing "_test", "Test", "Benchmark") and test tokens (env vars with "TEST" in the name, tokens starting with "Test").

func FindSourceChild added in v0.22.0

func FindSourceChild(g Graph, dirID string) string

FindSourceChild finds the "source" file child of a directory node. Returns the full source ID or "" if not found. Used by callees/ to resolve a callee directory to its source content.

func IsCalleesPath added in v0.22.0

func IsCalleesPath(path string) bool

IsCalleesPath returns true if the path contains a /callees segment boundary.

func IsCallersPath added in v0.22.0

func IsCallersPath(path string) bool

IsCallersPath returns true if the path contains a /callers segment boundary.

func IsDiagPath added in v0.22.0

func IsDiagPath(path string) bool

IsDiagPath returns true if the path contains a /_diagnostics segment.

func IsFileLevelSentinel added in v0.22.0

func IsFileLevelSentinel(nodeID string) bool

IsFileLevelSentinel reports whether a node id is engine bookkeeping rather than a construct a consumer can act on.

func NormalizeID added in v0.22.0

func NormalizeID(id string) string

NormalizeID strips a leading slash from node IDs. FUSE/NFS paths use "/foo/source" but graph keys are "foo/source". Only strips one leading slash; "//double" becomes "/double".

func ParseCalleesPath added in v0.22.0

func ParseCalleesPath(path string) (parentDir, entryName string)

ParseCalleesPath splits a callees path into (parentDir, entryName). E.g. "/funcs/Foo/callees/funcs_Bar_source" → ("/funcs/Foo", "funcs_Bar_source")

func ParseCallersPath added in v0.22.0

func ParseCallersPath(path string) (parentDir, entryName string)

ParseCallersPath splits a callers path into (parentDir, entryName). E.g. "/funcs/Foo/callers/funcs_Bar_source" → ("/funcs/Foo", "funcs_Bar_source") Returns ("", "") if not a valid callers path.

func ParseDiagPath added in v0.22.0

func ParseDiagPath(path string) (parentDir, fileName string)

ParseDiagPath splits a diagnostics path into (parentDir, fileName). E.g. "/vulns/func_a/_diagnostics/last-write-status" → ("/vulns/func_a", "last-write-status") Returns ("", "") if not a valid diagnostics path.

func ProjectionVersion added in v0.22.0

func ProjectionVersion(db *sql.DB) (string, bool)

ProjectionVersion returns the projection shape an artifact declares and whether it declares one at all.

A missing `_meta` table, a missing row and an empty value are all reported the same way — undeclared — because a reader cannot act on the difference: each means this artifact will not tell you its shape.

func PropRaw added in v0.20.0

func PropRaw(n *Node, key string) []byte

PropRaw returns a property's raw JSON bytes.

func PropString added in v0.20.0

func PropString(n *Node, key string) string

PropString returns a string-valued property, or "" when the node, the map, or the key is absent, or the value is not a JSON string.

func RequireProps added in v0.22.0

func RequireProps(db *sql.DB) error

RequireProps rejects a nodes table that mache wrote before the props column existed (mache-90b89b). Such a table has Properties base64'd into `record`, a format no current code reads — serving it would silently yield constructs with no lang/pkg/imports rather than failing.

The check is scoped by producer. mache's SQLiteWriter always has `context` (it is in CREATE TABLE, and ALTERed in for older tables); leyline-produced tables stop at source_file and carry no Properties at all. So `context` present with `props` absent is exactly "stale mache-written", and is the only case refused — a blanket refusal would reject every leyline .db, which since v0.18.0 is mache's primary input.

func SetPropRaw added in v0.20.0

func SetPropRaw(n *Node, key string, raw []byte)

SetPropRaw sets a property's raw JSON bytes, allocating Properties if nil. The caller is responsible for raw being valid JSON.

func SetPropString added in v0.20.0

func SetPropString(n *Node, key, value string)

SetPropString sets a string-valued property, allocating Properties if nil.

func SliceContent added in v0.22.0

func SliceContent(data, buf []byte, offset int64) int

SliceContent copies content bytes into buf at the given offset. Returns the number of bytes copied. Shared by all ReadContent implementations.

func VDirSymlinkTarget added in v0.22.0

func VDirSymlinkTarget(vdirParentDir, targetID string) string

VDirSymlinkTarget computes the relative symlink target from a virtual dir entry back to the target node in the graph. Works for both callers/ and callees/.

func WriteArenaHeader added in v0.22.0

func WriteArenaHeader(f *os.File, h *ArenaHeader) error

WriteArenaHeader serializes an ArenaHeader to the first 24 bytes at offset 0.

Types

type ASTKinder added in v0.22.0

type ASTKinder interface {
	// ASTKindChain returns the kind of the node at id, followed by the kinds
	// of those of its ancestors that have one, nearest first. ok is false
	// when the node itself has no _ast row.
	ASTKindChain(id string) (chain []string, ok bool, err error)
}

ASTKinder reports the tree-sitter kinds of a node and its ancestors: the question a kind filter asks ("is this a function, and is it inside a class?"). It exists so the asker does not spell the producer's columns, which projection-v6 renamed: _ast and nodes are keyed by node_id/id on v4 and by nid on v6 (mache-90cb9f).

type ActionResult added in v0.5.0

type ActionResult struct {
	NodeID  string `json:"node_id"`           // mache ID of the acted-upon node
	Action  string `json:"action"`            // "click", "type", "enter", "focus"
	Path    string `json:"path"`              // filesystem path
	Payload string `json:"payload,omitempty"` // optional (e.g., typed text)
}

ActionResult is returned when an action is performed on a graph node. Used by interactive graphs (browser DOM, iTerm2 sessions, macOS AX elements).

type ArenaFlusher added in v0.22.0

type ArenaFlusher struct {
	// contains filtered or unexported fields
}

ArenaFlusher writes a serialized .db file into the double-buffered arena and atomically flips the header so readers see the new version.

Flush is O(N) where N = DB size — the entire .db is copied to the inactive buffer on every flush. To amortize this cost, callers should use RequestFlush (coalesced) instead of FlushNow (synchronous) for writes. The coalescing goroutine batches rapid writes into a single flush per tick interval (default 100ms), so N concurrent agent writes within a tick produce 1 flush instead of N.

Benchmarked on M3 Max (BenchmarkArenaFlush):

108 KB  → ~4ms  (fsync-dominated floor)
  1 MB  → ~5ms
 10 MB  → ~10-25ms

Flush uses page-level diff: only changed SQLite pages are written to the inactive buffer, avoiding a full-DB copy on every swap.

func NewArenaFlusher added in v0.22.0

func NewArenaFlusher(arenaPath, masterDBPath string, ctrl *control.Controller) *ArenaFlusher

NewArenaFlusher creates a flusher that targets the given arena file and updates the control block on each flush. The masterDBPath is the writable temp file that WritableGraph mutates.

Call Start() to begin the coalescing goroutine, and Close() to stop it and perform a final flush.

func (*ArenaFlusher) Close added in v0.22.0

func (f *ArenaFlusher) Close() error

Close stops the coalescing goroutine and performs a final synchronous flush if dirty.

func (*ArenaFlusher) FlushNow added in v0.22.0

func (f *ArenaFlusher) FlushNow() error

FlushNow performs a synchronous flush. Use for final flush on unmount or when the caller needs to guarantee the arena is up-to-date.

func (*ArenaFlusher) RequestFlush added in v0.22.0

func (f *ArenaFlusher) RequestFlush()

RequestFlush marks the flusher as dirty. The coalescing goroutine will perform the actual flush on the next tick. Non-blocking, O(1).

func (*ArenaFlusher) Start added in v0.22.0

func (f *ArenaFlusher) Start(interval time.Duration)

Start begins the coalescing goroutine that flushes at most once per interval when dirty. Safe to call multiple times (idempotent).

type ArenaHeader added in v0.22.0

type ArenaHeader struct {
	Magic        uint32
	Version      uint8
	ActiveBuffer uint8
	Padding      [2]byte
	Sequence     uint64
	// DataSize is the exact byte length of the live payload in the
	// active buffer. Lets a reader hash `buf[..DataSize]` and compare
	// against the controller's current_root without parsing the
	// payload's own format (e.g. SQLite page count).
	DataSize uint64
}

ArenaHeader mirrors `rs/ll-core/core/src/layout.rs::ArenaHeader` in LLO. Byte layout: magic u32 | version u8 | active_buffer u8 | padding [2]u8 | sequence u64 | data_size u64 — total 24 bytes, aligned to a 4096-byte page.

func ReadArenaHeader added in v0.22.0

func ReadArenaHeader(f *os.File) (*ArenaHeader, error)

ReadArenaHeader reads the on-disk header from the file.

func (*ArenaHeader) CalculateActiveOffset added in v0.22.0

func (h *ArenaHeader) CalculateActiveOffset(fileSize int64) (int64, error)

CalculateArenaOffset returns the byte offset of the active buffer.

type CallExtractor

type CallExtractor func(content []byte, path, langName string) ([]QualifiedCall, error)

CallExtractor parses source code and returns qualified function call tokens. Used for on-demand "callees/" resolution. langName is the tree-sitter language identifier (e.g. "go", "python").

type Class added in v0.22.0

type Class struct {
	ID        int
	Label     string   // emergent: most-referenced token among members
	Members   []string // constituent node IDs
	InternalW float64  // total internal edge weight (density measure)
}

Class is an equivalence class of nodes (a community collapsed into one unit).

type Community added in v0.22.0

type Community struct {
	ID      int      // Community identifier
	Members []string // Node IDs belonging to this community
}

Community represents a cluster of densely-connected nodes detected by Louvain.

type CommunityResult added in v0.22.0

type CommunityResult struct {
	Communities []Community    // Detected communities, sorted by size descending
	Membership  map[string]int // Node ID → community ID
	Modularity  float64        // Final modularity score (0 to 1, higher = better partition)
	NumNodes    int            // Total nodes in the graph
	NumEdges    int            // Total edges (undirected)
	// PrunedTokens are hub tokens excluded from the projection, most-referenced
	// first. Reported rather than dropped silently: a caller reading these
	// communities is entitled to know which references did not shape them.
	PrunedTokens []PrunedToken
	// EffectiveMaxFanIn is the cap actually applied. It differs from the
	// requested one when the pair budget forced a tighter cap — reported so a
	// caller can tell "these are your communities" from "these are your
	// communities under a budget I had to impose".
	EffectiveMaxFanIn int
	// ProjectedPairs is the edge-insertion count the projection performed.
	ProjectedPairs int
}

CommunityResult holds the output of community detection.

func DetectCommunities added in v0.22.0

func DetectCommunities(refs map[string][]string, minCommunitySize int) *CommunityResult

DetectCommunities runs Louvain community detection on a bipartite refs graph. Input: refs maps token → []nodeID (which nodes reference that token). The algorithm projects this into a unipartite graph where two nodes are connected if they share at least one token, with edge weight = number of shared tokens. Returns communities of nodes that are densely co-referencing.

minCommunitySize filters out communities smaller than this (default 2 if 0).

func DetectCommunitiesWithFanIn added in v0.22.0

func DetectCommunitiesWithFanIn(refs map[string][]string, minCommunitySize, maxFanIn int) *CommunityResult

DetectCommunitiesWithFanIn is DetectCommunities with an explicit hub cap. maxFanIn <= 0 disables pruning and restores the pre-pruning behaviour, which is quadratic in the hottest token's fan-in — see DefaultMaxFanIn.

type CompositeGraph added in v0.5.0

type CompositeGraph struct {
	// contains filtered or unexported fields
}

CompositeGraph multiplexes multiple Graph backends under path prefixes. Mount "browser" → paths under /browser/ route to that sub-graph. Mount "iterm" → paths under /iterm/ route to that sub-graph. Root ListChildren returns the list of mount point names.

func NewCompositeGraph added in v0.5.0

func NewCompositeGraph() *CompositeGraph

NewCompositeGraph creates an empty composite graph.

func (*CompositeGraph) Act added in v0.22.0

func (c *CompositeGraph) Act(id, action, payload string) (*ActionResult, error)

Act implements Graph. Routes to the appropriate sub-graph.

func (*CompositeGraph) DefsMap added in v0.22.0

func (c *CompositeGraph) DefsMap() map[string][]string

DefsMap aggregates token → dir IDs across every mount that implements DefsMap(), prefixing each dir ID with its mount name. Sub-graphs that don't expose a defs index are skipped.

Lets find_definition and search work correctly on a composite graph: a token defined in mount A and mount B returns ["A/path/to/def", "B/path/to/def"] — agents see all definitions across mounts in one query.

func (*CompositeGraph) GetCallees added in v0.22.0

func (c *CompositeGraph) GetCallees(id string) ([]*Node, error)

GetCallees implements Graph. Routes to the local mount for in-mount resolution; if a cross-mount CallExtractor is configured (via SetCallExtractor), additionally resolves each extracted call against the federated DefsMap so callees living in OTHER mounts surface in the response. Local results win on dedupe; cross-mount results supplement.

func (*CompositeGraph) GetCallers added in v0.22.0

func (c *CompositeGraph) GetCallers(token string) ([]*Node, error)

GetCallers implements Graph. Searches all mounted sub-graphs.

func (*CompositeGraph) GetNode added in v0.22.0

func (c *CompositeGraph) GetNode(id string) (*Node, error)

GetNode implements Graph.

func (*CompositeGraph) Invalidate added in v0.22.0

func (c *CompositeGraph) Invalidate(id string)

Invalidate implements Graph.

func (*CompositeGraph) ListChildStats added in v0.22.0

func (c *CompositeGraph) ListChildStats(id string) ([]NodeStat, error)

ListChildStats implements Graph.

func (*CompositeGraph) ListChildren added in v0.22.0

func (c *CompositeGraph) ListChildren(id string) ([]string, error)

ListChildren implements Graph.

func (*CompositeGraph) LookupDef added in v0.22.0

func (c *CompositeGraph) LookupDef(token string) []string

LookupDef federates LookupDef across mounts: returns all dir IDs across every mount that defines `token`, prefixed with the mount name. Cheaper than DefsMap when only one token is wanted — MemoryStore + SQLiteGraph both expose LookupDef as O(1).

func (*CompositeGraph) Mount added in v0.22.0

func (c *CompositeGraph) Mount(prefix string, g Graph) error

Mount registers a sub-graph under the given prefix. Paths like "/<prefix>/..." are routed to this graph with the prefix stripped.

func (*CompositeGraph) MountPrefixOf added in v0.22.0

func (c *CompositeGraph) MountPrefixOf(id string) string

MountPrefixOf returns the mount-name prefix that would route id to a mounted sub-graph, or "" if id doesn't resolve to any mount (the virtual root or an unknown prefix).

MCP handlers use this to annotate cross-repo results with their mount of origin without having to parse node IDs themselves.

func (*CompositeGraph) ReadContent added in v0.22.0

func (c *CompositeGraph) ReadContent(id string, buf []byte, offset int64) (int, error)

ReadContent implements Graph.

func (*CompositeGraph) SearchDefs added in v0.22.0

func (c *CompositeGraph) SearchDefs(pattern string, limit int) map[string][]string

SearchDefs federates SearchDefs across every mount that implements it, then aggregates the results with mount-name prefixes applied to each node_id. Sub-graphs that don't expose SearchDefs (only DefsMap) fall back to a DefsMap iteration with sqlLikeMatch.

Without this method, find_definition / search role=definition on a CompositeGraph mounting an SQL-backed graph would silently return [] — SQLiteGraph's in-memory g.defs is empty for pre-built .db files, so aggregating DefsMap across mounts misses the real data that lives in node_defs. Each mount's SearchDefs handles its own SQL-vs-memory dispatch; this federates the results.

Bead from PR #373 post-fix audit: lazyGraph→CompositeGraph→ SQLiteGraph chain. lazyGraph satisfied the defsSearcher type assertion in the handler, but its inner (CompositeGraph) didn't implement SearchDefs, so lazyGraph returned nil. Handler fell through to DefsMap. CompositeGraph.DefsMap aggregated empty SQLiteGraph.DefsMaps. Result: [] for every pattern.

func (*CompositeGraph) SetCallExtractor added in v0.22.0

func (c *CompositeGraph) SetCallExtractor(fn CallExtractor)

SetCallExtractor wires a tree-sitter call extractor onto the composite for cross-mount callees resolution. With nil or no extractor set, GetCallees only consults the local mount's defs index — sub-graphs that already have their own extractor still resolve in-mount callees as before.

func (*CompositeGraph) SetCallExtractorPicker added in v0.22.0

func (c *CompositeGraph) SetCallExtractorPicker(picker func(local Graph) CallExtractor)

SetCallExtractorPicker wires a per-mount extractor selector. At extract time, GetCallees calls picker(localMount); a non-nil return value wins over the global extractor set via SetCallExtractor. Used to dispatch between CGO and pure-Go extractors based on the local mount's capabilities (e.g. presence of an `_ast` table). See ADR-0012.

Composing both: the picker is consulted first; if it returns nil, fall through to the SetCallExtractor-supplied global extractor. Callers that don't want per-mount dispatch can leave this unset.

func (*CompositeGraph) Unmount added in v0.22.0

func (c *CompositeGraph) Unmount(prefix string) error

Unmount removes a previously mounted sub-graph.

type ContentCache added in v0.22.0

type ContentCache struct {
	// contains filtered or unexported fields
}

ContentCache is a FIFO-bounded cache for resolved content bytes. Uses RWMutex so concurrent readers (MCP tool calls) don't block each other. Replaces the inline sync.Mutex caches that SQLiteGraph and WritableGraph previously maintained — the RWMutex promotion is safe because the old code never relied on exclusive read access (check-then-set unlocked between steps). Shared by MemoryStore, SQLiteGraph, and WritableGraph.

Note: the get→miss→resolve→put sequence in resolveContent is not atomic. Under high concurrency, multiple goroutines may miss the cache simultaneously and all invoke the resolver for the same key. This is benign — the first writer wins via Put's dedup check, and subsequent resolver results are discarded. The resolver (SQLite query + template render) is idempotent.

func NewContentCache added in v0.22.0

func NewContentCache(maxSize int) *ContentCache

NewContentCache creates a FIFO-bounded content cache.

func (*ContentCache) Delete added in v0.22.0

func (c *ContentCache) Delete(key string)

Delete removes a key from both the map and the keys slice. Invariant: entries and keys are always in sync — every key in the map has exactly one entry in the keys slice, maintained by Put (dedup) and Delete (linear scan + remove). The break-after-first-match in the scan is correct because Put never creates duplicate key entries.

func (*ContentCache) Get added in v0.22.0

func (c *ContentCache) Get(key string) ([]byte, bool)

Get retrieves cached content. Safe for concurrent readers.

func (*ContentCache) Put added in v0.22.0

func (c *ContentCache) Put(key string, value []byte)

Put stores content, evicting the oldest entry if at capacity. Deduplicates: if key already exists, updates the value without adding a duplicate key entry.

type ContentRef

type ContentRef struct {
	DBPath     string // Path to the SQLite database
	RecordID   string // Row ID in the results table
	Template   string // Content template to re-render
	ContentLen int64  // Pre-computed rendered byte length
}

ContentRef is a recipe for lazily resolving file content from a backing store. Instead of storing the full byte content in RAM, we store enough info to re-fetch it on demand.

type ContentResolverFunc

type ContentResolverFunc func(ref *ContentRef) ([]byte, error)

ContentResolverFunc resolves a ContentRef into byte content.

type DBPathProvider added in v0.22.0

type DBPathProvider interface {
	DBPath() string
}

DBPathProvider reports the path of the .db backing this graph, or "" when the backend has no file behind it.

"" is a documented sentinel meaning "no source", not an error: consumers that locate sibling artifacts by convention (ley-line-open writes a `.bindings.capnp` reference log next to the .db) must treat it as "nothing to consult" and carry on.

type DefTokener added in v0.22.0

type DefTokener interface {
	// DefTokenOf returns the token node_defs records for the node at id; ok
	// is false when the node defines nothing.
	DefTokenOf(id string) (token string, ok bool, err error)
}

DefTokener reports the symbol a node defines: the token node_defs records for it. A node id says nothing about its symbol on a leyline projection (it ends in a tree-sitter kind), and on projection-v6 it is an integer the caller never sees, so the answer has to come from the backend.

type DefsLookuper added in v0.22.0

type DefsLookuper interface {
	LookupDef(token string) []string
}

DefsLookuper answers "where is this exact symbol defined?" — the cheapest question on the symbol rung, and the one worth asking first. Backends implement it instead of DefsMapper wherever a single lookup can avoid snapshotting the whole index.

Returns the node IDs declaring token, or nil when nothing declares it.

type DefsMapper added in v0.22.0

type DefsMapper interface {
	DefsMap() map[string][]string
}

DefsMapper exposes the whole symbol index as token -> node IDs.

Prefer DefsLookuper or DefsSearcher: this copies the entire index, which is the expensive way to answer a question about one symbol. It earns its keep for whole-index work — building an overview, detecting duplicates.

type DefsNodeLookuper added in v0.22.0

type DefsNodeLookuper interface {
	LookupDefNodes(token string) ([]*Node, error)
}

DefsNodeLookuper is DefsLookuper's node-returning twin, for consumers that want the definition itself rather than its id.

Both exist deliberately. Ids are the right answer when the caller only needs identity — resolving nodes it will discard is waste — but returning ids from one accessor while Graph.GetCallers and Graph.GetCallees return nodes left every consumer writing the same conversion. That asymmetry is what drove downstream wrappers around this surface.

type DefsSearcher added in v0.22.0

type DefsSearcher interface {
	SearchDefs(pattern string, limit int) map[string][]string
}

DefsSearcher answers "what symbols look like this?" using SQL LIKE syntax ('%' matches any run, '_' a single character), capped at limit. The step between knowing a name and knowing only its shape.

type ErrGraphNotCached added in v0.22.0

type ErrGraphNotCached struct {
	Repo string
}

ErrGraphNotCached is returned by Get/Head when no graph exists for the repo.

func (*ErrGraphNotCached) Error added in v0.22.0

func (e *ErrGraphNotCached) Error() string

type Granularity added in v0.22.0

type Granularity string

Granularity selects the cell size community detection runs over.

The filesystem hierarchy is already encoded in every node id — `cmd/serve.go/function_declaration_1/block/...` IS fs→AST — so coarser cells are a pure prefix operation on the ids mache already has. No re-ingestion, no second index.

Why this exists: at construct granularity the projection runs over every AST node, so on mache itself the largest communities were things like `CHANGELOG.md/section/...` and individual test-function bodies — bounded and well-separated (modularity 0.985) but useless for "how should cmd/ be split". That question is ABOUT files and directories, so the cells must be files or directories.

const (
	// GranularityConstruct is the raw projection over AST-level node ids.
	GranularityConstruct Granularity = "construct"
	// GranularityFile aggregates every node to its source file.
	GranularityFile Granularity = "file"
	// GranularityDir aggregates every node to its file's directory.
	GranularityDir Granularity = "dir"
)

type Graph

type Graph interface {
	GetNode(id string) (*Node, error)
	ListChildren(id string) ([]string, error)
	// ListChildStats returns stat snapshots for all children of a directory.
	// Eliminates N individual GetNode calls during FUSE/NFS readdir.
	// Returns []NodeStat (value types, safe to read without a lock).
	ListChildStats(id string) ([]NodeStat, error)
	ReadContent(id string, buf []byte, offset int64) (int, error)
	GetCallers(token string) ([]*Node, error)
	GetCallees(id string) ([]*Node, error)
	// Invalidate evicts cached data for a node (size, content).
	// Called after write-back to force re-render on next access.
	Invalidate(id string)
	// Act performs an action on the node at the given path.
	// Interactive graphs (browser DOM, terminal sessions, macOS AX elements)
	// implement real actions. Passive graphs (code, data) return ErrActNotSupported.
	Act(id, action, payload string) (*ActionResult, error)
}

Graph is the interface for the FUSE layer. This allows us to swap the backend later (Memory -> SQLite -> Mmap).

type GraphCache added in v0.6.0

type GraphCache struct {
	// contains filtered or unexported fields
}

GraphCache is a thread-safe MemoryStore with automatic SQLite write-through persistence. It provides tree manipulation helpers for building and querying hierarchical node structures.

Mutations acquire the write lock, modify the underlying MemoryStore, then call ExportSQLite to flush the entire graph to disk. On construction, ImportSQLite loads any previously persisted state.

Domain-specific logic (fingerprint matching, eviction policies, custom node layouts) is NOT part of GraphCache. Consumers build those on top using the tree manipulation primitives and the Store() escape hatch.

func NewGraphCache added in v0.6.0

func NewGraphCache(dbPath string) *GraphCache

NewGraphCache creates a GraphCache backed by SQLite at dbPath. If dbPath is empty, the cache is purely in-memory (suitable for tests). If dbPath points to an existing SQLite file, the previously persisted graph is loaded via ImportSQLite.

func (*GraphCache) AppendChild added in v0.6.0

func (c *GraphCache) AppendChild(parentID, childID string) bool

AppendChild adds childID to the Children list of parentID if not already present. Returns false if the parent does not exist.

Copy-on-write: publishes a new *Node into the store rather than mutating the live parent in place, so readers holding a *Node from GetNode see an immutable snapshot.

func (*GraphCache) Batch added in v0.6.0

func (c *GraphCache) Batch(fn func(*MemoryStore)) error

Batch acquires the write lock, calls fn with the underlying MemoryStore, then persists once. Use this for multi-step mutations that must be atomic (no concurrent readers see intermediate state) and persist as a single SQLite write instead of N individual writes.

This is the correct way to implement complex operations like "backup sections, clear zones, rebuild zones, restore matching sections" — the entire sequence runs under one lock with one persist at the end.

func (*GraphCache) ClearChildren added in v0.6.0

func (c *GraphCache) ClearChildren(id string) bool

ClearChildren sets the Children list of a directory node to empty. Returns false if the node does not exist. Combined with RemoveChild on the parent, this effectively "deletes" a subtree: ExportSQLite only walks from roots, so orphaned nodes are not persisted.

Copy-on-write: see AppendChild.

func (*GraphCache) Close added in v0.6.0

func (c *GraphCache) Close() error

Close is a no-op. ExportSQLite manages its own DB connections per call.

func (*GraphCache) GetData added in v0.6.0

func (c *GraphCache) GetData(id string) ([]byte, bool)

GetData returns a copy of the Data field of a file node, or (nil, false) if the node does not exist or has no data. The returned slice is detached from the live store.

func (*GraphCache) GetNode added in v0.6.0

func (c *GraphCache) GetNode(id string) (*Node, bool)

GetNode returns a shallow copy of the node at the given id, or (nil, false) if not found. The returned *Node's Children and Data slices are detached from the live store so the caller may read them without holding the cache lock and without racing against concurrent mutations.

func (*GraphCache) ListChildren added in v0.6.0

func (c *GraphCache) ListChildren(id string) ([]string, bool)

ListChildren returns a copy of the Children of a directory node, or (nil, false) if the node does not exist or is not a directory. The returned slice is detached from the live store.

func (*GraphCache) Persist added in v0.6.0

func (c *GraphCache) Persist() error

Persist flushes the entire graph to SQLite. Called automatically by mutation methods. Use explicitly after direct Store() access.

func (*GraphCache) PutDir added in v0.6.0

func (c *GraphCache) PutDir(id string, isRoot bool) bool

PutDir ensures a directory node exists at the given id. If isRoot is true and the node is new, it is registered as a root node. Returns true if a new node was created.

func (*GraphCache) PutFile added in v0.6.0

func (c *GraphCache) PutFile(id string, data []byte)

PutFile creates or overwrites a file node with the given data. The node's ModTime is set to time.Now().

func (*GraphCache) RemoveChild added in v0.6.0

func (c *GraphCache) RemoveChild(parentID, childID string) bool

RemoveChild removes childID from the Children list of parentID. Returns false if the parent does not exist.

Copy-on-write: see AppendChild.

func (*GraphCache) RootIDs added in v0.6.0

func (c *GraphCache) RootIDs() []string

RootIDs returns all root node IDs.

func (*GraphCache) Store added in v0.6.0

func (c *GraphCache) Store() *MemoryStore

Store returns the underlying MemoryStore for direct access. Use this for batch operations: mutate the store directly, then call Persist() once. Callers are responsible for coordinating with GraphCache.mu if mixing direct store access with GraphCache mutation methods.

type GraphMeta added in v0.22.0

type GraphMeta struct {
	Repo       string    // e.g., "github.com/org/repo"
	Generation uint64    // monotonic version counter
	Size       int64     // .db file size in bytes
	ModTime    time.Time // last update time
	ETag       string    // content hash for conditional gets (optional)
}

GraphMeta is metadata about a persisted graph.

type GraphStore added in v0.22.0

type GraphStore interface {
	// Put persists a graph database for the given repo.
	// The reader provides the SQLite .db bytes. Generation is an
	// opaque version counter (monotonically increasing).
	Put(ctx context.Context, repo string, generation uint64, r io.Reader) error

	// Get retrieves the latest graph database for the given repo.
	// Returns the reader, generation, and modification time.
	// Returns ErrGraphNotCached if no graph exists for this repo.
	Get(ctx context.Context, repo string) (io.ReadCloser, uint64, error)

	// Head returns metadata without fetching the full database.
	// Used for cache validation (is my local copy still current?).
	Head(ctx context.Context, repo string) (*GraphMeta, error)

	// Delete removes a persisted graph for the given repo.
	Delete(ctx context.Context, repo string) error

	// List returns all repos with persisted graphs.
	// Use for garbage collection and admin tooling.
	List(ctx context.Context) ([]GraphMeta, error)
}

GraphStore persists and retrieves projected graph databases. The key is a repo identifier (e.g., "github.com/org/repo") and the value is a SQLite .db file containing the nodes/refs/defs tables.

type HotSwapGraph added in v0.5.2

type HotSwapGraph struct {
	// contains filtered or unexported fields
}

HotSwapGraph is a thread-safe wrapper that allows swapping the underlying graph instance.

func NewHotSwapGraph added in v0.5.2

func NewHotSwapGraph(initial Graph) *HotSwapGraph

func (*HotSwapGraph) Act added in v0.22.0

func (h *HotSwapGraph) Act(id, action, payload string) (*ActionResult, error)

Act delegates to current graph.

func (*HotSwapGraph) GetCallees added in v0.22.0

func (h *HotSwapGraph) GetCallees(id string) ([]*Node, error)

GetCallees delegates to current graph.

func (*HotSwapGraph) GetCallers added in v0.22.0

func (h *HotSwapGraph) GetCallers(token string) ([]*Node, error)

GetCallers delegates to current graph.

func (*HotSwapGraph) GetNode added in v0.22.0

func (h *HotSwapGraph) GetNode(id string) (*Node, error)

GetNode delegates to current graph.

func (*HotSwapGraph) Invalidate added in v0.22.0

func (h *HotSwapGraph) Invalidate(id string)

Invalidate delegates to current graph.

func (*HotSwapGraph) ListChildStats added in v0.22.0

func (h *HotSwapGraph) ListChildStats(id string) ([]NodeStat, error)

ListChildStats delegates to current graph.

func (*HotSwapGraph) ListChildren added in v0.22.0

func (h *HotSwapGraph) ListChildren(id string) ([]string, error)

ListChildren delegates to current graph.

func (*HotSwapGraph) ReadContent added in v0.22.0

func (h *HotSwapGraph) ReadContent(id string, buf []byte, offset int64) (int, error)

ReadContent delegates to current graph.

func (*HotSwapGraph) Swap added in v0.22.0

func (h *HotSwapGraph) Swap(newGraph Graph)

Swap atomically replaces the current graph with a new one. It closes the old graph (if it implements Closer, though Graph interface doesn't enforce it). Ideally, Graph implementations should be safe to close.

type IndexStaleness added in v0.22.0

type IndexStaleness struct {
	// BuiltAt is when the index was produced.
	BuiltAt time.Time `json:"built_at"`
	// SourceRoot is the tree it was built from.
	SourceRoot string `json:"source_root"`
	// ModifiedSince is how many source files changed after BuiltAt, capped at
	// staleScanCap — the point is "should I care", not a manifest.
	ModifiedSince int `json:"modified_since"`
	// DeletedSince counts indexed source files that no longer exist on disk.
	// Exact, not heuristic: the .db records every file it indexed
	// (nodes.source_file), so a missing path is a fact. A rename counts here
	// too — the indexed path IS gone, and the mtime walk cannot see it
	// (mv preserves mtimes, so the new path does not read as modified
	// either; without this field a rename was doubly invisible).
	DeletedSince int `json:"deleted_since,omitempty"`
	// Capped reports that a count hit the cap and the true number is larger.
	Capped bool `json:"capped,omitempty"`
}

IndexStaleness is how far a projected graph has drifted from the tree it was built from — the honest answer to "can I trust this index?".

Why it exists (mache-6c9e1d): the flagship serve path parses the repo ONCE at session start and serves that frozen .db for the session's whole life. Every edit after that is invisible to search/find_definition, and the longer and more productive a session, the wronger the answers — with nothing anywhere saying so. Until the live-reparse path exists, the warn-don't-lie floor is to REPORT the drift.

type LocalStore added in v0.22.0

type LocalStore struct {
	BaseDir string
}

LocalStore persists graphs to a local directory. Each repo gets a subdirectory: {base}/{repo-hash}/graph.db

func NewLocalStore added in v0.22.0

func NewLocalStore(baseDir string) *LocalStore

NewLocalStore creates a LocalStore backed by the given directory.

func (*LocalStore) Delete added in v0.22.0

func (s *LocalStore) Delete(_ context.Context, repo string) error

Delete removes a persisted graph from local disk.

func (*LocalStore) Get added in v0.22.0

func (s *LocalStore) Get(_ context.Context, repo string) (io.ReadCloser, uint64, error)

Get retrieves the graph database from local disk.

func (*LocalStore) Head added in v0.22.0

func (s *LocalStore) Head(_ context.Context, repo string) (*GraphMeta, error)

Head returns metadata without reading the full database.

func (*LocalStore) List added in v0.22.0

func (s *LocalStore) List(_ context.Context) ([]GraphMeta, error)

List returns all repos with persisted graphs.

func (*LocalStore) Put added in v0.22.0

func (s *LocalStore) Put(_ context.Context, repo string, generation uint64, r io.Reader) error

Put persists a graph database to local disk.

type MemoryStore

type MemoryStore struct {

	// Diagnostics: last write status per node path (for _diagnostics/ virtual dir).
	WriteStatus sync.Map // node path (string) → error message (string)
	// contains filtered or unexported fields
}

func ImportSQLite added in v0.4.0

func ImportSQLite(dbPath string) (*MemoryStore, error)

ImportSQLite reads nodes from a SQLite database into a new MemoryStore. The database must have a nodes table in the standard mache format.

This ONLY replicates the node tree (GetNode/ListChildren/ReadContent work on the result). It does not read node_defs/node_refs, so the returned MemoryStore's LookupDef returns nil for every token and QueryRefs errors "refsDB not initialized" — those indices live in MemoryStore's own AddDef/AddRef/InitRefsDB+FlushRefs machinery, which this function never calls. For a mache-produced .db (mache build / leyline parse) where you need LookupDef/QueryRefs/GetCallers working, use Open instead: it opens a *SQLiteGraph, which answers those directly against the file's own node_defs/node_refs tables with no import step at all.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

func (*MemoryStore) Act added in v0.22.0

func (s *MemoryStore) Act(id, action, payload string) (*ActionResult, error)

Act returns ErrActNotSupported — MemoryStore is a passive code graph.

func (*MemoryStore) AddDef added in v0.22.0

func (s *MemoryStore) AddDef(token, dirID string) error

AddDef records that a construct (dirID) defines the given token. Used by callees/ resolution: token → where it is defined. Uses copy-on-write: creates a new slice instead of appending to the existing one, so concurrent readers (GetCallees holds RLock) never see a partially-updated slice.

func (*MemoryStore) AddFileChildren added in v0.22.0

func (s *MemoryStore) AddFileChildren(parent *Node, files []*Node)

AddFileChildren atomically adds file nodes and appends their IDs to the parent directory's Children slice. Single lock acquisition for the batch.

Copy-on-write: publishes a NEW *Node into the map rather than mutating the caller's parent in place. Readers that obtained the previous *Node pointer from GetNode see an immutable snapshot of the old Children slice; subsequent GetNode callers see the updated node. This avoids a data race where a reader holds a *Node pointer (lock released) while a writer appends to the same Children slice.

Side effect: the caller's `parent` pointer is stale after this call — further mutations via that pointer are not reflected in the store.

func (*MemoryStore) AddNode added in v0.22.0

func (s *MemoryStore) AddNode(n *Node)

AddNode adds a non-root node to the store.

func (*MemoryStore) AddRef added in v0.22.0

func (s *MemoryStore) AddRef(token, nodeID string) error

AddRef records a reference from a file (nodeID) to a token.

func (*MemoryStore) AddRoot added in v0.22.0

func (s *MemoryStore) AddRoot(n *Node)

AddRoot registers a node as a top-level root and adds it to the store. Callers must explicitly declare roots — there is no heuristic.

func (*MemoryStore) Close added in v0.22.0

func (s *MemoryStore) Close() error

Close closes the refs database and removes the temp file.

func (*MemoryStore) DefsMap added in v0.22.0

func (s *MemoryStore) DefsMap() map[string][]string

DefsMap returns a snapshot of the token→dirIDs definition map. Used by find_definition's case-insensitive and fuzzy paths and by search role=definition — anywhere callers need to iterate. The anchored-exact path in find_definition should use LookupDef instead to avoid the O(N) snapshot copy.

Memoized — see RefsMap for the cache contract. Same read-only constraint applies. Callers that need a mutable copy should either deep-copy the result themselves or use LookupDef for per-token lookups.

func (*MemoryStore) DeleteFileNodes added in v0.22.0

func (s *MemoryStore) DeleteFileNodes(filePath string)

DeleteFileNodes removes all nodes that originated from the given source file. Uses the roaring bitmap index for O(k) lookup instead of O(N) full scan.

func (*MemoryStore) DeleteNodes added in v0.22.0

func (s *MemoryStore) DeleteNodes(ids []string)

DeleteNodes removes the named nodes and everything that pointed at them, whatever file they came from.

DeleteFileNodes cannot do this job. It finds nodes through the file→nodes bitmap, which indexNode only populates for nodes carrying an Origin — and a CONSTRUCT DIRECTORY has no Origin, only its source leaf does. So a re-ingest replaced `fns/alpha/source` and left `fns/alpha` behind as an empty husk, forever, one per edit (mache-399c25). The engine knows exactly which construct IDs a file claimed; this is how it hands them back.

func (*MemoryStore) FileMtime added in v0.22.0

func (s *MemoryStore) FileMtime(filePath string) time.Time

FileMtime returns the tracked mtime for a source file. Returns zero time if the file is not tracked.

func (*MemoryStore) FlushRefs added in v0.22.0

func (s *MemoryStore) FlushRefs() error

FlushRefs writes all accumulated refs (from AddRef) into the in-memory SQLite sidecar as roaring bitmaps. Guarded by sync.Once — safe to call multiple times; only the first call performs the flush.

func (*MemoryStore) ForgetFile added in v0.22.0

func (s *MemoryStore) ForgetFile(filePath string)

ForgetFile drops path's tracked mtime, so a later incremental pass does not treat a file that is gone as merely unchanged. The counterpart of RecordFileMtime; the nodes themselves come out through DeleteFileNodes and DeleteNodes (mache-31abc0).

func (*MemoryStore) GetCallees added in v0.22.0

func (s *MemoryStore) GetCallees(id string) ([]*Node, error)

GetCallees implements Graph. It extracts calls made by the construct, then looks up those tokens in the defs index to find definitions.

Three extraction paths are supported:

  1. AST-scoped (preferred): when the construct's Properties carry "ast_source_id"/"ast_scope_id" (persisted at projection time by engine_walk.go — see ingest.ASTScope) AND a scoped extractor is wired (requires a LIVE `_ast` table), GetCallees queries the pre-parsed `_ast` table directly, scoped to this construct. This is the fix for bead mache-fd9982: the legacy path below fed the GRAPH node id (e.g. "cmd/functions/evalOrAbs") as if it were the `_ast` source_id, which matched zero rows every time.
  2. node_refs fallback: when #1 is unavailable but the construct has a "source" file child, resolve its calls from s.refs (token -> []nodeID) instead — the same index GetCallers reads, inverted. s.refs already holds every call token ScopeCalls found for this construct at projection time (engine_walk.go's store.AddRef(token, sourceFileID)). Bare-token only (no qualifier fidelity) — strictly better than returning [] (bead mache-6fbaf1).
  3. Legacy content-based extractor: re-parses the construct's "source" file content. Kept for non-AST backends (or .dbs projected before this fix that lack the ast_source_id/ast_scope_id Properties AND have no node_refs coverage).

func (*MemoryStore) GetCallers added in v0.22.0

func (s *MemoryStore) GetCallers(token string) ([]*Node, error)

GetCallers implements Graph.

func (*MemoryStore) GetNode added in v0.22.0

func (s *MemoryStore) GetNode(id string) (*Node, error)

GetNode implements Graph.

func (*MemoryStore) InitRefsDB added in v0.22.0

func (s *MemoryStore) InitRefsDB() error

InitRefsDB opens an in-memory SQLite database with the same schema as SQLiteGraph's sidecar (node_refs + file_ids + mache_refs vtab). Must be called before FlushRefs. Safe to call multiple times (idempotent).

func (*MemoryStore) Invalidate added in v0.22.0

func (s *MemoryStore) Invalidate(id string)

Invalidate is a no-op for MemoryStore — nodes are updated in-place.

func (*MemoryStore) IsFileStale added in v0.22.0

func (s *MemoryStore) IsFileStale(filePath string) bool

IsFileStale returns true if the source file's current mtime differs from the tracked mtime (i.e., the file has been modified since indexing).

func (*MemoryStore) ListChildStats added in v0.22.0

func (s *MemoryStore) ListChildStats(id string) ([]NodeStat, error)

ListChildStats returns stat snapshots for all children under a single RLock. Returns []NodeStat (value types, no aliasing) — safe to read without a lock. Eliminates N individual GetNode calls during FUSE/NFS readdir. Missing children are silently skipped.

func (*MemoryStore) ListChildren added in v0.22.0

func (s *MemoryStore) ListChildren(id string) ([]string, error)

ListChildren implements Graph.

func (*MemoryStore) LookupDef added in v0.22.0

func (s *MemoryStore) LookupDef(token string) []string

LookupDef returns the dir IDs that define `token` without snapshotting the entire defs map. Returns nil for unknown tokens. O(1) under the read lock; the returned slice is a copy so callers can mutate it without races. Pair with DefsMap when the caller needs to iterate (case-insensitive matching, fuzzy substring, etc.); use this when one specific token's IDs are wanted.

func (*MemoryStore) NodesForPath added in v0.22.0

func (s *MemoryStore) NodesForPath(filePath string) []string

NodesForPath returns the IDs of every node whose origin file is filePath, in deterministic order. The path is canonicalised via filepath.EvalSymlinks to match the bookkeeping Ingest does. Returns an empty slice when no nodes are indexed under that path.

O(k) via the file→nodes bitmap; falls back to a full O(N) scan only when a path was added before the bitmap was populated (the same fallback path DeleteFileNodes uses, kept for parity).

Used by the file-watcher → SheafInvalidator wiring (cmd/serve.go) so the sheaf cascade has a node ID to look up the region for. Exposed via the NodesForPathProvider interface so non-MemoryStore backends can plug in without leaking *MemoryStore type assertions into serve.go.

func (*MemoryStore) QueryRefs added in v0.22.0

func (s *MemoryStore) QueryRefs(query string, args ...any) (*sql.Rows, error)

QueryRefs executes a SQL query against the in-memory refs database, which includes the mache_refs virtual table.

func (*MemoryStore) ReadContent added in v0.22.0

func (s *MemoryStore) ReadContent(id string, buf []byte, offset int64) (int, error)

ReadContent implements Graph. It handles both inline and lazy content. If the node has a SourceOrigin and the source file's mtime has changed, the refresher callback is invoked to re-ingest the file before reading.

func (*MemoryStore) RecordFileMtime added in v0.22.0

func (s *MemoryStore) RecordFileMtime(filePath string, mtime time.Time)

RecordFileMtime explicitly records the mtime for a source file. Called after re-ingestion to update the tracked mtime.

func (*MemoryStore) RefsMap added in v0.22.0

func (s *MemoryStore) RefsMap() map[string][]string

RefsMap returns a snapshot of the token→nodeIDs reference map. Used by community detection to build the co-reference graph.

Memoized: the first call after an invalidation builds the deep- copy snapshot; subsequent calls return the same instance until the next AddRef / DeleteFileNodes. Callers MUST treat the returned map as read-only — mutating it corrupts the cache for the next caller. Every existing caller (community detection, composite graph wiring) is read-only.

func (*MemoryStore) ReplaceFileNodes added in v0.22.0

func (s *MemoryStore) ReplaceFileNodes(filePath string, newNodes []*Node)

ReplaceFileNodes atomically replaces all nodes from a file with a new set. This prevents race conditions where files disappear during re-ingestion.

func (*MemoryStore) RootIDs added in v0.22.0

func (s *MemoryStore) RootIDs() []string

RootIDs returns a copy of the top-level root node IDs.

func (*MemoryStore) SearchDefs added in v0.22.0

func (s *MemoryStore) SearchDefs(pattern string, limit int) map[string][]string

SearchDefs returns up to `limit` token→nodeIDs entries whose token matches the SQL LIKE pattern. Mirrors SQLiteGraph.SearchDefs for dispatch uniformity — every backend that has a defs map should also be searchable, so the handler's defsSearcher check resolves the same regardless of backend (bead from PR #373 post-fix audit: lazyGraph wrapping a MemoryStore previously returned nil from SearchDefs because MemoryStore didn't implement it, and the handler's defsMapProvider fallback was unreachable).

func (*MemoryStore) SearchRefs added in v0.22.0

func (s *MemoryStore) SearchRefs(pattern, segment string, limit int) ([]RefHit, error)

SearchRefs implements RefSearcher over the mache_refs sidecar.

func (*MemoryStore) SetCallExtractor added in v0.22.0

func (s *MemoryStore) SetCallExtractor(fn CallExtractor)

SetCallExtractor configures the parser for on-demand callee resolution.

func (*MemoryStore) SetRefresher added in v0.22.0

func (s *MemoryStore) SetRefresher(fn func(filePath string) error)

SetRefresher configures a callback invoked when a source file is stale. The callback should re-ingest the file and update the store.

func (*MemoryStore) SetResolver added in v0.22.0

func (s *MemoryStore) SetResolver(fn ContentResolverFunc)

SetResolver configures lazy content resolution for nodes with ContentRef. Cache size scales with node count: 25% of nodes, floor 1024, ceiling 16384.

func (*MemoryStore) SetScopedCallExtractor added in v0.22.0

func (s *MemoryStore) SetScopedCallExtractor(fn ScopedCallExtractor)

SetScopedCallExtractor configures the AST-scoped extractor GetCallees prefers when a construct carries ast_source_id/ast_scope_id Properties.

func (*MemoryStore) ShiftOrigins added in v0.22.0

func (s *MemoryStore) ShiftOrigins(filePath string, afterByte uint32, delta int32)

ShiftOrigins adjusts StartByte/EndByte for all nodes from filePath whose origin starts at or after afterByte. delta is the signed byte count change (positive = content grew, negative = content shrank). Called after splice, BEFORE re-ingest, to keep sibling offsets correct.

func (*MemoryStore) UpdateNodeContent added in v0.22.0

func (s *MemoryStore) UpdateNodeContent(id string, data []byte, origin *SourceOrigin, modTime time.Time) error

UpdateNodeContent surgically updates a node's content and origin in-place. Preserves Children, Context, Properties, and Ref. Clears DraftData on success.

func (*MemoryStore) UpdateNodeContext added in v0.22.0

func (s *MemoryStore) UpdateNodeContext(id string, ctx []byte) error

UpdateNodeContext updates the Context field on a node (e.g., imports/package).

type MermaidOpts added in v0.22.0

type MermaidOpts struct {
	Layout  string // "TD", "LR", "BT", "RL" (default "TD")
	Compact bool   // omit member listings inside subgraphs
}

MermaidOpts controls diagram rendering.

type MountPrefixer added in v0.22.0

type MountPrefixer interface {
	MountPrefixOf(id string) string
}

MountPrefixer reports which mount a node ID came from, or "" for the base graph — the disambiguation a composite graph needs once a single query can answer across more than one repository.

type Node

type Node struct {
	ID        string
	Mode      fs.FileMode // fs.ModeDir for directories, 0 for regular files
	ModTime   time.Time   // Modification time
	Data      []byte      // Inline content (small files, nil for lazy nodes)
	Context   []byte      // Context content (imports/globals, for virtual 'context' file)
	DraftData []byte      // Draft content (uncommitted/invalid edits)
	Ref       *ContentRef // Lazy content reference (large files, nil for inline nodes)
	// Properties holds node metadata as JSON values (lang/pkg/imports/location/
	// ast_*). Access it only via PropString/PropRaw and their setters — the
	// encoding is their business (mache-90b89b).
	Properties map[string]json.RawMessage

	Children []string      // Child node IDs (directories only)
	Origin   *SourceOrigin // Source byte range (nil for dirs, JSON, SQLite nodes)
}

Node is the universal primitive. The Mode field explicitly declares whether this is a file or directory.

func (*Node) ContentSize added in v0.22.0

func (n *Node) ContentSize() int64

ContentSize returns the byte length of this node's content, regardless of whether it is inline or lazy.

type NodeStat added in v0.22.0

type NodeStat struct {
	ID          string
	IsDir       bool
	ContentSize int64
	ModTime     time.Time
	HasOrigin   bool // true if write-back is possible (Origin != nil)
}

NodeStat holds the immutable stat fields needed for directory listing. Value type — no pointers to mutable store data. Copying a NodeStat gives the caller a frozen snapshot that is safe to read without any lock.

type NodesForPathProvider added in v0.22.0

type NodesForPathProvider interface {
	NodesForPath(filePath string) []string
}

NodesForPathProvider is implemented by graph backends that can answer "which node IDs originated from this file?". MemoryStore implements it; other backends (SQLiteGraph, CompositeGraph) may either implement it directly or compose it via type assertion on their inner store.

Kept as a separate interface (rather than added to Graph) so consumers that don't need this query — most graph readers — aren't forced to implement it. Watcher-driven invalidation paths type-assert and degrade gracefully when the backing store doesn't satisfy the interface.

type NodesTableReader added in v0.22.0

type NodesTableReader struct {
	// contains filtered or unexported fields
}

NodesTableReader provides read methods for the nodes-table schema. Shared by SQLiteGraph (nodes-table fast path) and WritableGraph.

Parameterized by FileMode/DirMode so the same SQL queries produce read-only (0o444/0o555) or writable (0o644/0o755) nodes.

The caller owns the *sql.DB lifecycle — NodesTableReader holds a reference but does not close it.

func NewNodesTableReader added in v0.22.0

func NewNodesTableReader(db *sql.DB, tableName string, render TemplateRenderer,
	levels []*schemaLevel, fileMode, dirMode os.FileMode, cacheSize int,
) *NodesTableReader

NewNodesTableReader creates a reader for the nodes-table schema.

func (*NodesTableReader) Close added in v0.22.0

func (r *NodesTableReader) Close()

Close releases the reader's compiled statements. It does NOT close the *sql.DB, which the caller owns; call it before closing the db so the statements are released against a live connection.

func (*NodesTableReader) DB added in v0.22.0

func (r *NodesTableReader) DB() *sql.DB

DB returns the underlying database connection. Used by WritableGraph for write operations and Close.

func (*NodesTableReader) GetCallers added in v0.22.0

func (r *NodesTableReader) GetCallers(token string) ([]*Node, error)

GetCallers returns nodes that reference the given token via node_refs table.

func (*NodesTableReader) GetNode added in v0.22.0

func (r *NodesTableReader) GetNode(id string) (*Node, error)

GetNode returns a node by ID from the nodes table.

func (*NodesTableReader) Invalidate added in v0.22.0

func (r *NodesTableReader) Invalidate(id string)

Invalidate evicts cached content and size for a node.

func (*NodesTableReader) ListChildStats added in v0.22.0

func (r *NodesTableReader) ListChildStats(id string) ([]NodeStat, error)

ListChildStats returns stat snapshots for all children without rendering content.

func (*NodesTableReader) ListChildren added in v0.22.0

func (r *NodesTableReader) ListChildren(id string) ([]string, error)

ListChildren returns child IDs for a directory from the nodes table.

Empty-result contract: when the directory has no children, the returned slice is nil (not an empty slice). All callers must handle nil correctly — len(nil) is 0, range over nil is a no-op, and JSON-encoding nil produces null. Don't change to []string{} without auditing JSON consumers (the MCP serve_handlers list_directory shapes its own non-nil response).

Bead mache-0375fe.

func (*NodesTableReader) ReadContent added in v0.22.0

func (r *NodesTableReader) ReadContent(id string, buf []byte, offset int64) (int, error)

ReadContent resolves content and copies into buf at offset.

type PrunedToken added in v0.22.0

type PrunedToken struct {
	Token string `json:"token"`
	FanIn int    `json:"fan_in"`
}

PrunedToken is a token excluded for exceeding the fan-in cap.

type QualifiedCall

type QualifiedCall struct {
	Token     string // Function/method name (e.g., "Validate")
	Qualifier string // Package qualifier (e.g., "auth"); empty for unqualified calls
}

QualifiedCall represents a function call with optional package qualifier.

type QuotientEdge added in v0.22.0

type QuotientEdge struct {
	From         int                // class index (invariant: From < To)
	To           int                // class index
	Weight       float64            // aggregated cross-class coupling
	Tokens       []string           // boundary tokens creating this edge
	TokenWeights map[string]float64 // per-token contribution to Weight
}

QuotientEdge is an aggregated edge between two classes.

type QuotientGraph added in v0.22.0

type QuotientGraph struct {
	Classes []Class
	Edges   []QuotientEdge
	ClassOf map[string]int // node ID → class index
}

QuotientGraph compresses a full node graph into a diagram-scale object by collapsing nodes into equivalence classes (communities) and aggregating inter-class edges. The quotient G/P is uniquely determined by the partition P.

func ComputeQuotient added in v0.22.0

func ComputeQuotient(cr *CommunityResult, refs map[string][]string) *QuotientGraph

ComputeQuotient builds a QuotientGraph from a community partition and refs.

The algorithm mirrors buildRestrictions in sheaf.go: for each ref token referenced by nodes in multiple communities, create an inter-class edge with weight = product of member counts (bipartite projection weight).

Class labels are derived from the most-referenced token among class members. Ties are broken lexicographically for determinism.

func (*QuotientGraph) Mermaid added in v0.22.0

func (q *QuotientGraph) Mermaid(layout string) string

Mermaid renders the quotient graph as mermaid syntax.

Each Class becomes a subgraph (or a plain node if it has a single member). Each QuotientEdge becomes an arrow. Edge annotations show boundary tokens that contribute above the mean weight for that edge.

func (*QuotientGraph) MermaidWithOpts added in v0.22.0

func (q *QuotientGraph) MermaidWithOpts(opts MermaidOpts) string

MermaidWithOpts renders the quotient graph with full control over output.

type R2Store added in v0.22.0

type R2Store struct {
	Bucket    string
	AccountID string
	AccessKey string
	SecretKey string
}

R2Store persists graphs to Cloudflare R2. Key format: graphs/{repo-hash}/graph.db Debounce writes to avoid thrashing (caller's responsibility).

type RefHit added in v0.22.0

type RefHit struct {
	Token string
	ID    string
}

RefHit is one reference search result: the token referenced and the node that references it.

type RefRange added in v0.22.0

type RefRange struct {
	SourceID  string
	StartLine uint32
	StartCol  uint32
	EndLine   uint32
	EndCol    uint32
	StartByte uint32
	EndByte   uint32
}

RefRange is a reference's span in its source file. Lines and columns are 1-BASED, matching what an editor shows; the underlying `_ast` rows are tree-sitter's 0-based values and are converted here so no consumer has to remember which convention it is holding.

type RefResolution added in v0.22.0

type RefResolution string

Resolution says WHY a reference did or did not reach a definition. It exists because "no node id" has three different meanings for a consumer walking a blast radius, and collapsing them is how a tool reports the wrong radius with full confidence.

Measured on this repo's own projection (123,117 refs), the split is not close: 10.8% resolve uniquely, 13.2% are external, 17.8% are genuinely ambiguous, and 58.1% have no local definition at all. A boolean "resolved?" would put 89% of references in one undifferentiated bucket.

const (
	// RefResolved: exactly one definition in this projection. Follow it.
	RefResolved RefResolution = "resolved"
	// RefExternal: the reference is qualified by an import that leaves this
	// projection (stdlib, a third-party module). There is deliberately no
	// local target, and the local blast radius is genuinely zero — this is an
	// ANSWER, not a failure. Matching such a token against a same-named local
	// definition is the bug this value exists to prevent.
	RefExternal RefResolution = "external"
	// RefAmbiguous: several definitions share the token and nothing available
	// narrows it. Candidates are returned; the caller decides, because only
	// the caller knows whether a guess is acceptable.
	RefAmbiguous RefResolution = "ambiguous"
	// RefNoTarget: no definition anywhere in the projection. Language
	// builtins, locals, field names — node_refs records every identifier, not
	// only calls to definable symbols.
	RefNoTarget RefResolution = "no-target"
)

type RefResolver added in v0.22.0

type RefResolver interface {
	// RefRangeOf reports where a reference node sits, or nil when the
	// projection carries no spans.
	RefRangeOf(nodeID string) (*RefRange, error)
	// ResolveRef reports what a reference points at, classified.
	ResolveRef(nodeID string) (*RefTarget, error)
}

RefResolver answers the two questions a reference-walking consumer asks that Graph does not: where is this reference in the file, and what definition does it point at.

Both were previously reachable only by hand-writing SQL against node_refs, _ast and _imports — the join every consumer rewrote, and the bare-token matching that cannot tell a local Join from filepath.Join. See RefTarget.Resolution for why "did not resolve" is four distinct answers rather than a boolean.

type RefSearcher added in v0.22.0

type RefSearcher interface {
	SearchRefs(pattern, segment string, limit int) ([]RefHit, error)
}

RefSearcher answers `search role=reference`: references whose token matches a SQL LIKE pattern, optionally only those whose node sits under a path segment, at most limit of them. Engine sentinels (FileLevelSentinelPrefix) are never hits, as GetCallers does not return them either.

It is a capability rather than SQL in the handler because where references live depends on the backend: the mache_refs sidecar for MemoryStore and the legacy path, node_refs for a nodes-table .db, keyed by node_id on projection-v4 and by nid on projection-v6 (mache-90cb9f).

type RefTarget added in v0.22.0

type RefTarget struct {
	Resolution RefResolution
	// NodeID is the single definition, empty unless Resolution is RefResolved.
	NodeID string
	// Candidates lists the competing definitions when RefAmbiguous.
	Candidates []string
	// Via names the import path when RefExternal — the module the call leaves
	// through, which is what a dependency-aware consumer wants to record.
	Via string
}

RefTarget is where a reference points. NodeID is set ONLY for RefResolved: a caller cannot accidentally consume a guess, because an unresolved target has nothing to consume.

type RefsMapper added in v0.22.0

type RefsMapper interface {
	RefsMap() map[string][]string
}

RefsMapper exposes the reference index as node ID -> referenced node IDs, the edge set community detection and impact analysis run over.

type RefsQuerier added in v0.22.0

type RefsQuerier interface {
	QueryRefs(query string, args ...any) (*sql.Rows, error)
}

RefsQuerier runs SQL directly against the backing database — the escape hatch for questions the typed accessors above do not cover, including the `_ast` and `_lsp*` tables ley-line-open produces.

The tables available depend on who produced the .db; see docs/ARCHITECTURE.md for the capability matrix.

type SQLiteGraph added in v0.22.0

type SQLiteGraph struct {
	// contains filtered or unexported fields
}

SQLiteGraph implements Graph by querying the source SQLite database directly. No index copy, no ingestion step — the source DB's B+ tree IS the index.

Design: directory structure is derived lazily from schema + DB on first access, then cached in sync.Maps for lock-free concurrent reads from FUSE callbacks.

The scan is single-threaded and streaming: one sequential pass over all records, rendering name templates to build parent→child path relationships. This avoids the deadlock risk and channel overhead of a worker pool — SQLite sequential reads are I/O-bound and template rendering for name fields is cheap.

Memory model after scan:

  • dirChildren: sorted []string slices (one per directory), read-only post-scan
  • recordIDs: leaf dir path → DB row ID, for on-demand content resolution
  • contentCache: FIFO-bounded rendered content (avoids re-fetching hot files)

Content is never loaded during scan — only on FUSE read via resolveContent, which does a primary key lookup + template render + FIFO cache.

Cross-references (token → file bitmap) are stored in a sidecar database (<dbpath>.refs.db) to keep the source DB immutable. Refs are accumulated in-memory during ingestion and flushed once via FlushRefs.

func Open added in v0.22.0

func Open(dbPath string) (*SQLiteGraph, error)

Open opens a mache .db file for querying, with the same defaults mache's own CLI uses for a bare .db source (empty schema — auto-detected from the db's own tables — and mache's pure-Go template renderer). Returns a *SQLiteGraph, which implements Graph plus LookupDef/QueryRefs/GetCallers directly against the file — no import/populate step required.

This is the fix for the trap MemoryStore + ImportSQLite falls into: a MemoryStore built by ImportSQLite has an empty defs map and an uninitialized refs sidecar (ImportSQLite only replicates the node tree), so LookupDef silently returns nil and QueryRefs errors "refsDB not initialized" — not because the .db lacks the data, but because it was never copied into MemoryStore's separate in-memory indices. Open sidesteps the whole class by not copying anything.

OpenSQLiteGraph is the lower-level form, for callers supplying their own schema and template renderer.

func OpenSQLiteGraph added in v0.22.0

func OpenSQLiteGraph(dbPath string, schema *api.Topology, render TemplateRenderer) (*SQLiteGraph, error)

OpenSQLiteGraph opens a connection to the source DB and compiles the schema.

func (*SQLiteGraph) ASTKindChain added in v0.22.0

func (g *SQLiteGraph) ASTKindChain(id string) ([]string, bool, error)

ASTKindChain implements ASTKinder: one recursive walk up the nodes table, joined to _ast through lloschema's ASTSelect so the kinds read the same on either projection. Depth is capped at 256, far past any real nesting, so a cyclic parent chain in a malformed table cannot spin forever.

func (*SQLiteGraph) Act added in v0.22.0

func (g *SQLiteGraph) Act(id, action, payload string) (*ActionResult, error)

Act returns ErrActNotSupported — SQLiteGraph is a passive data graph.

func (*SQLiteGraph) AddDef added in v0.22.0

func (g *SQLiteGraph) AddDef(token, dirID string) error

AddDef records that a construct (dirID) defines the given token.

func (*SQLiteGraph) AddRef added in v0.22.0

func (g *SQLiteGraph) AddRef(token, nodeID string) error

AddRef accumulates a reference in-memory. No SQL is issued until FlushRefs. This eliminates the read-modify-write cycle per call — all bitmap mutations happen in RAM, and FlushRefs writes them in a single transaction. Not used for nodes-table path (refs already in main DB from mache build).

func (*SQLiteGraph) Close added in v0.22.0

func (g *SQLiteGraph) Close() error

Close closes both the source and sidecar database connections.

func (*SQLiteGraph) DB added in v0.22.0

func (g *SQLiteGraph) DB() *sql.DB

DB returns the main SQLite handle. Exposed so callers can query non-refs tables like `_ast` / `_source` / `_lsp*` that ley-line-built .db files carry. Mirrors NodesTableReader.DB() — same shape, same purpose. The returned handle is owned by SQLiteGraph; do not Close it, and queries should be read-only.

func (*SQLiteGraph) DBPath added in v0.22.0

func (g *SQLiteGraph) DBPath() string

DBPath returns the source .db file path. Implements the cmd-side dbPathProvider opt-in (see cmd/serve_find_smells.go) so canonical- view setup can locate the sibling .bindings.capnp event log next to this .db (mache-190508 step 3).

func (*SQLiteGraph) DefTokenOf added in v0.22.0

func (g *SQLiteGraph) DefTokenOf(id string) (string, bool, error)

DefTokenOf implements DefTokener.

func (*SQLiteGraph) DefsMap added in v0.22.0

func (g *SQLiteGraph) DefsMap() map[string][]string

DefsMap returns a snapshot of the token→dirIDs definition map.

On pre-built .db files (the `mache build` / `leyline parse` output) the in-memory g.defs map is empty — definitions live in the node_defs SQL table populated at build time. Falls through to a SQL scan so downstream consumers (get_impact, get_architecture, and the find_callees resolver fallback) see the full def set on both fresh in-process ingestion and pre-built .db sources.

In-memory entries take precedence on token collision (live ingestion is authoritative over the frozen-at-build snapshot), matching LookupDef's behavior and the SearchDefs pattern below.

inMemoryTokens guards multi-row accumulation: without it, the cp[token] existence check would skip every node_defs row past the first for any token (e.g. a method like Close defined across multiple types would only return one node_id). Bead mache-655e98.

func (*SQLiteGraph) EagerScan added in v0.22.0

func (g *SQLiteGraph) EagerScan() error

EagerScan pre-scans all root nodes so no FUSE callback ever blocks on a scan. Call this before mounting — fuse-t's NFS transport times out if a callback takes >2s.

func (*SQLiteGraph) FlushRefs added in v0.22.0

func (g *SQLiteGraph) FlushRefs() error

FlushRefs writes all accumulated refs to the sidecar database in a single transaction. Call once after ingestion is complete. Batches all inserts (len(fileIDMap) + len(pendingRefs)) into one transaction.

Guarded by sync.Once — safe to call multiple times; only the first call performs the flush. This prevents the double-call bug where a second flush would reset nextFileID to 0, causing ID collisions in file_ids.

func (*SQLiteGraph) GetCallees added in v0.22.0

func (g *SQLiteGraph) GetCallees(id string) ([]*Node, error)

GetCallees implements Graph. It resolves each call made by the construct to one or more definition node IDs using a fallback chain:

  1. Qualified lookup (e.g. "auth.Validate") via in-memory defs.
  2. SQL fallback to node_defs (pre-built .db only).
  3. Bare-token lookup via in-memory defs.
  4. Bare-token SQL lookup against node_defs.
  5. Suffix-match in node_defs ("*.token") for unambiguous receiver-method calls — see bead mache-9ca6af for the receiver-method root cause.
  6. Final fallback: name match in the nodes table.

Calls are extracted one of three ways:

  1. AST-scoped (preferred): when the construct's node Properties carry "ast_source_id"/"ast_scope_id" (persisted at projection time — see ingest.ASTScope) AND a scoped extractor is wired (requires a LIVE `_ast` table), GetCallees queries the pre-parsed `_ast` table directly, scoped to this construct. Fixes bead mache-fd9982: the legacy path below fed the GRAPH node id (e.g. "cmd/functions/evalOrAbs") as if it were the `_ast` source_id, which matched zero rows every time — find_callees silently returned nothing on every serve/mount .db.
  2. node_refs fallback: when #1 is unavailable — no `_ast` table (a `mache build --schema` .db never retains it: keeping `_ast` for a whole repo costs 228MB+162MB, prohibitive) or the construct lacks the Properties — but the construct has a "source" file child, resolve its calls from node_refs(token, node_id) instead. node_refs already holds every call token ScopeCalls found for this construct at projection time (engine_walk.go's `store.AddRef(token, sourceFileID)`), keyed by the construct's "source" file node id. This is bare-token only (no qualifier fidelity) — cross-package same-name over-match is possible, but strictly better than returning [] (bead mache-6fbaf1).
  3. Legacy content-based extractor: re-reads the construct's "source" file content and re-parses it. Kept for non-AST backends (or .dbs projected before this fix that lack the ast_source_id/ast_scope_id Properties AND have no node_refs coverage).

func (*SQLiteGraph) GetCallers added in v0.22.0

func (g *SQLiteGraph) GetCallers(token string) ([]*Node, error)

GetCallers returns the list of files (nodes) that reference the given token. For nodes-table path: queries main DB's node_refs (token, node_id) directly. For legacy path: reads roaring bitmaps from the sidecar refs database.

func (*SQLiteGraph) GetNode added in v0.22.0

func (g *SQLiteGraph) GetNode(id string) (*Node, error)

func (*SQLiteGraph) IndexStaleness added in v0.22.0

func (g *SQLiteGraph) IndexStaleness() (IndexStaleness, bool)

staleness derives the report from facts the artifact already carries: the .db file's own mtime IS the build time, and ley-line stamps _meta.source_root at parse. Nothing new is written anywhere — a pure read-side derivation, which also means it works retroactively on every existing .db.

Returns ok=false when the graph cannot answer (no dbPath, no _meta, source root gone) — absence of a staleness report must read as "unknown", never as "fresh".

func (*SQLiteGraph) Invalidate added in v0.22.0

func (g *SQLiteGraph) Invalidate(id string)

Invalidate evicts cached size and content for a node. Must be called after write-back modifies a file's content to prevent stale size/data from being served on the next Getattr or Read.

func (*SQLiteGraph) ListChildStats added in v0.22.0

func (g *SQLiteGraph) ListChildStats(id string) ([]NodeStat, error)

ListChildStats returns stat snapshots for all children of a directory. For the nodes-table fast path, it queries the nodes table for children with kind, size, and mtime — no content rendering. For the legacy scan path, it uses dirChildren + schema structure. ContentSize may be 0 for unvisited files (FUSE/NFS fall back to LOOKUP).

func (*SQLiteGraph) ListChildren added in v0.22.0

func (g *SQLiteGraph) ListChildren(id string) ([]string, error)

func (*SQLiteGraph) LookupDef added in v0.22.0

func (g *SQLiteGraph) LookupDef(token string) []string

LookupDef returns the dir IDs that define `token` without snapshotting the entire defs map. Returns nil for unknown tokens. Mirrors MemoryStore.LookupDef; see that method's docstring.

For pre-built DBs (useNodesTable path) the in-memory g.defs map is usually empty because data lives in the `node_defs` SQL table. Falls back to the SQL query on in-memory miss so find_definition works against ley-line-parsed databases. Matches the SQL pattern used by GetCallees for parity.

func (*SQLiteGraph) LookupDefNodes added in v0.22.0

func (g *SQLiteGraph) LookupDefNodes(token string) ([]*Node, error)

LookupDefNodes returns the definition NODES for token, closing the return-type asymmetry that made every consumer write the same conversion loop:

LookupDef(token)  ->  []string          // ids
GetCallers(token) ->  ([]*Node, error)  // nodes
GetCallees(id)    ->  ([]*Node, error)  // nodes

Ids from one accessor and Nodes from the next is why modmap wrapped this surface in its own callerResolver. LookupDef stays as-is — ids ARE the right answer when the caller only needs identity, and fetching nodes it will throw away is waste. This is the variant for when it actually wants the node.

A definition whose node cannot be loaded is SKIPPED rather than failing the call: node_defs can outlive a node during an incremental reparse, and one stale row should not deny a caller the definitions that did resolve.

func (*SQLiteGraph) QueryRefs added in v0.22.0

func (g *SQLiteGraph) QueryRefs(query string, args ...any) (*sql.Rows, error)

QueryRefs executes a SQL query against the refs database. For nodes-table path: queries the main DB (node_refs has (token, node_id)). For legacy path: queries the sidecar (includes mache_refs virtual table).

func (*SQLiteGraph) ReadContent added in v0.22.0

func (g *SQLiteGraph) ReadContent(id string, buf []byte, offset int64) (int, error)

func (*SQLiteGraph) RefRangeOf added in v0.22.0

func (g *SQLiteGraph) RefRangeOf(nodeID string) (*RefRange, error)

RefRangeOf returns where the reference node sits in its file.

Every consumer previously hand-wrote this join (node_refs.node_id -> _ast), which is why it is here: the join is not the interesting part of anyone's program. Returns nil when the node has no `_ast` row — a standalone mache projection carries no spans, and nil is the honest answer rather than a zero-valued range that reads as "line 0".

func (*SQLiteGraph) RefsMap added in v0.22.0

func (g *SQLiteGraph) RefsMap() map[string][]string

RefsMap returns a token→nodeIDs map for community detection. For the nodes-table path, queries node_refs (token, node_id). For the legacy bitmap path, decodes bitmaps and resolves file IDs.

Engine `_file_level:` sentinels are stripped here, for every path, because this is a CONSUMER-facing aggregation: its output feeds community detection, architecture layering and context packing, none of which can do anything with a bookkeeping id that names no construct. Leaving them in is not a cosmetic problem — measured on a schema-projected build of cmd/, 1,939 of 15,073 refs were sentinels, and they cluster together because they co-occur by construction, so they crowd out the real communities (mache-8f6abf).

Filtered at this boundary rather than in each builder below, so the two backends cannot disagree about it — leylinegraph already filtered in SQL while these two did not, which is how the same API came to return different data depending on which producer wrote the .db.

The rows stay in node_refs. dead_code reads them directly and needs them.

func (*SQLiteGraph) ResolveRef added in v0.22.0

func (g *SQLiteGraph) ResolveRef(nodeID string) (*RefTarget, error)

ResolveRef answers "what definition does this reference point at?" for the node id of a reference site.

It replaces bare-token matching against node_refs.token, which cannot distinguish a local `Join` from `filepath.Join` and will happily return the wrong one. The classification is derived, never guessed:

no local def + a known import  -> RefExternal (Via names the import)
no local def, no import        -> RefNoTarget
exactly one local def          -> RefResolved
several, narrowed to one       -> RefResolved
several, still several         -> RefAmbiguous (candidates returned)

Note what is NOT here: no attempt to decide whether an import path "looks external" by inspecting its shape. Whether github.com/you/yours is inside your module is something the caller knows and mache does not, so mache reports the import path and lets the caller judge.

func (*SQLiteGraph) SearchDefs added in v0.22.0

func (g *SQLiteGraph) SearchDefs(pattern string, limit int) map[string][]string

SearchDefs returns up to `limit` token→nodeIDs entries whose token matches the SQL LIKE pattern. Used by `search role=definition` to avoid materializing the full defs map.

For pre-built .db files the in-memory g.defs map is empty; this pushes the filter down to SQL against node_defs. Mirrors LookupDef's fallback structure: in-memory first (with sqlLikeMatch-style check), then SQL. Bead mache-9cba08.

func (*SQLiteGraph) SearchRefs added in v0.22.0

func (g *SQLiteGraph) SearchRefs(pattern, segment string, limit int) ([]RefHit, error)

SearchRefs implements RefSearcher.

func (*SQLiteGraph) SetCallExtractor added in v0.22.0

func (g *SQLiteGraph) SetCallExtractor(fn CallExtractor)

SetCallExtractor configures the parser for on-demand callee resolution.

func (*SQLiteGraph) SetScopedCallExtractor added in v0.22.0

func (g *SQLiteGraph) SetScopedCallExtractor(fn ScopedCallExtractor)

SetScopedCallExtractor configures the AST-scoped extractor GetCallees prefers when a construct carries ast_source_id/ast_scope_id Properties.

type SQLiteResolver added in v0.6.7

type SQLiteResolver struct {
	// contains filtered or unexported fields
}

SQLiteResolver resolves ContentRef entries by fetching records from SQLite and re-rendering their content templates.

func NewSQLiteResolver added in v0.6.7

func NewSQLiteResolver(render TemplateRenderer) *SQLiteResolver

NewSQLiteResolver creates a resolver that uses the given template renderer to render content from SQLite records.

func (*SQLiteResolver) Close added in v0.22.0

func (r *SQLiteResolver) Close()

Close closes all open database connections and clears the pool. Subsequent Resolve calls will re-open databases as needed.

func (*SQLiteResolver) Resolve added in v0.22.0

func (r *SQLiteResolver) Resolve(ref *ContentRef) ([]byte, error)

Resolve fetches a record from SQLite and renders its content template.

type ScopedCallExtractor added in v0.22.0

type ScopedCallExtractor func(sourceID, scopeID, langName string) ([]QualifiedCall, error)

ScopedCallExtractor returns qualified call tokens made BY a single construct, scoped to it. Unlike CallExtractor, it is keyed on the construct's REAL `_ast`/`_source` id and `_ast` scope node id — not a graph node id and not source content — so it works directly against a pre-parsed `_ast` table without re-deriving anything from the graph.

GetCallees prefers this over the legacy content-based CallExtractor when the construct's node carries "ast_source_id"/"ast_scope_id" Properties (persisted at projection time; see ingest.ASTScope). Feeding the graph node id as a source_id into the old content-based path matched zero `_ast` rows every time — the root cause of find_callees silently returning nothing on the serve/mount path (bead mache-fd9982).

type SheafBackend added in v0.22.0

type SheafBackend interface {
	Invalidate(regionID int) ([]int, error)
}

SheafBackend is the subset of SheafClient that SheafInvalidator needs. Defined as an interface to allow testing without a real daemon.

type SheafInvalidator added in v0.22.0

type SheafInvalidator struct {
	// contains filtered or unexported fields
}

SheafInvalidator wraps a Graph with sheaf-aware cascading invalidation. When a node is invalidated, it looks up the node's community (region), asks the ley-line daemon which regions are transitively affected, then invalidates all nodes in those affected regions.

If the SheafClient is nil or the daemon is unavailable, it falls back to plain Graph.Invalidate on the single node.

func NewSheafInvalidator added in v0.22.0

func NewSheafInvalidator(graph Graph, sheaf SheafBackend, result *CommunityResult) *SheafInvalidator

NewSheafInvalidator creates a SheafInvalidator. All parameters are optional:

  • graph may be nil (all operations become no-ops)
  • sheaf may be nil (falls back to single-node invalidation)
  • result may be nil (falls back to single-node invalidation)

func (*SheafInvalidator) CommunityResultForRouting added in v0.22.0

func (si *SheafInvalidator) CommunityResultForRouting() *CommunityResult

CommunityResultForRouting returns the current CommunityResult under a read lock. The pointer is returned without cloning — callers MUST treat it as read-only (the membership map is replaced wholesale by future SetCommunityResult / SetState writers, which install a NEW pointer rather than mutating in place, so a stable snapshot is safe to iterate without further locking).

Exists for the sheaf event subscriber's router (cmd/sheaf_subscribe.go), which needs to walk membership to convert region IDs into node IDs for invalidation. Named "ForRouting" rather than the obvious CommunityResult() to signal the read-only contract.

func (*SheafInvalidator) HasResult added in v0.22.0

func (si *SheafInvalidator) HasResult() bool

HasResult reports whether the invalidator has a CommunityResult to look up regions in. When false, InvalidateWithCascade degrades to single-node invalidate (still correct, just no cascade). Callers (e.g. the watcher) use this to decide whether to skip the lookup entirely vs. fall through.

func (*SheafInvalidator) InvalidateAllScoped added in v0.22.0

func (si *SheafInvalidator) InvalidateAllScoped() int

InvalidateAllScoped evicts every node the invalidator's CommunityResult claims membership for, WITHOUT calling the sheaf backend to compute a cascade. Returns the number of nodes evicted.

This is the coarse-v1 "invalidate everything" path used by the watcher-driven `daemon.sheaf.invalidate` topic (LLO PR #140, LLO v0.6+). The daemon has already declared its entire working set stale by the time it emits `scope: "all-known"`, so re-cascading per-region against the daemon would be redundant chatter — we know the answer is "all of them". A future fine-grained (`scope: "changed-only"`) emit path exists (LLO bead `e40566`); it stays on the per-region cascade path via InvalidateNodesWithCascade.

No-op when the invalidator, graph, or CommunityResult is nil (same null-safety contract as InvalidateNodesWithCascade). Skipping the backend also means this method does not touch si.sheaf, so it's safe to call even when no SheafBackend is installed — the membership snapshot is enough.

func (*SheafInvalidator) InvalidateNodesWithCascade added in v0.22.0

func (si *SheafInvalidator) InvalidateNodesWithCascade(ids []string) int

InvalidateNodesWithCascade is the batched-by-region variant of InvalidateWithCascade. The watcher passes every node touched by a file edit; this method dedupes them to the underlying set of unique region IDs and calls the daemon exactly once per unique region.

Why this exists (PR #383 Copilot #7): the naive `for _, id := range ids { InvalidateWithCascade(id) }` loop hits the daemon once per node. On a large file with 50 functions all in the same community, that's 50 redundant region invalidations — each bumps the generation counter and re-cascades the same regions on the daemon side. This method collapses that to one call per unique region.

IDs not present in membership fall back to single-node Graph.Invalidate (same fallback as InvalidateWithCascade for unknown nodes). When sheaf or membership is nil, the entire input falls back to per-id Graph.Invalidate.

Returns total nodes invalidated across all paths (cascades + fallbacks).

func (*SheafInvalidator) InvalidateWithCascade added in v0.22.0

func (si *SheafInvalidator) InvalidateWithCascade(id string, membership map[string]int) int

InvalidateWithCascade invalidates a node and, if sheaf is available, cascades the invalidation to all nodes in transitively affected regions.

The membership map is used to look up which community the node belongs to. If membership is nil, si.result.Membership is used.

Returns the number of nodes invalidated.

func (*SheafInvalidator) SetCommunityResult added in v0.22.0

func (si *SheafInvalidator) SetCommunityResult(cr *CommunityResult)

SetCommunityResult updates the community detection result used for lookups. Call this after re-running community detection.

func (*SheafInvalidator) SetSheaf added in v0.22.0

func (si *SheafInvalidator) SetSheaf(b SheafBackend) (prior SheafBackend)

SetSheaf swaps the sheaf backend. Pass nil to disable cascades (the invalidator falls back to single-node Graph.Invalidate). Used by the serve startup path to attach a SheafClient lazily — the file watcher can install the invalidator before the ley-line daemon is reachable, then swap in a real backend once a connection is established.

Returns the prior backend so callers that own SheafBackend resources (e.g. a SheafClient wrapping a UDS socket) can close the prior one without leaking. Returns nil if no prior backend was installed.

func (*SheafInvalidator) SetState added in v0.22.0

func (si *SheafInvalidator) SetState(result *CommunityResult, sheaf SheafBackend) (prior SheafBackend)

SetState atomically swaps both the CommunityResult and SheafBackend under a single Lock. Use this when both pieces are being replaced together (e.g. from get_communities after PushTopology succeeds). The standalone SetSheaf / SetCommunityResult paths open a window where the watcher can observe new membership paired with old sheaf (or vice versa) — see TestSheafInvalidator_SetState_AtomicSwap and PR #383 Copilot #6 for the cascade-mismatch failure mode.

Returns the prior backend so the caller can close it without leaking. Returns nil if no prior backend was installed.

type SourceOrigin

type SourceOrigin struct {
	FilePath  string `json:"file"`
	StartByte uint32 `json:"start_byte"`
	EndByte   uint32 `json:"end_byte"`

	// Line/column of the same range, 1-based, or 0 when unknown.
	//
	// Byte offsets are the right primitive for write-back — a splice needs
	// bytes — but they are the wrong unit for a READER. No editor, terminal,
	// or LLM context references source by byte; everything downstream speaks
	// "path:line". Carrying both means a consumer never has to re-read the
	// file and count newlines to act on a result (mache-e57065).
	//
	// 1-based because that is what every consumer of a line number expects.
	// ley-line-open's `_ast` stores tree-sitter's 0-based rows, so the
	// producer side adds one; 0 here therefore means "not known", never
	// "first line". Columns follow the same convention.
	StartLine uint32 `json:"start_line,omitempty"`
	StartCol  uint32 `json:"start_col,omitempty"`
	EndLine   uint32 `json:"end_line,omitempty"`
	EndCol    uint32 `json:"end_col,omitempty"`
}

SourceOrigin tracks the byte range of a construct in its source file. Used by write-back to splice edits into the original source.

type StalenessReporter added in v0.22.0

type StalenessReporter interface {
	IndexStaleness() (IndexStaleness, bool)
}

StalenessReporter is the capability get_overview probes for. On graphs that cannot answer (MemoryStore, composites) the overview simply omits the block.

type TemplateRenderer added in v0.6.7

type TemplateRenderer func(tmpl string, values map[string]any) (string, error)

TemplateRenderer renders a Go text/template string with the given values map.

type WritableGraph added in v0.22.0

type WritableGraph struct {
	// contains filtered or unexported fields
}

WritableGraph is a read-write SQLite backend for arena-mode mounts. Read methods delegate to NodesTableReader (shared with SQLiteGraph). Write methods mutate the nodes table and flush to the arena.

func OpenWritableGraph added in v0.22.0

func OpenWritableGraph(masterDBPath string, schema *api.Topology, render TemplateRenderer, flusher *ArenaFlusher) (*WritableGraph, error)

OpenWritableGraph opens a writable connection to the master .db. The DB must have a nodes table (created by mache build).

func (*WritableGraph) Act added in v0.22.0

func (g *WritableGraph) Act(id, action, payload string) (*ActionResult, error)

func (*WritableGraph) Close added in v0.22.0

func (g *WritableGraph) Close() error

Close closes the database connection.

func (*WritableGraph) DBPath added in v0.22.0

func (g *WritableGraph) DBPath() string

DBPath returns the path to the writable master database.

func (*WritableGraph) Flush added in v0.22.0

func (g *WritableGraph) Flush()

Flush requests a coalesced arena flush. Non-blocking.

func (*WritableGraph) FlushNow added in v0.22.0

func (g *WritableGraph) FlushNow() error

FlushNow performs a synchronous arena flush.

func (*WritableGraph) GetCallees added in v0.22.0

func (g *WritableGraph) GetCallees(id string) ([]*Node, error)

func (*WritableGraph) GetCallers added in v0.22.0

func (g *WritableGraph) GetCallers(token string) ([]*Node, error)

func (*WritableGraph) GetNode added in v0.22.0

func (g *WritableGraph) GetNode(id string) (*Node, error)

func (*WritableGraph) Invalidate added in v0.22.0

func (g *WritableGraph) Invalidate(id string)

func (*WritableGraph) ListChildStats added in v0.22.0

func (g *WritableGraph) ListChildStats(id string) ([]NodeStat, error)

func (*WritableGraph) ListChildren added in v0.22.0

func (g *WritableGraph) ListChildren(id string) ([]string, error)

func (*WritableGraph) ReadContent added in v0.22.0

func (g *WritableGraph) ReadContent(id string, buf []byte, offset int64) (int, error)

func (*WritableGraph) UpdateRecord added in v0.22.0

func (g *WritableGraph) UpdateRecord(id string, content []byte) error

UpdateRecord updates a file node's content in the nodes table.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL