knowledge

package
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: GPL-3.0 Imports: 44 Imported by: 0

Documentation

Index

Constants

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
const (
	ScopeShared  = string(notes.ScopeShared)
	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.

View Source
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.

View Source
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.

View Source
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.

View Source
const DefaultBudget = 50

DefaultBudget bounds the neighborhood a query collects, so a match on a high-degree node cannot pull in the whole graph.

View Source
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.

View Source
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

View Source
var ErrNoStore = errors.New("knowledge: no persisted graph")

ErrNoStore reports that the knowledge store has never been written (no manifest).

View Source
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

func AnchorNodeID(kind, target, scope string) string

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

func CouldMatchLazyLayer(input string) bool

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

func IsHistoryAttr(key string) bool

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

func IsRuntimeAttr(key string) bool

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

func ProjectPaths(cacheDir string) []string

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

func SeedsLazyLayer(input string) bool

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.

func StoreDir

func StoreDir(cacheDir string) string

StoreDir returns the knowledge-store directory for a resolved cache dir.

func UnionInto

func UnionInto(dst, src *Graph)

UnionInto merges src's nodes and edges into dst.

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

type CoverageBlock struct {
	StartLine int
	EndLine   int
	NumStmt   int
	Hits      int
}

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

type CoverageFacts struct {
	Ratio   float64
	Covered int
	Total   int
}

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

func Qualified(g *Graph, workspace string) *Graph

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

func (g *Graph) Dependents(id string) []string

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

func (g *Graph) FileFacts(relPath string) 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

func (g *Graph) Fingerprint() string

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

func (g *Graph) HasSymbols() bool

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

func (g *Graph) NearestNode(input string) string

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

func (g *Graph) NearestSymbol(ref string) string

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

func (g *Graph) Neighborhood(seeds []string, budget int, relations []string) *Graph

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

func (g *Graph) SetRoot(root string)

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

func (r NoteResolver) DeclDigest(_ context.Context, a notes.Anchor) (string, error)

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

func (r NoteResolver) Digest(_ context.Context, a notes.Anchor) (string, error)

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.

func (NoteResolver) Resolves added in v0.4.0

func (r NoteResolver) Resolves(_ context.Context, a notes.Anchor) bool

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

func AssembleShards(in Inputs) []Shard

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

func (s *Store) Load(ctx context.Context) (*Graph, error)

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

func (s *Store) MergeSymbolShards(ctx context.Context, g *Graph) error

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

func (s *Store) MergeSymbolShardsByID(ctx context.Context, g *Graph, symbolIDs []string) error

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

type SymbolSpan struct {
	ID        string
	Label     string
	StartLine int
	EndLine   int
}

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.

Jump to

Keyboard shortcuts

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