Documentation
¶
Index ¶
- Constants
- Variables
- func AnchorNodeID(kind, target, scope string) string
- func Answer(input string, matched bool, cov Coverage) types.KnowledgeAnswer
- func CouldMatchLazyLayer(input string) bool
- func DiffGraphs(baseLabel string, before, after types.KnowledgeGraphOutput) types.KnowledgeGraphDiff
- func IsHistoryAttr(key string) bool
- func IsRuntimeAttr(key string) bool
- func LoadRuntimeEvents(cacheDir string) []types.DiagnosticEvent
- func ProjectPaths(cacheDir string) []string
- func RecordRuntimeEvents(cacheDir string, fresh []types.DiagnosticEvent) error
- func SeedsLazyLayer(input string) bool
- func StoreDir(cacheDir string) string
- func UnionInto(dst, src *Graph)
- type BuildOptions
- type Coverage
- type CoverageBlock
- type CoverageFacts
- type FileCoverage
- type FileFacts
- type Graph
- func (g *Graph) AddEdge(e types.KnowledgeEdge)
- func (g *Graph) AddNode(n types.KnowledgeNode)
- func (g *Graph) Dependents(id string) []string
- func (g *Graph) Edges() []types.KnowledgeEdge
- func (g *Graph) Explain(ref string) (types.KnowledgeExplainOutput, bool)
- func (g *Graph) FileFacts(relPath string) FileFacts
- func (g *Graph) Fingerprint() string
- func (g *Graph) HasSymbols() bool
- func (g *Graph) Merge(nodes []types.KnowledgeNode, edges []types.KnowledgeEdge)
- func (g *Graph) NearestNode(input string) string
- func (g *Graph) NearestSymbol(ref string) string
- func (g *Graph) Neighborhood(seeds []string, budget int, relations []string) *Graph
- func (g *Graph) Nodes() []types.KnowledgeNode
- func (g *Graph) Output() types.KnowledgeGraphOutput
- func (g *Graph) Path(a, b string) (types.KnowledgePathOutput, bool)
- func (g *Graph) Query(input string, budget int) types.KnowledgeQueryOutput
- func (g *Graph) QueryPage(input string, budget, offset, limit int) types.KnowledgeQueryOutput
- func (g *Graph) Refs(ref string) (types.KnowledgeRefsOutput, bool)
- func (g *Graph) Resolve(input string, limit int) []types.KnowledgeMatch
- func (g *Graph) Routing() types.KnowledgeRouting
- func (g *Graph) Select(input string, budget int) types.KnowledgeGraphOutput
- func (g *Graph) SetRoot(root string)
- func (g *Graph) Stats(kind string) types.KnowledgeStats
- func (g *Graph) SymbolAt(relPath string, line int) SymbolSpan
- func (g *Graph) Unreferenced() []types.UnreferencedEntry
- type Inputs
- type NoteResolver
- func (r NoteResolver) DeclDigest(_ context.Context, a notes.Anchor) (string, error)
- func (r NoteResolver) Digest(_ context.Context, a notes.Anchor) (string, error)
- func (r NoteResolver) ForScope(scope string) NoteResolver
- func (r NoteResolver) NodeID(a notes.Anchor) string
- func (r NoteResolver) Resolves(_ context.Context, a notes.Anchor) bool
- type RemoteShards
- type Shard
- type Store
- func (s *Store) Load(ctx context.Context) (*Graph, error)
- func (s *Store) MergeSymbolShards(ctx context.Context, g *Graph) error
- func (s *Store) MergeSymbolShardsByID(ctx context.Context, g *Graph, symbolIDs []string) error
- func (s *Store) Sync(ctx context.Context, shards []Shard, fps map[string]string, refresh bool) (*Graph, error)
- type SymbolFacts
- type SymbolSpan
Constants ¶
const ( // AttrEngine is the engine (toolchain runtime) a project runs, mirrored onto // each of its targets so a target card names its engine without walking to the // project node. AttrEngine = "engine" // AttrTargetCount is a project's target count - its size at a glance, without // counting contains edges. AttrTargetCount = "target_count" )
Static-metadata attribute keys. These surface data the extractors already parse (the engine a project runs, its target count, a doc's frontmatter) directly onto nodes, so `magus explain` answers "what toolchain / how big / what is this doc" without a second describe or a cross-reference. Additive: absent when unknown.
const ( // AttrDurationP75Ms is a target's p75 run duration in milliseconds. AttrDurationP75Ms = "duration_p75_ms" // AttrLastOutputRef is the output reference id (the "out1a2b3c" token) of the // target's most recent captured execution, so an agent can jump from a target node // straight to its last output with `magus query output <ref>` - the query -> target // -> output two-hop. Sourced from the output store (the timing history carries no // refs); absent when the store holds no execution for the target. AttrLastOutputRef = "last_output_ref" // AttrLastRunOK is whether that most recent execution succeeded ("true"/"false"), so // the ref's outcome is legible from the node without fetching the output. AttrLastRunOK = "last_run_ok" )
Runtime-performance attribute keys. Unlike the static keys above these are OBSERVED (from local run history, not workspace sources), so they ride the isolated @runtime shard: an agent planning work sees a target's cost without a separate history query, and the observed/derived split stays clean. Absent when no history backs the target.
const ( // AttrDirFiles is how many path-bearing files/docs the directory holds transitively. AttrDirFiles = "dir_files" // AttrDirCommits is the summed git churn (commit counts) across those files - where a // subsystem's change activity concentrates. AttrDirCommits = "dir_commits" // AttrDirLanguages is the sorted, comma-joined set of languages present under the // directory, derived from file extensions. Distinct from a file node's single-valued // "language" attr - a directory spans languages, so this is a dir-scoped set. AttrDirLanguages = "dir_languages" )
Directory aggregate keys. These roll up from a directory's files (transitively) so a dir node reads as a subsystem summary - the granularity agent memory anchors to and dir-level coupling/churn queries read against. All are deterministic and OS-agnostic (git commit counts, extension-derived languages, slash-relative paths), so the @dirs shard is remote-shareable like @registry and @vcs.
const ( // AttrCoveredStmts is how many statements the profile recorded at least one hit for. AttrCoveredStmts = "covered_stmts" // AttrTotalStmts is the instrumented statement count backing the ratio - the // denominator, so a 0/0 file is distinguishable from a small sample. AttrTotalStmts = "total_stmts" )
Coverage attribute keys. Like the runtime keys these are OBSERVED - parsed from the local Go coverage profile magus produces (`magus run coverage`), not from workspace sources - so they ride an isolated, lazily-loaded @coverage shard that folds onto the file and symbol nodes SCIP already minted. They answer "which code lacks coverage" straight off a node. Absent when no profile covers the file/symbol.
const ( ScopePrivate = string(notes.ScopePrivate) )
Derived from the store's own scopes rather than restated, so the shard names, node IDs, and the config keys a diagnostic prints can never disagree about what a scope is called.
const ( StalenessCurrent = "current" // the prose is at least as recent as its subject StalenessOutrun = "outrun" // the subject changed after the prose did StalenessPetrified = "petrified" // the subject has been moving for a long time without it )
Prose staleness levels, folded onto doc and note nodes and read by retrieval ranking.
The signal is NOT calendar age. A doc written three years ago about a subsystem nobody has touched since is perfectly current, and decaying it by age would flag it for no reason - which is how a signal earns the right to be ignored. What is measured instead is DIVERGENCE: the prose was last touched, and then the thing it describes moved on without it. That is a fact about two commit dates, not a heuristic about age.
const ( AttrStaleness = "staleness" AttrOutrunDays = "outrun_days" )
AttrStaleness and AttrOutrunDays carry the divergence onto a prose node.
AttrOutrunDays is the raw number so a reader can judge for themselves and a UI can say WHY something ranked low. The bucket exists only so ranking has something coarse to key on; the number is the evidence, and it is the thing to show.
const (
AttrPackageVersion = "version"
)
attrPackageManager, AttrPackageVersion, attrPackageIndirect and attrPackageReplaced are the attrs a package node carries. Named rather than spelled inline because a mistyped literal would silently match nothing on query instead of failing.
const DefaultBudget = 50
DefaultBudget bounds the neighborhood a query collects, so a match on a high-degree node cannot pull in the whole graph.
const ProvenanceRuntime = runtimeShardName
ProvenanceRuntime marks an edge the runtime shard contributed, so consumers that must not depend on local run history can drop it after the shard boundary is gone. It keeps the "@" because provenance otherwise holds a source path, often a bare top-level directory name: plain "runtime" would collide with a runtime/ directory.
const QualifierSep = "//"
QualifierSep joins a workspace to a node ID in a global graph ("web//spell:go"); kept a substring so fuzzy resolution still matches the bare ID.
Variables ¶
var ErrNoStore = errors.New("knowledge: no persisted graph")
ErrNoStore reports that the knowledge store has never been written (no manifest).
var ErrShardMiss = errors.New("knowledge: shard not on remote")
ErrShardMiss reports that a shard key is not on the remote. GetShard returns it (not a nil reader) for a miss, so the contract is unambiguous: a nil error means a non-nil reader.
Functions ¶
func AnchorNodeID ¶ added in v0.4.0
AnchorNodeID renders one note anchor as the node ID the graph mints for it, or "" for a kind with no node form. scope is the scope of the ANCHORING note, because a note-to-note anchor names a note in the SAME store: a private note referring to "auth" means its own, not the team's.
One home for a mapping with three callers across two process phases - assembly (which turns an anchor into an edge), resolution (which asks whether an anchor still names something live), and the console handler. Two hand-kept copies existed and had already diverged on exactly the case a reader is least likely to notice: the resolver's copy took no scope, so it looked up a private note's note-anchor in the SHARED namespace, reported it dangling, and told the author to re-anchor a note that was never broken - while assembly had minted the edge correctly all along.
func Answer ¶ added in v0.4.0
func Answer(input string, matched bool, cov Coverage) types.KnowledgeAnswer
Answer classifies a lookup's result against its coverage. input is the query text, used only to ask whether the lazy layer was relevant at all.
func CouldMatchLazyLayer ¶ added in v0.4.0
CouldMatchLazyLayer reports whether a query could ever match a node in the lazily-loaded layer, which is a weaker question than SeedsLazyLayer: it asks whether that layer is RELEVANT, not whether it was loaded.
It is DERIVED from SeedsLazyLayer rather than deciding the same thing a second way, and that is the whole safety property: relevance is a strict superset of seeding by construction, so widening SeedsLazyLayer can never leave a query that now loads the layer outside the set of queries allowed to caveat it. Two parallel implementations is exactly how `kind=file <name>` came to skip the shards AND assert a verified absence about them.
The two differ for exactly the queries where an unloaded-layer caveat would mislead. `kind:author` returning nothing has nothing to do with code symbols, so telling the reader that symbols were not searched points them at a layer that could not have held the answer. A query naming only kinds outside lazyLayerKinds has ruled the layer out itself; everything else leaves it open.
func DiffGraphs ¶
func DiffGraphs(baseLabel string, before, after types.KnowledgeGraphOutput) types.KnowledgeGraphDiff
DiffGraphs reports how the graph changed from base to current: nodes added, removed, or changed (same ID, different data), and edges added or removed. baseLabel names the base revision (or baseline file) and is echoed into the result.
func IsHistoryAttr ¶ added in v0.4.0
IsHistoryAttr reports whether an attr key is derived from git history, so a reproducible export can drop it.
Distinct from IsRuntimeAttr because the failure is different. An observed attr varies by MACHINE; these vary by COMMIT, which is worse for a checked-in artifact: committing anything moves the churn, so the file invalidates itself and the drift gate fires on the very commit that regenerated it.
func IsRuntimeAttr ¶ added in v0.4.0
IsRuntimeAttr reports whether an attr key holds observed run history, so a reproducible export can drop it. A func, not an exported slice: an importer could write to the slice and silently un-strip a key. Mirrors isRuntimeShard.
func LoadRuntimeEvents ¶
func LoadRuntimeEvents(cacheDir string) []types.DiagnosticEvent
LoadRuntimeEvents reads the persisted runtime diagnostic records; a missing or unreadable file yields no events (runtime enrichment is best-effort).
func ProjectPaths ¶ added in v0.4.0
ProjectPaths returns the project paths recorded in the knowledge manifest, sorted, or nil when no readable manifest exists. The manifest is the honest cheap source for "which projects exist": its shard keys come from ws.ListProjects at graph-build time, so one small ReadFile answers the question in a hook where a workspace eval is not allowed.
func RecordRuntimeEvents ¶
func RecordRuntimeEvents(cacheDir string, fresh []types.DiagnosticEvent) error
RecordRuntimeEvents merges fresh events into the persisted records, deduped by (unit, code) and capped at defaultRuntimeCap (oldest dropped). Called once at run end; best-effort (an unwritable cache is not fatal).
func SeedsLazyLayer ¶ added in v0.4.0
SeedsLazyLayer reports whether an input targets the lazily-loaded @symbols shards, so a caller knows to merge them into the default graph: a symbol: ID, any kind the layer holds (incl. wildcard), a defines/references/calls relation, or any language filter. It must agree with scoreNode - a match that reaches those shards without seeding here returns empty. Over-eager is safe: it only loads shards a later filter may discard.
Types ¶
type BuildOptions ¶
type BuildOptions struct {
Immutable bool // set when cache.write.enabled is false: load-only, never write
Refresh bool // force a full rebuild regardless of fingerprints
MaxBytes int64 // soft cap on the shards dir; 0 = unlimited
Remote RemoteShards // optional remote shard backing; nil = local-only
}
BuildOptions carries the build toggles so callers pass named fields rather than a row of transposable booleans.
type Coverage ¶ added in v0.4.0
type Coverage struct {
// Seeded reports that the lazily-loaded @symbols shards were merged for this lookup.
Seeded bool
// Probed reports that the declared-index probe ran. False means Gaps says nothing - an
// empty gap list from a failed probe would read as verified coverage.
Probed bool
// Gaps are the projects whose declared symbol index could not be read.
Gaps []types.KnowledgeSymbolGap
// Stale are the workspace-relative projects whose built index predates its sources.
Stale []string
// IndexOnly marks a lookup whose ENTIRE evidence base is the symbol index, so a stale
// index leaves a miss unverifiable. `magus refs` is the one: it resolves symbol nodes
// and consults nothing else. A general query reads many layers, and downgrading every
// empty one in an actively edited tree would make the verdict noise a caller learns to
// ignore - the same trap refs' -o name exit code documents.
IndexOnly bool
}
Coverage is what a lookup was actually able to consult. Every field is an OBSERVATION, never a re-derivation: Seeded is what the caller loaded, not what it should have loaded, so a caller that skips the lazy layer cannot also be the one that decides the skip was harmless.
type CoverageBlock ¶
CoverageBlock is one line-range record from a Go coverage profile: the statement count the range holds and whether the run hit it. Retained per file (not just aggregated) so coverage can be attributed to individual symbols by line range.
type CoverageFacts ¶
CoverageFacts is a covered/total statement tally and its ratio (0..1), read back from the coverage attrs the @coverage overlay folded onto a file or symbol node.
type FileCoverage ¶
type FileCoverage struct {
Path string
Covered int
Total int
Blocks []CoverageBlock
}
FileCoverage is one source file's parsed coverage: its workspace-relative path, the covered/total statement totals (the file-level ratio), and the raw blocks (for symbol-level attribution). Covered counts statements the run hit at least once.
func ParseCoverage ¶
func ParseCoverage(profile []byte, modulePath string) []FileCoverage
ParseCoverage parses a Go coverage profile into per-file coverage, keyed by workspace-relative path. Profile lines are module-qualified ("github.com/egladman/magus/internal/foo.go:12.2,14.16 3 1"), so modulePath (the go.mod module path) is stripped to recover the workspace-relative path the file and symbol nodes use. A line outside modulePath (a nested module, or a std/vendored path) is dropped rather than mis-attributed. Malformed lines are skipped, never fatal: coverage is best-effort enrichment. The result is sorted by path for deterministic assembly. A profile that covers nothing yields nil.
type FileFacts ¶
type FileFacts struct {
// Coverage is the file-level observed coverage, or nil when the @coverage overlay
// does not cover this file (no `magus run coverage`, or the file has no statements).
Coverage *CoverageFacts
// Symbols are the symbols defined in the file, sorted by descending reference count
// then ID, so the most-referenced (highest-blast-radius) symbol leads.
Symbols []SymbolFacts
}
FileFacts is the impact overlay for one workspace-relative source file: the symbols it defines (each with how widely it is referenced and its observed coverage) and the file-level coverage. It is the read surface `magus affected --impact` folds onto its blast radius - callers (from the SCIP reference edges the @symbols shards carry) and coverage (from the @coverage overlay) for exactly the files a changeset touched. The zero value (no coverage, no symbols) is the honest answer for a file with no ingested symbol node, so the caller degrades gracefully rather than treating it as an error.
type Graph ¶
type Graph struct {
// contains filtered or unexported fields
}
Graph is the in-memory knowledge graph: the union of every shard's nodes and edges, keyed for dedup and emitted in deterministic order. It is assembled at load time (shards are authoritative on disk; there is no continuously merged file). Not safe for concurrent mutation - build it on one goroutine, then read.
Reads ARE safe for concurrent use, including from many goroutines that have never seen each other: the daemon's warm graph hands the same *Graph to concurrent HTTP/MCP requests once assembly finishes. The lazy indices below are the one place a "read" still writes, so they coalesce concurrent first-builds under a mutex rather than racing (see ensureAdj, projectPaths).
func Build ¶
func Build(ctx context.Context, cacheDir string, opts BuildOptions, in Inputs, log *slog.Logger) (*Graph, error)
Build is the cache-first entry point: it assembles every shard from the gathered inputs, fingerprints each by content, reconciles them against the persisted store, and returns the merged in-memory graph. First run pays a full build; steady state writes only the shards whose content changed.
func NewGraph ¶
func NewGraph() *Graph
NewGraph returns an empty graph ready for AddNode/AddEdge/Merge.
func Qualified ¶
Qualified returns a copy of g with every node ID and edge endpoint prefixed by "<workspace>//", so a global graph can union many workspaces without ID collisions. The input graph is not modified.
func (*Graph) AddEdge ¶
func (g *Graph) AddEdge(e types.KnowledgeEdge)
AddEdge inserts a directed edge, deduplicating by (source, target, relation). On collision the higher-confidence edge (extracted over inferred, then higher score) is kept, so the merged graph is independent of shard load order.
func (*Graph) AddNode ¶
func (g *Graph) AddNode(n types.KnowledgeNode)
AddNode inserts a node, or upgrades an existing one with the same ID by filling empty fields from the newcomer. Idempotent: the same node from two shards (e.g. an op node the registry declares and a project references) merges cleanly. Only EMPTY fields are filled - when both shards carry a non-empty Doc/Source/Label, the first writer wins and insertion order decides.
func (*Graph) Dependents ¶ added in v0.4.0
Dependents returns every node that transitively DEPENDS ON id, as ids, nearest first.
Deliberately narrower than blastRadius below, which is a different question wearing a similar name: blastRadius counts everything that reaches a node by ANY relation, so a doc that documents a spell is in it. This walks `depends_on` alone, which is what "what rebuilds if I change this" means - and the two diverge hard. Nothing depends_on a spell (a target USES one), so a spell's blastRadius is in the hundreds while its Dependents is empty, and both are correct answers to their own question.
Ids rather than a count, because the caller highlights them. Unbudgeted: the result is bounded by the depends_on subgraph, which is the build DAG rather than the whole knowledge graph.
func (*Graph) Edges ¶
func (g *Graph) Edges() []types.KnowledgeEdge
Edges returns every edge sorted by (source, target, relation).
func (*Graph) Explain ¶
func (g *Graph) Explain(ref string) (types.KnowledgeExplainOutput, bool)
Explain resolves ref to a node and returns its context card, or ok=false when nothing resolves.
func (*Graph) FileFacts ¶
FileFacts returns the caller and coverage overlay for a workspace-relative source file. It walks the file node's outgoing `defines` edges to the symbols declared in it, tallies each symbol's incoming SCIP `references` edges (occurrence count and distinct files), and reads the coverage attrs the @coverage overlay merged onto the file and symbol nodes. A file with no symbol node (no SCIP index ingested, or a non-code file) yields the zero value. Callers must have merged the @symbols and @coverage shards (KnowledgeGraphWithSymbols / MergeWorkspaceSymbols) first; on a symbol-free graph every file yields the zero value.
func (*Graph) Fingerprint ¶
Fingerprint is a content hash of the graph's shape: the sorted node IDs and edge keys. It identifies the graph state so a stateless pagination cursor can detect that the graph changed underneath it (a warm-graph invalidation between pages) and fail loudly rather than return an incoherent slice. Deterministic (fed from the sorted Nodes/Edges), SHA256 to match the rest of the store.
func (*Graph) HasSymbols ¶
HasSymbols reports whether the graph holds any ingested code symbol node. refs and a symbol-seeded query load the @symbols shards lazily, so when this returns false after that load, no SCIP index has been ingested at all. Callers use it to tell "no index built" apart from "index built, but this symbol is absent": the former is fixed by building the index, the latter by correcting the symbol.
func (*Graph) Merge ¶
func (g *Graph) Merge(nodes []types.KnowledgeNode, edges []types.KnowledgeEdge)
Merge folds a shard's nodes and edges into the graph.
func (*Graph) NearestNode ¶ added in v0.4.0
NearestNode returns the id of the node whose name is a typo away from input's single free-text term, or "" when nothing is that close. The query's kind filters still apply, so a suggestion is a node the query would have accepted.
Only a single bare term is answered: with two terms there is no one thing the reader misspelled, and a wildcard that matched nothing is a pattern to widen rather than a name to correct.
func (*Graph) NearestSymbol ¶ added in v0.4.0
NearestSymbol is NearestNode restricted to code symbols, for refs - which resolves nothing else, so suggesting a target or a doc there would name something refs would miss a second time.
func (*Graph) Neighborhood ¶
Neighborhood collects the induced subgraph reachable from seeds within a node budget, treating edges as bidirectional so a query surfaces both what a node depends on and what depends on it. When relations is non-empty, only edges with those relations are traversed. Returns a fresh Graph for deterministic output.
func (*Graph) Nodes ¶
func (g *Graph) Nodes() []types.KnowledgeNode
Nodes returns every node sorted by ID (stable, deterministic).
func (*Graph) Output ¶
func (g *Graph) Output() types.KnowledgeGraphOutput
Output renders the merged graph as the node-link export. Nodes and edges are sorted so identical inputs produce byte-identical JSON (required for cache fingerprinting, golden tests, and meaningful diffs).
func (*Graph) Path ¶
func (g *Graph) Path(a, b string) (types.KnowledgePathOutput, bool)
Path resolves both endpoints and returns the shortest connecting path (edges bidirectional). ok=false only when an endpoint fails to resolve; a resolved pair with no connection returns Found=false.
func (*Graph) Query ¶
func (g *Graph) Query(input string, budget int) types.KnowledgeQueryOutput
Query resolves the input to seeds and returns the ranked matches plus their neighborhood subgraph, bounded by budget. It is the unpaged view: every match, in one response - QueryPage with a zero offset and no limit, so the two cannot drift.
func (*Graph) QueryPage ¶
func (g *Graph) QueryPage(input string, budget, offset, limit int) types.KnowledgeQueryOutput
QueryPage is Query with a match window: it returns the total MatchCount but only the matches in [offset, offset+limit) (limit <= 0 means "to the end"), and builds the neighborhood from that page's seeds so a page is a self-contained result. It is the substrate for MCP pagination, where a large match set (symbol references) must be returned across several bounded responses. offset past the end yields an empty page with the true MatchCount, so a caller can stop.
func (*Graph) Refs ¶
func (g *Graph) Refs(ref string) (types.KnowledgeRefsOutput, bool)
Refs resolves ref to a node and lists where it is defined and every file that references it, as occurrence-shaped sites (file + count + lines) rather than a node-link neighborhood. The reference counts and lines come from the SCIP-ingested `references` edges' provenance; `defines` edges give the definition file(s). Sites are sorted by file for determinism. ok=false when ref does not resolve.
func (*Graph) Resolve ¶
func (g *Graph) Resolve(input string, limit int) []types.KnowledgeMatch
Resolve returns nodes matching the query, ranked by score (desc) then ID (asc), truncated to limit (0 = no limit).
func (*Graph) Routing ¶
func (g *Graph) Routing() types.KnowledgeRouting
Routing derives the compact "query first" routing summary: per-kind counts with a few highest-degree anchor nodes, and per-project target counts with key targets. Degree (in + out) is the cheap "how connected / how central" proxy the plan calls god nodes; ties break by ID so the summary is deterministic.
Two inputs are excluded because MAGUS.md is committed and drift-gated. Runtime edges, so the table does not rank on which diagnostics THIS machine tripped. And git history - the author kind and its `authored` edges - because that varies by COMMIT: a contributor appearing under a second identity moved the author count and rewrote a committed file that no source change had touched. Degree is what makes the second one subtle, since authored edges also decide which nodes each row lists as anchors.
`magus graph stats` keeps both on purpose: an interactive query wants local context, so its EdgeCount and god nodes differ from these. Independence from the MACHINE is still not claimed - the @docs/@buzz filesystem walks feed this table.
func (*Graph) Select ¶
func (g *Graph) Select(input string, budget int) types.KnowledgeGraphOutput
Select resolves the input to seeds and returns the induced neighborhood as a node-link export (the emit side of `magus graph export --select`), sharing the seed+neighborhood logic with Query so graph and query stay one substrate. An input that resolves to nothing yields an empty graph.
func (*Graph) SetRoot ¶ added in v0.4.0
SetRoot records the workspace root the graph's node IDs are relative to. Every resolution path normalizes a pasted path against it (see normalizePaths), so a graph without one answers absolute and backslash spellings as if they were literal text.
func (*Graph) Stats ¶
func (g *Graph) Stats(kind string) types.KnowledgeStats
Stats computes the knowledge-graph analytics behind `magus graph stats`: god nodes (highest degree - where risk concentrates), orphans (isolated docs, unused spells), and doc coverage per documentable kind. kind, when non-empty, scopes every section to that node kind. Deterministic and LLM-free.
func (*Graph) SymbolAt ¶ added in v0.4.0
func (g *Graph) SymbolAt(relPath string, line int) SymbolSpan
SymbolAt returns the symbol whose definition encloses a 1-based line of a workspace-relative file, by the nearest-preceding-definition rule.
The exported form of a lookup this package had written twice, unexported and single-purpose: once to attribute coverage blocks and once inside the SCIP parser to attribute calls. Both answered the same question, and neither could be asked from outside - so the review surface, which wants "which function is this hunk in", had no way to find out.
Nearest-preceding rather than range-containment BECAUSE the end line is often missing. A containment test would answer "no symbol" for every indexer that emits no enclosing range, which is the commoner case and the one where a reader still wants an answer. Where EndLine IS known a caller can check it and decide; where it is not, this still names the declaration the line belongs to.
The zero value means no symbol covers the line: a file with no ingested symbols, a line above the first definition, or a graph whose symbol shards were never merged. All three are "magus does not know", never "this line belongs to nothing".
func (*Graph) Unreferenced ¶ added in v0.4.0
func (g *Graph) Unreferenced() []types.UnreferencedEntry
Unreferenced lists the symbols this workspace defines and nothing in it names.
A symbol qualifies when nothing outside its own definition file reaches it: no call from a symbol defined elsewhere, and no reference from another file.
Both clauses are needed and they are deliberately symmetric on same-file use. The calls edge is the sharp one - it names the caller, so an unreferenced function is genuinely uncalled rather than merely unmentioned. The file-reference clause covers what a call edge cannot be: a struct, a field, a constant, which are referenced and never called. Applying the same-file rule to only one of them would hide every function used once in its own file while listing every type in exactly that position.
This is a measurement, not a verdict. The graph cannot see reflection, interface dispatch, a call site in a file no indexer covered, a build tag that was off when the index ran, or any consumer outside this workspace. It also cannot see a call from a package-level initializer, which has no enclosing range for the attribution to land in. The caller is responsible for carrying that caveat to the reader, which is why the CLI output states it and why the result rides a coverage verdict.
type Inputs ¶
type Inputs struct {
Graph types.TargetGraphOutput // TargetGraph(): projects, targets, deps, charms, spell ops
Spells []types.Spell // ListSpells(): spell + op nodes
Modules []types.ModuleEntry // host modules, each with Methods populated
Diagnostics []types.DiagnosticCode // AllDiagnosticCodes()
// Root is the absolute workspace root, used by the docs and buzz-source
// extractors to scan the filesystem. Empty disables those extractors (the
// store tests build from synthetic Inputs with no tree to scan).
Root string
// Runtime carries diagnostics fired during prior runs, read from the local
// runtime records. It is the ONLY non-deterministic input: derived from run
// history, not workspace sources, so it lands in an isolated @runtime shard
// that is excluded from remote export and skippable at load.
Runtime []types.DiagnosticEvent
// Timings carries observed per-target run cost (p75 duration, cache hit rate)
// from the local timing history. Like Runtime it is non-deterministic and lands
// in the @runtime shard; it annotates existing target nodes rather than adding
// edges.
Timings []types.KnowledgeTiming
// OutputRefs carries each target's most recent captured-output reference from the
// local output store. Like Timings it is non-deterministic and lands in the @runtime
// shard, folding last_output_ref / last_run_ok attrs onto existing target nodes rather
// than adding edges - the query -> target -> last output two-hop.
OutputRefs []types.KnowledgeOutputRef
// Symbols maps a project path to the code symbols ingested from its SCIP index
// (empty unless the project declares one in config). Each becomes a per-project
// @symbols shard - deterministic, so remote-shareable like the other extracted
// shards, and destined for lazy loading (it can dwarf the domain graph).
Symbols map[string][]types.KnowledgeSymbol
// Packages maps a project path to the third-party dependencies its manifest
// declares, at the versions that manifest resolves to. They merge into the single
// @packages shard rather than one per project, because a package node is shared
// between the projects that require it - see packagesShardName. Deterministic, so
// remote-shareable like the other extracted shards.
Packages map[string][]types.KnowledgePackage
// VCS carries per-file git history metadata (empty unless knowledge.vcs.enabled and
// the workspace is a git repo). It folds onto existing file nodes in the @vcs shard
// as attrs - deterministic per commit, so remote-shareable.
VCS []types.KnowledgeVCS
// DeclaredSpells is the set of spell names some project declares in its magusfile
// `spells:` list (the union over projects). It lets the orphan lens tell a genuinely
// dead spell (declared here, nothing runs it) from a compiled-in builtin that is
// merely available and unused - only declared spells are orphan candidates.
DeclaredSpells map[string]bool
// VCSAuthorship includes the author nodes + authored edges in the @vcs shard
// (knowledge.vcs.authorship, default on). False keeps only the per-file vcs_* attrs.
VCSAuthorship bool
// NotesPath is the workspace-relative directory holding human-authored notes
// (knowledge.notes.path), empty when the workspace declares none. It is not a source
// of nodes here - it EXCLUDES one: the docs walk indexes every .md in the tree, so
// without this a notes store becomes kind:doc nodes and stops being distinguishable
// from documentation.
NotesPath string
// Notes carries the workspace's human-authored notes with their anchors already
// resolved to node IDs (empty unless knowledge.notes.path is declared). Committed to
// the repo, so deterministic and remote-shareable - the opposite of @memory.
Notes []types.KnowledgeNote
// PrivateNotes are the reader's own notes (knowledge.notes.personal), which may live
// outside any repository. Same shape and same anchors as Notes; different trust, and a
// shard that is never remote-exported.
PrivateNotes []types.KnowledgeNote
// Coverage carries per-file statement coverage parsed from the local Go coverage
// profile (empty unless a profile is present). Like Runtime/Timings it is observed,
// not extracted, so it lands in the isolated @coverage shard - folding a coverage
// ratio onto the file (and, via SCIP def lines, symbol) nodes rather than churning
// the deterministic @symbols shards it annotates.
Coverage []FileCoverage
}
Inputs are the already-gathered describe outputs the assembler composes. The caller (the CLI/composition root) fetches these from the workspace so that internal/graph/knowledge depends only on types - it never reaches into the registry, host, or spell packages itself.
type NoteResolver ¶ added in v0.4.0
type NoteResolver struct {
// contains filtered or unexported fields
}
NoteResolver answers anchor resolution from a knowledge graph's node set.
The dependency runs ONE WAY on purpose: this package learns about notes, and internal/notes still knows nothing about the graph. That is what keeps the store unit-testable with no graph to build, and it is why the resolver lives here rather than beside the store it serves.
It lives in a package rather than at a composition root because it now has two callers - `magus notes verify` and the console's NotesService - and the previous arrangement, one copy per caller, is exactly how the anchor-to-id mapping silently diverged.
func NewNoteResolver ¶ added in v0.4.0
func NewNoteResolver(root string, g *Graph) NoteResolver
NewNoteResolver indexes g once. Bind it to a store with ForScope before checking that store's notes; the zero scope resolves a note-to-note anchor in the shared namespace, which is right for the shared store and wrong for the private one.
func (NoteResolver) DeclDigest ¶ added in v0.4.0
DeclDigest fingerprints the anchored symbol's declaration line, which is what grades a content change as "what this IS moved" rather than "how it does it moved".
Symbols only. A file anchor's declaration is the file, so grading it against itself would answer nothing; project, target and note anchors have no content at all. Both return "" with a nil error - an ungraded finding, never a second complaint about the same anchor.
func (NoteResolver) Digest ¶ added in v0.4.0
Digest fingerprints the anchored source as it is right now.
No failure path invents a change: a caller reads both the empty digest and the error as "no opinion" for drift purposes, because a gate that cries wolf out of its own blind spot gets ignored and an ignored gate is worse than none. What the error adds is WHICH blind spot, since the four causes below are differently actionable and used to be one silence.
func (NoteResolver) ForScope ¶ added in v0.4.0
func (r NoteResolver) ForScope(scope string) NoteResolver
ForScope returns a copy bound to one store. The maps are shared by reference and read only, so this is cheap enough to do per store rather than indexing the graph twice.
func (NoteResolver) NodeID ¶ added in v0.4.0
func (r NoteResolver) NodeID(a notes.Anchor) string
NodeID renders an anchor as the node ID the graph mints for it, in this resolver's scope. Exported because a caller that resolved an anchor usually wants to LINK to the node it named, and re-deriving the id at the call site is how the two spellings drifted before.
type RemoteShards ¶
type RemoteShards interface {
GetShard(ctx context.Context, key string) (io.ReadCloser, error)
PutShard(ctx context.Context, key string, r io.Reader) error
}
RemoteShards lets the store ride a remote cache backend: shards are content-addressed by fingerprint, so Put is idempotent and Get restores an evicted shard by the same key. GetShard returns a non-nil reader on a hit, or ErrShardMiss on a miss. Nil = local-only.
type Shard ¶
type Shard struct {
Name string
Nodes []types.KnowledgeNode
Edges []types.KnowledgeEdge
}
Shard is a named, independently-fingerprinted slice of the graph: one per project (its magusfile-derived nodes) plus the singleton registry shard. Shards are authoritative on disk; the merged graph is assembled in memory.
func AssembleShards ¶
AssembleShards builds every shard from the gathered inputs: the registry shard plus one per project in the graph. Order is registry first, then projects in their TargetGraph order.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists and loads knowledge shards. It is the cache-first backing: a query loads shards, fingerprint-checks them, rebuilds only what is stale.
func NewStore ¶
func NewStore(cacheDir string, immutable bool, maxBytes int64, remote RemoteShards, log *slog.Logger) *Store
NewStore returns a store rooted at <cacheDir>/knowledge. immutable is set when cache.write.enabled is false (Sync writes nothing, warns if stale). maxBytes soft-caps the shards dir (0 = unlimited); remote optionally backs shards (nil = local).
func (*Store) Load ¶
Load reads the persisted graph from disk without any assembly. Returns ErrNoStore when the store has never been written. Used for the cache-only fast path (a warm store answers without touching workspace sources).
func (*Store) MergeSymbolShards ¶
MergeSymbolShards merges every persisted @symbols shard into g in place, restoring an LRU-evicted shard from the remote by fingerprint if needed. It is the on-demand half of lazy symbol loading: the default graph (Sync/Load) omits symbol shards for scale, and a symbol-seeded query calls this to pull them in. Best-effort by design: no store yet is not an error (a workspace that never ingested symbols just finds nothing), but a present-but-unreadable shard is surfaced.
func (*Store) MergeSymbolShardsByID ¶
MergeSymbolShardsByID merges only the @symbols shards that mention the given symbol IDs into g, for a scale-safe reverse lookup (`magus refs S` on an exact ID loads a handful of shards, not all). It falls back to a FULL symbol load - never an under-load - whenever the routing index cannot be trusted to be both fresh and helpful: absent, corrupt, stale (its ShardsKey no longer matches the manifest), or yielding no shards for these ids (a fuzzy symbol:-prefixed ref, or an unknown id).
func (*Store) Sync ¶
func (s *Store) Sync(ctx context.Context, shards []Shard, fps map[string]string, refresh bool) (*Graph, error)
Sync reconciles freshly-assembled shards against the persisted store and returns the merged in-memory graph. It writes only shards whose fingerprint changed, prunes shards no longer present (free deletion/rename reconciliation), and rewrites the manifest. In immutable mode it writes nothing but still returns the merged graph, warning once if the persisted store is stale. refresh forces every shard to be treated as stale (a full rebuild).
type SymbolFacts ¶
type SymbolFacts struct {
ID string
Label string
RefCount int
FileCount int
Coverage *CoverageFacts
}
SymbolFacts is one symbol defined in a changed file: its identity, how many references and distinct referencing files the symbol index recorded for it (the caller spread), and its own observed coverage when a profile is loaded.
type SymbolSpan ¶ added in v0.4.0
SymbolSpan is one symbol's identity and the lines it occupies in its defining file.
EndLine is 0 when the indexer emitted no enclosing range, which several do not. That is the honest answer rather than a guessed extent, and a caller expanding a hunk to "the whole symbol" has to decide what to do with a start and no end rather than being handed a fabricated one.