Documentation
¶
Index ¶
- Constants
- Variables
- func FilepathWalk(repo string, followSymlinks bool, fn func(rel string, src []byte)) error
- func IsEncryptedSnapshot(path string) (bool, error)
- func IsEncryptedSnapshotBytes(b []byte) bool
- func IsMinified(rel string, src []byte) bool
- func NeedsCodeParser(rel string) bool
- func ParseOneFile(p *parser.Parser, rel string, src []byte) parser.Result
- func ParseRepo(repo string, followSymlinks bool) (nodes []parser.NodeInfo, edges []parser.EdgeInfo, files int, ...)
- func SplitRepoSpec(spec string) (label, path string)
- func WriteSnapshot(path string, s *Snapshot) error
- func WriteSnapshotFor(path string, s *Snapshot, keys SnapshotKeys, loadedEncrypted bool) error
- func WriteSnapshotWith(path string, s *Snapshot, keys SnapshotKeys) error
- type EmbedFingerprint
- type Forgotten
- type Indexer
- func NewIndexer(ctx context.Context, repoSpecs []string, embedder index.Embedder, ...) (*Indexer, error)
- func NewIndexerFromResults(ctx context.Context, label, repoPath string, files map[string]parser.Result, ...) (*Indexer, error)
- func NewIndexerFromSnapshot(ctx context.Context, snap *Snapshot, embedder index.Embedder, ...) (*Indexer, error)
- func NewIndexerSeeded(ctx context.Context, repoSpecs []string, embedder index.Embedder, ...) (*Indexer, error)
- func (ixr *Indexer) Graph() *graph.Graph
- func (ixr *Indexer) Index() *index.Index
- func (ixr *Indexer) Labels() []string
- func (ixr *Indexer) Meta() map[string]map[string]string
- func (ixr *Indexer) Put(ctx context.Context, rel string, res parser.Result) error
- func (ixr *Indexer) Reindex(ctx context.Context) error
- func (ixr *Indexer) Remove(ctx context.Context, rels []string) (int, error)
- func (ixr *Indexer) SetExplicitSummary(s string)
- func (ixr *Indexer) SetMeta(meta map[string]map[string]string)
- func (ixr *Indexer) SnapshotFor(ctx context.Context, label string, fp EmbedFingerprint, ...) (*Snapshot, error)
- func (ixr *Indexer) Status() map[string]any
- func (ixr *Indexer) Update(ctx context.Context, paths []string) (changed, removed, skipped int, unowned []string, err error)
- type MapCounters
- type Note
- type ProjectInfo
- type Registry
- func (r *Registry) AddSnapshot(path string) error
- func (r *Registry) AddSnapshotBytes(ctx context.Context, b []byte) error
- func (r *Registry) FlushAll(ctx context.Context)
- func (r *Registry) Forget(ctx context.Context, label, qn string) (Forgotten, error)
- func (r *Registry) Labels() []string
- func (r *Registry) Metrics() map[string]any
- func (r *Registry) ProjectList() []ProjectInfo
- func (r *Registry) RecordSearch(label string, hits int)
- func (r *Registry) Remember(ctx context.Context, label string, n Note) (Remembered, error)
- func (r *Registry) RemoveSnapshotByLabel(label string)
- func (r *Registry) RemoveSnapshotByPath(path string)
- func (r *Registry) Resolve(ctx context.Context, mapName, query string) ([]mcpserver.Resolved, error)
- func (r *Registry) ResolveByQN(ctx context.Context, qn string) (mcpserver.Resolved, error)
- func (r *Registry) ResolveMap(ctx context.Context, label string) (mcpserver.Resolved, error)
- func (r *Registry) SetMemoryTTL(d time.Duration)
- func (r *Registry) SetSnapshotDir(dir string)
- func (r *Registry) SetSnapshotKeys(k SnapshotKeys)
- func (r *Registry) StartMemorySweeper(ctx context.Context, every time.Duration) (wait func())
- func (r *Registry) StartSweeper(ctx context.Context)
- func (r *Registry) Status() map[string]any
- func (r *Registry) SweepMemory(ctx context.Context, label string, now time.Time) (int, error)
- func (r *Registry) Totals() (nodes, edges int)
- func (r *Registry) UpdateByPaths(ctx context.Context, paths []string) (changed, removed, skipped int, unowned []string, err error)
- func (r *Registry) WatchDir(ctx context.Context, dir string) error
- type RegistryStats
- type RegistryStatsSnapshot
- type Remembered
- type RoutingSnapshot
- type Snapshot
- type SnapshotBody
- type SnapshotHeader
- type SnapshotKeys
- func (k SnapshotKeys) CanDecrypt() bool
- func (k SnapshotKeys) Encrypts() bool
- func (k SnapshotKeys) IdentityRecipient() string
- func (k SnapshotKeys) KeyPath() string
- func (k SnapshotKeys) Recipient() string
- func (k SnapshotKeys) RecipientMismatch() bool
- func (k SnapshotKeys) WriteRecipient(loadedEncrypted bool) age.Recipient
Constants ¶
const SnapshotVersion = snapshotVersion
SnapshotVersion is snapshotVersion for callers outside the package that construct a Snapshot by hand (the vectors subcommand's test fixture): a hand-built header with a stale Version is rejected at read, and a test that hardcodes the number breaks silently on the next bump instead of tracking it.
Variables ¶
var ErrSnapshotEncrypted = errors.New("snapshot is encrypted")
ErrSnapshotEncrypted is returned (wrapped) when a snapshot is encrypted and no identity was configured, or none of the configured identities matches. Callers that register many maps errors.Is on it to skip-and-warn rather than fail the whole server.
Functions ¶
func FilepathWalk ¶
FilepathWalk walks a repo, skipping ignored dirs and minified bundles, and calls fn with each readable file's repo-relative path and bytes. Shared by full and incremental indexing so both honour the same skip rules.
followSymlinks controls whether a symlinked directory is descended into. filepath.WalkDir never does this on its own — a symlink's DirEntry reports IsDir() false even when its target is a directory, so a symlinked dir is silently invisible to the walk by default. Default false here too: turning it on globally would make every future build's shape depend on wherever a repo happens to symlink, and a link into a large tree (or into its own parent) could quietly balloon or hang a build that never asked for it. The snapshot builder's opt-in -follow-symlinks flag is the only caller that sets it true. A symlink to a plain file is read either way — that already worked before this parameter existed, since a plain read follows a file symlink transparently.
func IsEncryptedSnapshot ¶
IsEncryptedSnapshot sniffs the file at path. It reads a handful of bytes and is safe to call for every file at registration time.
func IsEncryptedSnapshotBytes ¶
IsEncryptedSnapshotBytes is IsEncryptedSnapshot for an in-memory blob.
func IsMinified ¶
IsMinified reports whether a file is a minified or generated bundle that should not be indexed.
skipDir already excludes vendor/, node_modules/ and dist/, but a bundle committed into an ordinary directory slips straight through, and the cost is severe: one vendored d3.v7.min.js parses into ~2000 single-letter "functions" — 77% of every node in this repo's own graph before this check existed. Those nodes swamp real symbols in search ranking, and each one is embedded and carried in every snapshot.
Two signals, because neither alone is enough. The name check catches the conventional cases cheaply. The content check catches a bundle named anything at all (webpack's main.<hash>.js), and is what makes this robust rather than a list of spellings; it is confined to files that need the code parser, so an unwrapped Markdown doc with very long lines is never dropped.
func NeedsCodeParser ¶
NeedsCodeParser reports whether extracting this file requires the Tree-sitter parser — i.e. whether it is not Markdown, YAML, SQL, an .env file, an application config file or a Dockerfile. In a CGO-free build such a file cannot be re-indexed, and callers must SKIP it rather than parse it: ParseOneFile would return zero nodes there, which the incremental update path is entitled to read as "the file was deleted" and would use to drop the file's existing nodes from a worked copy. Silent data loss, from a build flag.
func ParseOneFile ¶
ParseOneFile dispatches a single file's bytes to the right extractor: Markdown → Doc/Section nodes, K8s YAML → Manifest nodes, SQL → schema-object nodes (Table, View, Routine, Trigger, Sequence), else Tree-sitter code. rel is the repo-relative path. Shared by full and incremental indexing.
In a CGO-free build the default branch reaches the parser stub and yields no nodes; callers that cannot tolerate that must gate on NeedsCodeParser first.
func ParseRepo ¶
func ParseRepo(repo string, followSymlinks bool) (nodes []parser.NodeInfo, edges []parser.EdgeInfo, files int, langs map[string]int, err error)
ParseRepo walks a repo directory and parses every supported file into a flat node/edge list. Shared by the CLI dump path and the live indexer.
func SplitRepoSpec ¶
SplitRepoSpec parses "label=path" or "path" (label defaults to the dir name).
func WriteSnapshot ¶
WriteSnapshot gob-encodes header then body into a single gzip stream at path. Writing them as two consecutive values lets ReadSnapshotHeader decode just the header and stop. Plaintext; WriteSnapshotWith adds encryption.
func WriteSnapshotFor ¶
func WriteSnapshotFor(path string, s *Snapshot, keys SnapshotKeys, loadedEncrypted bool) error
WriteSnapshotFor is WriteSnapshotWith for a file that may be a rewrite of an existing snapshot: loadedEncrypted says whether that file was encrypted when it was read, and SnapshotKeys.WriteRecipient turns the two into the actual recipient (nil = plaintext). Registry.flush and the live server's flush use this so a map that arrived encrypted stays encrypted under a key alone.
func WriteSnapshotWith ¶
func WriteSnapshotWith(path string, s *Snapshot, keys SnapshotKeys) error
WriteSnapshotWith is WriteSnapshot with key material. When keys.Encrypts() the gzip+gob stream is wrapped in age to keys' recipient (an explicit -snapshot-recipient, else the loaded identity's own public key), so the file starts with the age header instead of the gzip magic; readers sniff for either. With no keys it writes exactly what WriteSnapshot always wrote.
Types ¶
type EmbedFingerprint ¶
EmbedFingerprint identifies the embedding configuration a snapshot was built with. A snapshot is only valid for a server using the SAME fingerprint — a mismatch means the vectors are in a different space and must not be trusted.
type Indexer ¶
type Indexer struct {
// contains filtered or unexported fields
}
Indexer owns the live graph + index and the per-repo parsed state needed to update incrementally. The graph/index are swapped atomically on each rebuild so MCP readers always see a consistent snapshot.
Two construction paths exist: NewIndexer (live parse, CGO — see indexer_cgo.go) and NewIndexerFromSnapshot (parse-free load from a pre-built snapshot, pure Go). The CGO-free static build uses only the snapshot path.
func NewIndexer ¶
func NewIndexer(ctx context.Context, repoSpecs []string, embedder index.Embedder, logger zerolog.Logger, followSymlinks bool) (*Indexer, error)
NewIndexer parses each repo fully and builds the initial graph + embeddings. followSymlinks is passed straight through to the walk (see FilepathWalk) and remembered on the Indexer so a later Reindex uses the same setting.
func NewIndexerFromResults ¶
func NewIndexerFromResults(ctx context.Context, label, repoPath string, files map[string]parser.Result, embedder index.Embedder, logger zerolog.Logger) (*Indexer, error)
NewIndexerFromResults builds an indexer from pre-constructed per-"file" parser.Results (nodes + edges) rather than by parsing a repo on disk, then runs the embed pass so every node gets a vector and the HNSW is built. This is the entry point for NON-code mental maps — e.g. a service dependency graph (services as nodes, calls as edges) built from a JSON file — which reuse the exact same graph/index/snapshot pipeline as code maps without any parsing (so it compiles and runs CGO-free). label is the map name; repoPath is a descriptive origin recorded in the snapshot header (e.g. the source JSON path).
func NewIndexerFromSnapshot ¶
func NewIndexerFromSnapshot(ctx context.Context, snap *Snapshot, embedder index.Embedder, logger zerolog.Logger) (*Indexer, error)
NewIndexerFromSnapshot reconstructs an indexer from a pre-built snapshot WITHOUT calling the embedder for any node: the snapshot's text→vec cache is loaded first, so Build is all cache-hits and only (re)builds the HNSW index. The embedder is retained solely for embedding queries at search time.
A snapshot built with `snapshot -no-embed` (snap.Header.NoEmbed) carries an empty cache on purpose: this marks the loaded Index no-embed too (index.Index.SetNoEmbed), so Build leaves every node unembedded instead of treating the empty cache as ordinary cache misses and embedding the whole repo on load — the read-side half of never re-embedding a no-embed map. Search over the result degrades to its lexical channel; other tools (graph walks, node lookup) are unaffected.
This is the load path of the read-only `serve-snapshots` server (Mode B); the resulting indexer is treated as read-only (Update/Reindex are never called). It performs NO parsing, so it compiles and runs without CGO.
func NewIndexerSeeded ¶
func NewIndexerSeeded(ctx context.Context, repoSpecs []string, embedder index.Embedder, logger zerolog.Logger, followSymlinks bool, seed map[string][]float32, noEmbed bool) (*Indexer, error)
NewIndexerSeeded is NewIndexer with the embedding cache pre-loaded from seed (a previous snapshot's Body.Cache, built under the same fingerprint), so only nodes whose text is new are embedded. The repo is still parsed in full: the seed saves the embed pass, never the facts, so a rebuild picks up every extractor change. Vectors are keyed by node text, not identity, which is what makes a vector from an older build valid for an unchanged node in a new one. Unused seed entries are pruned before anything can be written.
noEmbed is the `snapshot -no-embed` case: the repo is still parsed and linked in full, but the embedder is never called for any node, seeded or not (callers pass a nil/empty seed alongside it — see runSnapshot). Kept as its own parameter rather than inferred from seed being empty, since an ordinary cold build (no previous snapshot to seed from) also has an empty seed and must still embed normally.
Why: `snapshot` rebuilt a 35k-node repository in 18 minutes, nearly all of it re-embedding text the snapshot it replaced already held.
func (*Indexer) Labels ¶
Labels returns the served repo labels. Used by serve mode to snapshot each repo's graph to disk (periodic + on shutdown).
func (*Indexer) Meta ¶
Meta returns the opaque per-node attribute bag carried by the snapshot (QN → string k/v), or nil if none. recall never interprets it — a consumer (e.g. triage) reads its own keys. The returned map must not be mutated.
func (*Indexer) Put ¶
Put replaces (or, when res carries no nodes, removes) the per-file Result stored under rel in the indexer's single repo, rebuilds the graph and embeds only the nodes that lost their vector on the swap. rel is a repo-relative path in name only: no file is read, and none needs to exist.
func (*Indexer) Remove ¶
Remove drops every per-file Result stored under rels (paths that are not held are ignored) and rebuilds once, so sweeping a batch of expired notes costs one graph build rather than one per note. Nothing is embedded: a removal only loses vectors. Returns how many of rels were actually held.
func (*Indexer) SetExplicitSummary ¶
SetExplicitSummary attaches an operator-written router description that SnapshotFor will use verbatim (see summaryFor) instead of deriving one from the README or graph stats. A builder calls this before snapshotting, e.g. from the snapshot subcommand's -summary flag. Empty clears any override.
func (*Indexer) SetMeta ¶
SetMeta attaches the opaque per-node attribute bag (QN → string k/v) that SnapshotFor will write verbatim into Body.Meta. A builder calls this before snapshotting; recall assigns the map no meaning. Nil clears it.
func (*Indexer) SnapshotFor ¶
func (ixr *Indexer) SnapshotFor(ctx context.Context, label string, fp EmbedFingerprint, embedder index.Embedder) (*Snapshot, error)
SnapshotFor captures the current single-repo graph as a portable Snapshot: the parsed per-file Results, the embedding cache (verbatim), and a synthesized router summary embedded via the embedder. The indexer must hold exactly one repo (the snapshot subcommand builds one project at a time).
func (*Indexer) Update ¶
func (ixr *Indexer) Update(ctx context.Context, paths []string) (changed, removed, skipped int, unowned []string, err error)
Update re-parses the given paths (absolute or repo-relative), replaces their per-file results, rebuilds the graph (which re-resolves dependents), and re-embeds only the changed/new nodes.
skipped counts files that are deliberately not extracted: code files in a CGO-free binary, and minified bundles in any build. They are left exactly as they were in the graph. unowned lists paths that resolved to no served repo. Callers should surface both: a caller told only "changed: 0" cannot distinguish "your edit produced no nodes" from "this build cannot read your file" or "I never found the file you named".
type MapCounters ¶
MapCounters is one map's search/hit totals, JSON-friendly.
type Note ¶
Note and Remembered are the mcpserver types under their engine names: the seam is declared over there because engine imports mcpserver (Registry implements Resolver), and an alias keeps one definition instead of a copy that could drift.
type ProjectInfo ¶
ProjectInfo is one project's label + residency, for the UI selector.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds every registered project and routes resolves to the right one. It implements mcpserver.Resolver.
func NewRegistry ¶
func NewRegistry(embedder index.Embedder, fp EmbedFingerprint, fanout int, ttl time.Duration, writable bool, logger zerolog.Logger) *Registry
NewRegistry builds an empty registry. fanout is how many projects a semantic (no explicit project) route consults; ttl is the idle-eviction window (0 = never evict).
func (*Registry) AddSnapshot ¶
AddSnapshot registers a project from its snapshot header WITHOUT loading the body: the entry starts evicted (ixr=nil) carrying only its routing vector; the graph is read on first Resolve. A fingerprint mismatch is rejected here.
func (*Registry) AddSnapshotBytes ¶
AddSnapshotBytes registers a snapshot carried IN MEMORY (a gzipped-gob blob, e.g. stored in a MemoryCell) rather than on disk. Like AddSnapshot it reads only the header up front and defers the body to first Resolve; the bytes are retained on the entry so an evicted graph reloads without a file. A fingerprint mismatch is rejected. Registering the same label again replaces the entry (drop the old in-memory graph first via RemoveSnapshotByLabel if it may be loaded).
The routing vector (SummaryVec) is what semantic search matches against; an entry without one is INVISIBLE to a query-only search (rankBySummary skips it). Externally-built gobs may ship an empty SummaryVec, so when it's missing we synthesize it here by embedding the header Summary (or the label as a last resort) — otherwise a cell-carried map could only be reached by an exact map= arg, never by a bare query. A synthesis failure is non-fatal: the map still registers and remains reachable by exact label.
func (*Registry) FlushAll ¶
FlushAll flushes every dirty project — call on graceful shutdown so edits made since the last eviction are not lost.
func (*Registry) Forget ¶
Forget removes the note qn from the map label and flushes at once. It is remember's undo and nothing wider: the qn must name a Memory node, in a map holding only notes, and label must be that map — so a caller cannot reach a code or catalog node through it, and a qn from another map is refused by name rather than silently not found.
func (*Registry) Labels ¶
Labels returns the registered project labels (sorted), for status/logging.
func (*Registry) Metrics ¶
Metrics composes every metrics surface this registry owns into one JSON-friendly map: query-mechanics search stats (shared across every map, process-lifetime cumulative — see index.SearchStats), routing/map-lifecycle stats (this registry's own), and — when the configured embedder exposes it — its query-embedding call/cache-hit counters (the one place that number is actually tracked; see index.EmbedStatter). Feeds both the /metrics HTTP endpoint and the get_graph_stats MCP tool so the two surfaces can never drift apart.
func (*Registry) ProjectList ¶
func (r *Registry) ProjectList() []ProjectInfo
ProjectList returns every registered project with its current residency, sorted by label — feeds the /projects endpoint and the UI's project selector.
func (*Registry) RecordSearch ¶
RecordSearch attributes one map's search + returned-hit count to RegistryStats. Implements mcpserver.Resolver — the mcpserver package's search loop calls this once per resolved map, right after that map's index.Index.Search returns, since Registry itself never calls Search and so never sees a hit count on its own.
func (*Registry) Remember ¶
Remember writes one note into the map named label, creating the map when it does not exist, and flushes the snapshot at once. The immediate flush is deliberate: with an idle TTL of 0 the only other flush is shutdown, and a crash in between would lose every note since; a memory map is small, so the write costs milliseconds.
func (*Registry) RemoveSnapshotByLabel ¶
RemoveSnapshotByLabel de-registers a project by label and drops its in-memory graph. No-op if absent. Used to replace/evict a memory-carried snapshot.
func (*Registry) RemoveSnapshotByPath ¶
RemoveSnapshotByPath de-registers whichever project was loaded from path (its snapshot file was deleted). Drops the in-memory graph too. No-op if no entry references that path.
func (*Registry) Resolve ¶
func (r *Registry) Resolve(ctx context.Context, mapName, query string) ([]mcpserver.Resolved, error)
Resolve routes a search to one or more loaded map graphs, in three steps: an exact `mapName` label is a free fast path; then labels NAMED in the route text seed the set; then the semantic rank fills whatever fanout is left. A `mapName` hint that is not a label (e.g. "mcp governance" for the label security-policies) is embedded and cosine-matched against every map's summary vector, the same brute-force scan used for query routing. Maps are returned best-first; each is loaded (from snapshot if evicted) and touched.
func (*Registry) ResolveByQN ¶
ResolveByQN routes a node-keyed tool (get_code_node, trace_code_calls, impact) by the project prefix of a repo-namespaced qualified name ("label//..."). No embedding is needed — the project is named explicitly inside the qn.
func (*Registry) ResolveMap ¶
ResolveMap loads (reviving from snapshot if evicted) the named map for the browser graph UI and returns its graph. An empty label picks the first map in sorted order — so /ui has something to show without an explicit selector.
func (*Registry) SetMemoryTTL ¶
SetMemoryTTL sets the lifetime of a note that names none. Zero restores the built-in default (mcpserver.DefaultMemoryTTL, a week); a negative duration (mcpserver.TTLNever) makes notes permanent unless they say otherwise.
func (*Registry) SetSnapshotDir ¶
SetSnapshotDir names the directory a memory map created at runtime is written into. Registered snapshots each know their own path; a map that does not exist yet has nowhere to go without this.
func (*Registry) SetSnapshotKeys ¶
func (r *Registry) SetSnapshotKeys(k SnapshotKeys)
SetSnapshotKeys installs the key material every snapshot read and write goes through: an identity opens encrypted files and keeps them encrypted on flush; only an explicit recipient encrypts plaintext or newly created maps. See snapshotkeys.go for the model and why. Call before AddSnapshot.
func (*Registry) StartMemorySweeper ¶
StartMemorySweeper sweeps every resident notes map on a timer until ctx is done. The load-time sweep already keeps readers honest; this one is for a map nobody has read in a while, so the snapshot on disk (and what a fresh process would register) shrinks without waiting for a search. No-op when every <= 0 or the registry is read-only.
It returns a wait func that blocks until the sweeper has stopped and any sweep already in flight has finished. A sweep removes the notes from the graph and only then flushes the snapshot (sweepExpired), so "the graph holds no expired note" does not mean the writing is over: a caller that stops watching there races the flush. The server ignores this — its sweeper lives as long as the process — but a test whose snapshot dir is removed when it returns must wait, or RemoveAll deletes the file under os.Create and fails with the directory not empty.
func (*Registry) StartSweeper ¶
StartSweeper runs the idle-eviction loop until ctx is done. No-op when ttl<=0.
func (*Registry) Status ¶
Status reports per-project residency for an optional read-only status endpoint.
func (*Registry) SweepMemory ¶
SweepMemory removes every note in the map label whose expiry is at or before now and flushes when anything went. label must be resident-able; a map that is not a notes map is left alone and reports zero.
func (*Registry) Totals ¶
Totals sums the node and edge counts across all registered projects (from their headers, no graph load) — a one-line "size of everything served".
func (*Registry) UpdateByPaths ¶
func (r *Registry) UpdateByPaths(ctx context.Context, paths []string) (changed, removed, skipped int, unowned []string, err error)
UpdateByPaths routes each changed file to its owning project, re-indexes it in that project's worked copy, and marks the project dirty so it is flushed to disk before eviction / on shutdown. Paths that resolve to no snapshotted repo are returned in unowned rather than dropped: a caller that mistypes a path, or names a file this server does not serve, must not be told "changed: 0" and left to read that as "already up to date".
func (*Registry) WatchDir ¶
WatchDir watches the snapshot directory and keeps the registry in sync with the files on disk without a restart: a new <label>.graph.gob.gz is registered (and a refreshed one re-registered) once its writes settle; a removed one is de-registered immediately. Returns after starting the watcher; the watch goroutine stops on ctx cancellation.
Create/Write events are DEBOUNCED per path (see watchDebounce) so a multi-chunk write registers exactly once, on the finished file. Remove/Rename de-register by path and cancel any pending registration for it. Errors on a single file are logged, not fatal — the watcher keeps running.
type RegistryStats ¶
type RegistryStats struct {
// contains filtered or unexported fields
}
RegistryStats accumulates process-lifetime routing and map-lifecycle metrics for a Registry (Mode B): which map(s) actually got searched and how often, whether a search was explicitly addressed to a map or semantically routed, how wide a semantic fanout actually consulted, and how often a map's body was lazily loaded from its snapshot or evicted back out. It is the request/registry-level complement to index.SearchStats (query-mechanics-level: the semantic/lexical partition and latency inside one map's search) — Registry injects one shared *index.SearchStats into every *index.Index it builds (see load) precisely so those counts also survive the evict/reload cycle that recreates each project's Index; this type is the Registry's own equivalent, owned directly since Registry itself is the one object that already lives for the whole process.
Every counter here is either a lock-free atomic or a small map guarded by an RWMutex whose VALUES are themselves atomics — so incrementing an existing map's counters never blocks a concurrent increment of a different map's counters, only the rare "first search for this label" path takes the write lock.
func NewRegistryStats ¶
func NewRegistryStats() *RegistryStats
NewRegistryStats returns a fresh, zeroed RegistryStats.
func (*RegistryStats) RecordSearch ¶
func (s *RegistryStats) RecordSearch(label string, hits int)
RecordSearch attributes one map's search + returned-hit count. Exported because the caller that knows both a search's resolved map label and its hit count is the mcpserver package's search loop (over Resolve's result), not Registry itself — Registry only decides WHICH maps to consult, not how many hits each one returns. mcpserver.Resolver reaches it as a narrow interface method so mcpserver need not import this package's concrete type.
func (*RegistryStats) Snapshot ¶
func (s *RegistryStats) Snapshot() RegistryStatsSnapshot
Snapshot reads every counter at one point in time.
type RegistryStatsSnapshot ¶
type RegistryStatsSnapshot struct {
Routing RoutingSnapshot `json:"routing"`
Maps map[string]MapCounters `json:"maps"`
MapLoads int64 `json:"mapLoads"`
MapEvictions int64 `json:"mapEvictions"`
}
RegistryStatsSnapshot is a point-in-time, JSON-friendly readout of RegistryStats. (Named to avoid colliding with the unrelated graph Snapshot type this package already exports — see snapshot.go.)
type RoutingSnapshot ¶
type RoutingSnapshot struct {
ExplicitRoutes int64 `json:"explicitRoutes"`
SemanticRoutes int64 `json:"semanticRoutes"`
AvgFanoutConsulted float64 `json:"avgFanoutConsulted"`
}
RoutingSnapshot is a point-in-time readout of routing outcomes.
type Snapshot ¶
type Snapshot struct {
Header SnapshotHeader
Body SnapshotBody
}
Snapshot is the full on-disk payload: header then body, gob-encoded into one gzip stream so the header can be read alone.
func ReadSnapshot ¶
ReadSnapshot decodes the full snapshot (header + body) for loading into the graph on first resolve / after eviction. Plaintext files only; ReadSnapshotWith also opens encrypted ones.
func ReadSnapshotFromWith ¶
func ReadSnapshotFromWith(r io.Reader, keys SnapshotKeys) (*Snapshot, error)
ReadSnapshotFromWith is ReadSnapshotFrom with key material.
func ReadSnapshotWith ¶
func ReadSnapshotWith(path string, keys SnapshotKeys) (*Snapshot, error)
ReadSnapshotWith is ReadSnapshot with key material (see ReadSnapshotHeaderWith for the encrypted-without-key behaviour).
type SnapshotBody ¶
type SnapshotBody struct {
Files map[string]parser.Result // relpath -> parsed Nodes+Edges
Cache map[string][]float32 // nodeText -> vector (the index cache, verbatim)
// HNSW is the serialized ANN index over Cache's vectors. Optional and
// additive (older snapshots leave it nil): when present, load deserializes it
// and SKIPS the rebuild (the dominant cold-load cost); when absent, load
// rebuilds the ANN from the vectors. No version bump — gob reads a missing
// field as nil and the load path falls back to rebuild.
HNSW map[string][]byte
// Meta is an opaque, schema-free attribute bag per node: QN → arbitrary
// string key/values. The snapshot format assigns it NO meaning — recall never
// interprets it; it only round-trips it. Whoever builds the graph owns the
// schema: a code graph might write {"visibility":"public"}, a service graph
// {"tier":"1","domain":"Platform","symptoms":"..."}. Keys live as constants in
// the writer's/reader's code, so adding an attribute is a change
// there with no format bump here. Optional and additive like HNSW — old
// snapshots decode it as nil, old readers ignore it, new readers tolerate it. All
// values are strings by design (a consumer does its own strconv), matching how
// mmap-cell data already stores typed values as text.
Meta map[string]map[string]string
}
SnapshotBody is the heavy part: the parsed graph and its embedding cache.
type SnapshotHeader ¶
type SnapshotHeader struct {
Version int
Label string // project / repo namespace label
RepoPath string // absolute repo root at build time
Fingerprint EmbedFingerprint
BuiltAt time.Time
Nodes int // graph node count at build time (for /status without loading)
Edges int // graph edge count at build time
Summary string // synthesized project description (router input)
SummaryVec []float32 // embedding of Summary — the always-resident routing vector
// SummarySource records where Summary came from: "explicit" (an operator-
// written -summary flag), "readme" (the nearest-to-root README), or "stats"
// (the symbol-name / doc-title fallback, engine/indexer.go's summaryFor).
// Additive field like Nodes/Edges — an older snapshot decodes it as "" and
// is reported as such, never misattributed to one of the three real
// sources. Exists purely for diagnosis: a routing vector you cannot
// inspect is a routing vector you cannot debug (docs/todo/corpus-map-router
// measured this blind before this field existed — three of five served
// maps were routing on a summary built from zero symbol names).
SummarySource string
// Facts is the parser.FactsVersion the graph was extracted under. Additive
// like SummarySource: an older snapshot decodes it as 0. A lower number
// than the running binary's means facts are missing, not that the file is
// unreadable — StaleFacts reports it, and nothing rejects the snapshot.
Facts int
// NoEmbed records that this snapshot was built with `snapshot -no-embed`:
// Body.Cache and Body.HNSW are empty on purpose (the embed pass was
// skipped), and SummaryVec is empty too. Additive like SummarySource/Facts
// — an older snapshot decodes it as false, correctly, since none of them
// were ever built this way. NewIndexerFromSnapshot reads it back to mark
// the loaded Index no-embed (index.Index.SetNoEmbed) rather than silently
// re-embedding every node on load.
NoEmbed bool
}
SnapshotHeader is the small, cheap-to-read prefix of a snapshot: enough to register a project and route to it (Summary + SummaryVec) without decoding the whole graph body.
func ReadSnapshotHeader ¶
func ReadSnapshotHeader(path string) (*SnapshotHeader, error)
ReadSnapshotHeader decodes only the header — cheap enough to run over every snapshot at server start (register + routing vector, body deferred to load). Plaintext files only; ReadSnapshotHeaderWith also opens encrypted ones.
func ReadSnapshotHeaderFromWith ¶
func ReadSnapshotHeaderFromWith(r io.Reader, keys SnapshotKeys) (*SnapshotHeader, error)
ReadSnapshotHeaderFromWith is ReadSnapshotHeaderFrom with key material.
func ReadSnapshotHeaderWith ¶
func ReadSnapshotHeaderWith(path string, keys SnapshotKeys) (*SnapshotHeader, error)
ReadSnapshotHeaderWith is ReadSnapshotHeader with key material. An encrypted file with no matching identity fails with ErrSnapshotEncrypted (wrapped, with the flag to pass), never with a gzip or gob decode error.
func (SnapshotHeader) StaleFacts ¶
func (h SnapshotHeader) StaleFacts() bool
StaleFacts reports whether a snapshot was extracted before facts this binary's extractors emit.
type SnapshotKeys ¶
type SnapshotKeys struct {
// contains filtered or unexported fields
}
SnapshotKeys is the optional key material snapshot reads and writes carry. The zero value means "no keys": plaintext files read and write as before, an encrypted file is refused with ErrSnapshotEncrypted.
func LoadSnapshotKeys ¶
func LoadSnapshotKeys(identityPath, recipient string) (SnapshotKeys, error)
LoadSnapshotKeys reads an age identity file (identityPath, "" for none) and parses a recipient string (`age1…`, "" for none). Either may be empty; both empty yields the zero SnapshotKeys.
func (SnapshotKeys) CanDecrypt ¶
func (k SnapshotKeys) CanDecrypt() bool
CanDecrypt reports whether an identity is loaded.
func (SnapshotKeys) Encrypts ¶
func (k SnapshotKeys) Encrypts() bool
Encrypts reports whether writes of NEW or plaintext-origin files are encrypted — i.e. whether a recipient was named. An identity alone does not count: see WriteRecipient.
func (SnapshotKeys) IdentityRecipient ¶
func (k SnapshotKeys) IdentityRecipient() string
IdentityRecipient is the loaded identity's own public key, as text for logs; "" without an identity. It is what a file read encrypted is re-encrypted to.
func (SnapshotKeys) KeyPath ¶
func (k SnapshotKeys) KeyPath() string
KeyPath is where the identity was read from, for logs.
func (SnapshotKeys) Recipient ¶
func (k SnapshotKeys) Recipient() string
Recipient is the explicit public key writes encrypt to, as text for logs; "" when no -snapshot-recipient was given.
func (SnapshotKeys) RecipientMismatch ¶
func (k SnapshotKeys) RecipientMismatch() bool
RecipientMismatch reports the one combination worth a startup warning: both an identity and a recipient are set, and they differ. Writes then go to a key this process cannot open, so after a restart the server cannot read what it wrote. Not refused — an operator rotating keys wants exactly that, briefly — but it must be visible in the log.
func (SnapshotKeys) WriteRecipient ¶
func (k SnapshotKeys) WriteRecipient(loadedEncrypted bool) age.Recipient
WriteRecipient decides how one write is protected. loadedEncrypted says whether the file being rewritten was encrypted when it was read (false for a new file). nil means plaintext. This is the single place the write policy lives — see the package comment for the rule and the incident behind it.