types

package
v0.4.0 Latest Latest
Warning

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

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

Documentation

Overview

Package types holds magus's pure domain types. No filesystem, VCS, or process-execution dependencies.

Index

Constants

View Source
const (
	// DiffRoleOutput is a declared target output. It is GENERATED, so reviewing its diff is
	// reading a machine's opinion of a change made elsewhere - the source edit is the review.
	DiffRoleOutput = "output"
	// DiffRoleSource is a declared source: it feeds cache keys and the affected set.
	DiffRoleSource = "source"
	// DiffRoleMaintained is written by magus itself outside any target.
	DiffRoleMaintained = "maintained"
	// DiffRoleUnclaimed is declared by nothing and invalidates nothing.
	DiffRoleUnclaimed = "unclaimed"
)

DiffRole repeats FileEntry.Role rather than importing its meaning, because the review's use of it is narrower: the only question a reader is asking is whether this file is something they must read or something a target will rewrite.

View Source
const (
	// DiffReadUnknown means no receipt store was readable, so the question was not asked.
	// It is the zero value, and it must never render as unread: "nobody has read this" and
	// "nobody checked whether anyone read this" are opposite claims and only one accuses.
	DiffReadUnknown = ""
	// DiffReadUnread means the store was read and holds no receipt for this file.
	DiffReadUnread = "unread"
	// DiffReadRead means a receipt covers this file at exactly its current content.
	DiffReadRead = "read"
	// DiffReadStale means a receipt exists for this file at DIFFERENT content: it was read,
	// then edited. Distinct from unread because it is the more dangerous shape - somebody
	// did look, which is exactly why nobody will look again.
	DiffReadStale = "stale"
)

The DiffRead constants say whether anybody recorded reading a file AT the content it holds now. Untyped strings, matching the older DiffRole and DiffSurface constants beside them rather than the newer types.Evidence: DiffFile.ReadState is one more string field on a struct whose peers are all strings, and typing this one alone would make the odd one out.

It is the one fact in a review that no analysis can supply. Every other annotation here describes what a change DOES; this describes whether a person weighed it, which is the property that decays when work is produced faster than it is read.

Never inferred. magus could watch an editor and call an open file read, and a measure satisfied by scrolling is worse than none because it launders skimming into review. A receipt exists only where somebody typed `magus diff --ack`.

View Source
const (
	// DiffSurfaceInternal means every referent lives inside the defining project. A change
	// here cannot break a consumer that the workspace does not also rebuild.
	DiffSurfaceInternal = "internal"
	// DiffSurfacePublic means at least one referent lives in ANOTHER project: the symbol is
	// API surface across a boundary the workspace itself draws.
	DiffSurfacePublic = "public"
	// DiffSurfaceUnknown means no symbol index covered the file, so the question was not
	// answered. It must never render as "internal" - "we did not look" and "we looked and
	// found nothing" are different facts, and collapsing them is how a signal earns the right
	// to be ignored.
	DiffSurfaceUnknown = "unknown"
)

DiffSurface is how far a changed symbol's referents reach, which is the question a semver decision actually turns on. It is EVIDENCE, never a verdict: magus reports where a symbol is used and lets the reader decide the bump.

It deliberately stops short of claiming a break. Deciding that needs signature compatibility, which needs a base-side index magus does not have (the symbol shards describe the working tree, not history) and language semantics magus does not model. A tool that guessed at it would be wrong in exactly the cases that matter most - an unchanged signature with changed behavior, a widened parameter type - and a breaking-change warning nobody trusts is worse than none. The per-language answer belongs in a spell op (an apidiff), whose output joins this same review.

View Source
const (
	HotspotDefinition = "Hotspots are files (or projects) where edit frequency meets " +
		"complexity - the prime refactoring targets: code both churned often and hard to understand."
	AffinityDefinition = "Affinity is how often projects change in the same commit (temporal " +
		"coupling). A hidden pair has affinity without either declaring a dependency on the other " +
		"- a candidate architectural smell."
	OwnershipDefinition = "Ownership shows author concentration: who touches each project most, " +
		"how many distinct authors it has (bus factor), and whether it has gone quiet (abandonment risk)."
	TrendDefinition = "Trend compares the recent and earlier halves of the window: a positive " +
		"delta is a rising hotspot (accelerating activity), a negative one is cooling."
	VolatilityDefinition = "Volatility reads run-outcome history, not git: each (project, target) " +
		"pair's recent pass/fail/volatile record scored by its Wilson lower bound. A pair at or above " +
		"the configured threshold is flagged volatile - a flakiness signal, the prime stabilization targets."
	UnreferencedDefinition = "Unreferenced lists code symbols the workspace defines and nothing " +
		"in it names: no call from another symbol, and no file outside the one defining them. It reads " +
		"the SCIP-backed knowledge graph, not git. This is a measurement, not a verdict - reflection, " +
		"interface dispatch, build tags, generated call sites, and any consumer outside this workspace " +
		"are all invisible to it, so read each entry before deleting anything."
)

Per-lens descriptions, shown by each lens and reused in the combined report.

View Source
const (
	KindProject = "project"
	KindTarget  = "target"
	KindSpell   = "spell"
	KindOp      = "op"
	KindTool    = "tool" // the program an op runs (argv[0]); ops and spells `use` it
	KindCharm   = "charm"
	KindModule  = "module"

	KindMethod     = "method" // a callable bound to a host module (fs.stat) - magus's built-in API surface
	KindDiagnostic = "diagnostic"
	KindDoc        = "doc"        // markdown doc page (phase 4)
	KindDocSection = "docsection" // a heading within a doc page; the graph's retrieval unit for prose
	KindFile       = "file"       // a .buzz source file (phase 4)
	KindDir        = "dir"        // a directory between a project and its files; the containment tree layer
	KindFunction   = "function"   // a callable defined in a .buzz source file (Buzz-authored)
	KindImport     = "import"     // an unresolvable buzz import literal (phase 4)
	KindRationale  = "rationale"  // a NOTE/WHY/HACK/TODO comment (phase 4)
	KindOwner      = "owner"      // a CODEOWNERS owner (@user, @org/team, email)
	KindSymbol     = "symbol"     // a definition ingested from a SCIP index (compiled-language source, e.g. Go)
	KindAuthor     = "author"     // a git contributor; `authored` the files they touched (emergent, vs the declared owner)
	// KindNote is the one kind that is INJECTED rather than extracted. Every other kind is a
	// projection of workspace content - a doc from markdown, a rationale from a comment, a
	// symbol from an index, an author from git - so deleting the graph and rebuilding
	// recovers all of them. A note's content originates with a person and no rebuild
	// recovers it, which is also why nothing but a person may write one.
	KindNote = "note" // a human-authored note from the declared notes store

	// KindPackage is a THIRD-PARTY dependency a project declares in its manifest, at
	// the version that manifest resolves to. It is the one node kind whose subject
	// lives outside the workspace, which is the point: a workspace that knows it is on
	// connect v1.20.0 can answer a question about connect against v1.20.0, instead of
	// against whatever the open web happens to rank first.
	//
	// Distinct from KindModule (a magus host module a magusfile calls, e.g. fs) and
	// from KindSymbol (a definition, which for an external package comes from a SCIP
	// index rather than a manifest). A package and the symbols it defines are the same
	// dependency seen from two sides: the manifest says what is INSTALLED, the SCIP
	// monikers say what is actually IMPORTED, and neither answers the other's question.
	KindPackage = "package"
)

Knowledge node kinds. The universe is the magus domain, not general source code. Values are stable wire strings and the <kind> segment of a node ID.

View Source
const (
	RelationDependsOn    = "depends_on"    // project->project, target->target, project->package
	RelationContains     = "contains"      // project->target, spell->op, project->file/doc
	RelationUses         = "uses"          // target->op
	RelationReferences   = "references"    // charm->target/project; reused for file->symbol (SCIP)
	RelationDocuments    = "documents"     // doc->spell/diagnostic/module (phase 4)
	RelationCalls        = "calls"         // function->function (buzz); symbol->symbol, from a SCIP index (v8)
	RelationImports      = "imports"       // file->file / file->import (phase 4)
	RelationRationaleFor = "rationale_for" // rationale->function (phase 4)
	RelationEmits        = "emits"         // target->diagnostic, runtime (phase 8)
	RelationOwns         = "owns"          // owner->project/file, from CODEOWNERS
	RelationDefines      = "defines"       // file->symbol, from a SCIP index
	RelationProduces     = "produces"      // target->file/doc, from magus.outputs (v5)
	RelationConsumes     = "consumes"      // target->file/doc, from magus.inputs (v5)
	RelationAuthored     = "authored"      // author->file, from git history (v6)
	// RelationAnnotates completes the trio for the three ways knowledge attaches to code, kept
	// distinct because their provenance differs: documents is doc->entity, rationale_for is
	// an in-code marker->function, and annotates is a human note->entity. A note attaches to
	// an ENTITY and never to a position inside one, which is what lets its breakage be
	// reported rather than silently drifting.
	RelationAnnotates = "annotates" // note->symbol/file/project/target/note, from the notes store
)

Knowledge edge relations. Values are stable wire strings.

View Source
const (
	ConfidenceExtracted = "extracted"
	ConfidenceInferred  = "inferred"
)

Edge confidence. Extracted edges are read directly off a parsed source (score 1.0); inferred edges come from a documented rubric (fuzzy doc mentions, etc.) and carry a sub-1.0 score. Phase 1 emits only extracted edges.

View Source
const (
	KnowledgeQueryDefinition = "query resolves search terms to knowledge-graph " +
		"nodes and returns the ranked matches plus the surrounding neighborhood " +
		"(the induced subgraph, collected up to a node budget)."
	KnowledgeExplainDefinition = "explain shows one node's context: its data, its " +
		"incoming and outgoing edges with provenance, and how many nodes reach it."
	KnowledgePathDefinition = "path connects two nodes: the shortest chain of edges " +
		"between them (edges walked in either direction), with each hop's relation."
)

Retrieval-subcommand definitions (query/explain/path). These complement describe ("what is declared"): explain answers "how is it connected".

View Source
const (
	// DigestAbsent is a path with nothing on disk when it was released: a file the
	// lease deleted, or a declared glob, which is a pattern rather than a path.
	DigestAbsent = "absent"
	// DigestDir is a directory. A tree has no single content digest, and hashing one
	// on every put would walk it, so the next agent is told to go look instead.
	DigestDir = "dir"
	// DigestUnreadable is a path that IS there and could not be hashed: unreadable,
	// not a regular file, or too large to hash under the store's lock. Distinct from
	// DigestAbsent because "the releaser deleted it" and "something is there nobody
	// could read" send the next agent to different places.
	DigestUnreadable = "unreadable"
)

Digests that are not a content hash. A digest is `sha256:<hex>` of the file's bytes when the path held one; these say why it could not be, so a reader is never handed a hash-shaped value that is not a hash. Named for the FIELD they land in (LeaseRelease.Digest) rather than for releases, which they do not classify.

View Source
const AnchorContextLines = 3

AnchorContextLines is how many lines of context each side of the quote carries.

Three, which is the same width unified diff chose for the same reason: enough to disambiguate a repeated line, few enough that the anchor costs a fraction of the hunk it describes.

View Source
const CharmCD = "cd"

CharmCD is a reserved built-in charm: the opt-in continuous-delivery toggle a target's body reads via has_charm to publish its artifact (push an image, upload an archive). It pairs with the ci target — magus run ci:cd. Reserved so it is recognized everywhere and the typo guard skips it (see undeclaredCharms). It is the additive opposite of write — it adds a deliver side effect rather than flipping check→mutate — and the ci gate does not strip it, so a ci run can still deliver.

View Source
const CharmDefinition = "A charm is a named, shared execution modifier applied as an " +
	"RFC 6902 JSON Patch over a target's argv: it changes how a target runs (rw, gha), " +
	"never which target or project runs. See docs/charms.md."

CharmDefinition is the human-readable description of a charm shown by "magus describe charms".

View Source
const CharmGHA = "gha"

CharmGHA is a reserved built-in charm: opt into GitHub Actions output. Spells that drive a tool with a GitHub-annotation output mode declare it in their charms table to swap the tool's reporter to that format (so failures surface as inline `::error::` workflow annotations on the PR). Set it in CI via `magus run ci:gha`. Reserved so the typo guard skips it everywhere (a ci run fans out to tools that don't support it, where it is simply a no-op — see undeclaredCharms); the ci gate does not strip it (unlike rw), so the annotations survive into ci.

View Source
const CharmReadWrite = "rw"

CharmReadWrite is a reserved built-in charm: the read→write toggle that flips check-only targets (format, lint, generate) to mutate in place. Magusfiles read it via has_charm("rw"); its per-tool effect is declared by each spell's charms table. The name is reserved so it is recognized everywhere — the typo guard skips it (see undeclaredCharms) and the read-only ci gate strips it (see RunCI).

View Source
const CharmRelock = "relock"

CharmRelock is a reserved built-in charm: the grant to rewrite dependency state (a lockfile, or go.mod/go.sum) rather than verify it. Deliberately not part of rw - rw regenerates derived output from this tree and so is reproducible, while a dependency refresh reads a registry and yields different bytes on different days. Folded into rw, a workspace with default_charms: [rw] would re-resolve dependencies during an unrelated build. Stripped from ci alongside rw (see RunCI).

View Source
const ContextParamAnnotation = `magus\Context`

ContextParamAnnotation is the exact parameter type annotation that marks a target: an exported magusfile function is a target if and only if its FIRST parameter carries this annotation. Buzz namespaces a qualified type with a backslash (`serialize\Boxed`), not a dot, so the context type is spelled `magus\Context`; the dotted `magus.Context` is not valid Buzz type syntax (the parser stops at the dot). Recognition keys on this raw annotation string, independent of whether the checker can resolve the type (it treats an unknown qualified name permissively). A ctx-less exported function is rejected at load (MGS1008).

View Source
const CrossFileMember = "file"

CrossFileMember is the reserved member on a project-import handle (`<alias>.file("rel")`) that resolves a cross-project file to a workspace-relative path. The static extractor (internal/describe) and the runtime resolver (internal/interp/bindings) MUST agree on this name; this single const is the shared source of truth so the two cannot drift apart.

View Source
const EvaluatedTargetDefinition = "An evaluated target shows the fully-resolved " +
	"dispatch plan for a specific path:target pair: the workspace-rooted source and " +
	"output globs that feed the cache key, the chain of targets it composes in " +
	"invocation order, the spells that will fire (with " +
	"target-specific sources), " +
	"and any behavioral policy (CheckClean, TrackVolatile, Exclusive)."

EvaluatedTargetDefinition is the human-readable description of an evaluated target shown by "magus describe".

View Source
const EventSchemaVersion = 1

EventSchemaVersion is the current envelope version. Bump on breaking changes; additive optional fields do not require a bump.

View Source
const FileDefinition = "Describe file classifies paths against the workspace's declared " +
	"globs: the project that owns each path, whether it is a declared output (generated: " +
	"regenerate it, never hand-edit), a declared source (it feeds cache keys and the " +
	"affected set), or one magus maintains itself outside any target, and which projects " +
	"claim it either way. It answers \"can I disregard this changed file\" from the " +
	"workspace's own declarations, and - over several paths in one call - which single " +
	"declaration covers more than one of them."

FileDefinition is the human-readable description printed by "magus describe file".

View Source
const InsightDefinition = "Insight shows where a codebase's attention and risk concentrate. " +
	"Four lenses read VCS history: hotspots (churn x complexity, the prime refactoring targets), " +
	"affinity (projects that change together, and whether a dependency edge explains it), " +
	"ownership (author concentration and bus factor), and trend (rising vs cooling activity). " +
	"A fifth lens, volatility, reads run-outcome history instead: targets whose pass/fail record " +
	"flaps (a Wilson-scored flakiness signal). A sixth, unreferenced, reads the knowledge graph: " +
	"code symbols nothing else in the workspace names."

InsightDefinition is the umbrella description of the insight lenses.

View Source
const KnowledgeGraphDefinition = "The knowledge graph is a deterministic, " +
	"cache-backed graph of the magus domain: projects, targets, spells, ops, charms, " +
	"modules, methods, and diagnostics, connected by verified relations (depends_on, " +
	"contains, uses, references, documents). Every node and edge is extracted or " +
	"rubric-inferred from workspace sources - no LLM pass - so it is safe to rebuild " +
	"implicitly and query instead of grepping."

KnowledgeGraphDefinition is the human-readable description printed by "magus graph export".

View Source
const KnowledgeGraphDiffDefinition = "graph diff reports how the knowledge graph " +
	"changed between a base revision and the working tree: the nodes and edges added, " +
	"removed, or (for nodes) changed. It is the PR-review blast-radius artifact - what " +
	"a change did to the domain's shape - emitted as json or markdown, never rendered. " +
	"Edge diffs are structural: an edge is identified by (source, target, relation), so " +
	"a re-scored or re-provenanced edge that keeps those three is not reported as changed."

KnowledgeGraphDiffDefinition is the human-readable description of `magus graph diff`.

View Source
const KnowledgeOccurrencesDefinition = "refs --occurrences lists every exact source range " +
	"where a symbol appears, complete and column-precise, with each range verified against " +
	"the file on disk. It is the view a mechanical rewrite needs; the default file:line " +
	"view caps its line list and cannot say which occurrence on a line to replace."

KnowledgeOccurrencesDefinition is the human-readable description of the occurrence view.

View Source
const KnowledgeRefsDefinition = "refs lists where an ingested code symbol is " +
	"defined and every file that references it, as file:line rows drawn from the " +
	"SCIP index. It is the occurrence-shaped view (a flat list) that a symbol's fan-in " +
	"needs, which query's node-link neighborhood renders poorly."

KnowledgeRefsDefinition is the human-readable description of `magus refs`.

View Source
const KnowledgeSchemaVersion = 10

KnowledgeSchemaVersion is stamped into every exported graph, shard, and manifest. External consumers - agent skills, MCP tools, other tools reading the node-link JSON - check it; a bump is a changelog event. Increment when the node/edge shape or ID scheme changes in a way that would break a consumer that parsed the old form. v2 added a "command" kind; v3 a "tool" kind coupled to it. v4 retires "command" (its rendered argv was always identical to the op's static base command, so it was a redundant copy of the op) and moves the model onto the op: an op carries an `argv` attr and `uses` the tool (argv[0]) it runs, so `explain tool:go` reaches every op that runs go and a target reaches its tool via target->op->tool. v2/v3 were unreleased. v5 adds the build I/O layer: `produces`/`consumes` edges from a target's declared magus.outputs/inputs to the file and doc nodes they match, so a generated file is self-labeled by its producing target; plus workspace-wide authored-markdown doc nodes carrying a `role` attr (readme/agent/changelog/...) and a `documents` edge to their project. v6 adds the "author" kind: a git contributor, with `authored` edges to the files they touched (the EMERGENT maintainer, to set against a file's DECLARED CODEOWNERS owner). v7 changes no node or edge shape at all: it bumps because shard fingerprints are now computed by streaming fields into SHA256 instead of hashing marshaled JSON, so every shard's fingerprint VALUE differs from a v6 store's. The manifest check treats a version mismatch as a full rebuild, which is exactly the migration needed - without the bump, a v6 cache would read as current while every fingerprint disagreed, and a changed shard would never be rewritten. v8 adds symbol->symbol `calls` edges to the @symbols shards, attributed from the SCIP occurrence's enclosing_range (the callee is referenced from inside the caller's body). The relation and both node kinds already existed, so a v7 consumer parses a v8 graph without changing - but it would read a symbol's edge set as complete when it is not, and the shard fingerprints all differ, so the bump is what forces the rebuild. v9 adds `secret_refs` to a target node: the credential references the target names, alongside the `reads_secrets` flag that already recorded that it names any. The field is additive, so a v8 consumer parses a v9 graph unchanged - the bump is for the OTHER direction, a v8 store on disk. Its target shards were extracted before the field existed, and the magusfile they were extracted from has not changed, so nothing else would invalidate them: the version mismatch is what forces the rebuild that puts the references there. v10 adds the "docsection" kind: one node per markdown heading, carrying its goldmark auto-heading-id anchor, so an agent retrieves the relevant section of a doc rather than the whole page. A page `contains` its sections and a section `contains` its subsections. The kind is additive, so a v9 consumer parses a v10 graph unchanged - the bump is for a v9 store on disk, whose doc shards were extracted before headings were indexed and whose source markdown has not changed, so only a version mismatch forces the rebuild that adds the sections.

View Source
const KnowledgeStatsDefinition = "Graph stats reads the knowledge graph to show " +
	"where the workspace concentrates and where it is neglected: god nodes (the most " +
	"connected spells, modules, and targets - the structural risk), connectivity (how " +
	"fragmented the graph is - components and isolated nodes the builder never linked), " +
	"orphans (isolated nodes and spells nothing uses), and doc coverage (the share of " +
	"diagnostics, spells, and modules that have a doc). It is the structural companion to insight's " +
	"git-history lenses."

KnowledgeStatsDefinition is the human-readable description of `magus graph stats`.

View Source
const MagusfileSpellName = "magusfile"

MagusfileSpellName is the spell a project's own magusfile is bound as. It is matched by name because that spell is a single global instance standing in for every project's magusfile - see Project.MagusfileTargets for why it cannot answer for one.

View Source
const MaxLeaseIDLen = 128

MaxLeaseIDLen bounds a lease id: long enough for a branch-shaped ledger name, short enough that the id stays a correlation key rather than a payload riding every event line.

View Source
const ModuleDefinition = "A module is a magus standard-library namespace a magusfile imports for " +
	"host capabilities - filesystem, exec, vcs, crypto, http, and more. Import " +
	"each under its bare name (import \"fs\", then fs.glob(...)); magus layers these " +
	"methods onto Buzz's own stdlib. The magus forms are sandbox-aware; some methods " +
	"also exist in Buzz's own stdlib."

ModuleDefinition is the human-readable description shown by "magus describe modules".

View Source
const NotableRankCutoff = 50

NotableRankCutoff is how far down the hotspot ranking is still worth SHOWING.

The rank itself is honest at any depth; rendering it is not. "Hotspot #1278" tells a reader nothing except that a ranking exists, and a field that is usually meaningless is a field they learn to skip - which then costs them the one time it says #3. The data stays whole and the display is selective, the same trade the guard makes by explaining only what it is sure of. It is EXPORTED so a renderer can state the rule rather than leaving a reader to infer it. An unexplained cutoff makes a missing rank ambiguous - outside the top N, or never computed? - and the two have opposite implications for how much of the ranking to trust.

View Source
const ProjectDefinition = "A project is a directory the workspace recognized as a " +
	"unit of work, bound to one or more spells. Projects are " +
	"discovered by the presence of a magusfile (magusfile.buzz, or a magusfiles/ subdirectory), " +
	"or reported by a workspace provider the magusfile wired, " +
	"and are the basic unit of caching, scheduling, and dependency tracking."

ProjectDefinition is the human-readable description of a project shown by "magus describe projects".

View Source
const SpellDefinition = "A spell is a language/runtime adapter that " +
	"teaches magus how to build, test, lint, and format projects of a given type. " +
	"Spells are registered at startup and bound to projects by importing the spell " +
	"and listing it in the spells of magus.project in the magusfile."

SpellDefinition is the human-readable description of a spell shown by "magus describe spells".

View Source
const StreamSchema = 1

StreamSchema is the envelope version, stamped on every line as "schema".

Bump it when a field is renamed or removed, or when an existing field changes meaning. Adding a new StreamEventType, or a new optional field to a body, is additive and does NOT bump it: a subscriber is required to ignore types and fields it does not recognize, which is what lets magus grow the taxonomy without breaking every client.

View Source
const TargetCI = "ci"

TargetCI is the one reserved built-in target: the affected-set anchor that `magus affected ci` and `magus affected --plan` key off. It lives in the magusfile (composed via magus.needs), never in a spell. Compare against it only after normalizing the candidate name (see Normalize).

View Source
const TargetDefinition = "A target is a named unit of work (e.g. build, test, lint) declared as an " +
	"exported function in a project's magusfile, which may compose a spell's ops. " +
	"'ci' is the conventional anchor that the affected set keys off - magus runs it " +
	"read-only but does not hardcode which targets compose it; the magusfile wires " +
	"them together with ctx.needs."

TargetDefinition is the human-readable description of a target shown by "magus describe targets".

View Source
const TargetGraphDefinition = "The target dependency graph is the ctx.needs " +
	"DAG of a project's magusfile: each node is a target (an exported function), each " +
	"edge a dependency it composes. It is extracted statically from the magusfile " +
	"source, so it shows every edge - including both arms of a runtime branch - and " +
	"flags any dependency cycle (which the run path rejects during dispatch)."

TargetGraphDefinition describes "magus describe graph".

View Source
const ToolDefinition = "A tool is a binary a spell drives. magus probes its version on " +
	"every run, because that version keys the cache. A project may also hold it to a " +
	"window - an inclusive min and an exclusive below - intersected with what the " +
	"declaring spell requires."

ToolDefinition is the human-readable description printed by "magus describe tool".

View Source
const WorkspaceDefinition = "A workspace is a magus root directory that owns a set " +
	"of projects, a configuration file, a content-addressed cache, and VCS " +
	"integration. Every magus invocation operates within exactly one workspace, " +
	"identified by walking up from the current directory to the nearest go.mod."

WorkspaceDefinition is the human-readable description of a workspace shown by "magus describe workspaces".

Variables

View Source
var ErrAffectedFallback = errors.New("affected: cannot compute affected set")

ErrAffectedFallback is returned when the VCS cannot compute a definitive changed-files set.

ErrDiag is a sentinel for use with errors.Is on DiagnosticError values.

View Source
var ErrNoCache = errors.New("magus: no cache available")

ErrNoCache is returned by cache operations on a cache-free (Inspect) workspace.

View Source
var ErrNotFound = errors.New("magus: not found")

ErrNotFound is the canonical "miss" sentinel for I/O-backed lookups.

View Source
var ErrSpellNameRequired = errors.New("magus: spell name required")

ErrSpellNameRequired is returned by magus.WithSpell when called with an empty name.

View Source
var ErrSpellNotRegistered = errors.New("magus: spell not registered")

ErrSpellNotRegistered is returned by magus.WithSpell when the named spell is not registered.

View Source
var ErrUnknownProject = errors.New("magus: unknown project")

ErrUnknownProject is returned (wrapped) by WorkspaceRepository.ExpandPath when a caller refers to a project path that does not exist.

View Source
var ErrUnregisteredDep = errors.New("magus: dependency not registered")

ErrUnregisteredDep is returned by (*Workspace).Graph when a declared dependency path has not been registered.

View Source
var ErrVCSUnknown = errors.New("vcs: unknown VCS")

ErrVCSUnknown is returned by the VCS resolver when an explicit VCS name is given but no built-in or registered implementation matches it.

View Source
var ErrVCSUnsupported = errors.New("vcs: operation not supported by this VCS")

ErrVCSUnsupported is returned by operations not supported by a VCSDriver.

View Source
var ProjectOptions = []ProjectOption{
	{Key: "name"},
	{Key: "depends_on"},
	{Key: "outputs"},
	{Key: "sources"},
	{Key: "exclusive"},
	{Key: "spells"},
	{Key: "watch_ignore"},
	{Key: "targets"},
	{Key: "no_language", Since: "0.4.0"},
	{Key: "tools", Since: "0.4.0"},
	{Key: "review_required", Since: "0.5.0"},
}

ProjectOptions is the ONE list of recognized magus.project keys.

One list because there were two: the engine's and a parallel copy in the dry-run host, which had already drifted - the dry copy silently rejected a key the engine accepted, so a magusfile could pass a real run and fail a preview. A shared table in a near-leaf package is the only shape where that cannot recur.

Functions

func AppendInvocationAncestor added in v0.4.0

func AppendInvocationAncestor(ctx context.Context, pid int, id string) context.Context

AppendInvocationAncestor returns ctx with this invocation appended to the ancestry already on it. Called once per invocation, as soon as its id is known.

func CaptureExit

func CaptureExit(ctx context.Context, code int)

CaptureExit stores code on ctx's exit capture if one is present (set by the interpreter). It is a no-op outside a captured run. Called by os.exit and magus.fatal alongside returning/raising an ExitError.

func CauseText added in v0.4.0

func CauseText(err error) string

CauseText is what a failure record carries as its cause: the concise form when the error has one, else the full message. Callers that have already printed the project and target use this so the cause adds information instead of echoing.

func ChainMemoryMB added in v0.4.0

func ChainMemoryMB(p *Project, target string, lookup func(path string) *Project) (mb int, declaredBy string)

ChainMemoryMB is the largest memory declaration anywhere in what a target will actually run: its own, and every target it composes with ctx.needs, transitively. It returns the figure and the target that declared it.

Without the fold a declaration is INERT for the command people run: only `ci` is scheduled as a step, so the `test` it composes reaches neither the limiter nor machine-wide admission. The MAXIMUM, not the sum, because a chain runs in order.

lookup resolves a cross-project step and may return nil, in which case that step contributes nothing rather than a guess. It lives here because admission and doctor must agree on one figure.

func ChainSkipCacheOutputs added in v0.4.0

func ChainSkipCacheOutputs(p *Project, target string, lookup func(path string) *Project) []string

ChainSkipCacheOutputs is the declared output of every skip_cache target a target composes with ctx.needs, transitively, as workspace-rooted globs.

These globs belong in the composing step's cache key. The engine cannot key a skip_cache target's real inputs, since not being keyable is why it opted out, but it can key the artifact the target maintains: with these globs in the parent's sources, an artifact that moved turns the parent's hit into a miss, so the parent re-runs against what the gate below actually produced instead of replaying an entry recorded against different bytes.

func CharmsFromContext

func CharmsFromContext(ctx context.Context) []string

CharmsFromContext returns the active execution charms, or nil if none were set.

func CodeURL added in v0.3.0

func CodeURL(c DiagnosticCode) string

CodeURL returns the documentation URL for an MGS code. (URL resolution is domain-specific, so it is a function on the magus domain rather than a method on the shared Code type.)

func ContextWithGraphObserver

func ContextWithGraphObserver(ctx context.Context, o Observer) context.Context

ContextWithGraphObserver returns a context carrying a request-scoped graph observer. Use this instead of SetGraphObserver when sharing a workspace across goroutines.

func DescribeGaps added in v0.4.0

func DescribeGaps(gaps []KnowledgeSymbolGap) string

DescribeGaps renders a gap list as "libs/api (not-indexed), docs (not-indexed)".

func EmitDiagnostic added in v0.2.0

func EmitDiagnostic(ctx context.Context, ev DiagnosticEvent)

EmitDiagnostic records ev to the sink in ctx, or is a no-op when none is installed (the common CLI path).

func EnsureReturnCapture added in v0.4.0

func EnsureReturnCapture(ctx context.Context) context.Context

EnsureReturnCapture installs a sink only when ctx has none, and is what the engine calls on the way in.

Without it the sink existed only where the CLI happened to install it (`run` and `affected`), so every other entry point - the MCP run tool, `magus x`, the merge driver, the symbol indexer - executed with no sink and the cache snapshotted Value: nil into a durable entry. Warming a target through MCP and then asking `magus run <target> -o json` for its value reported none, because the entry that replayed had never recorded one.

It leaves an existing sink alone: the CLI installs its own so it can read the values back, and a second sink deeper in would collect them where nobody looks.

func FormatDiagnostic

func FormatDiagnostic(c DiagnosticCode, msg string) string

FormatDiagnostic formats a diagnostic message with code and doc URL for slog logging.

func HasCharm

func HasCharm(ctx context.Context, charm string) bool

HasCharm reports whether charm is among the active execution charms. This membership test is how a spell opts into a charm's behavior; charms it does not test for are simply ignored. The query is normalized and the active set is already canonical (WithCharms normalizes on store), so a spell that tests has_charm("noCache") matches a "target:no-cache" suffix regardless of casing or separator.

func HasInvocationAncestor added in v0.4.0

func HasInvocationAncestor(ctx context.Context, pid int, id string) bool

HasInvocationAncestor reports whether the invocation identified by pid and id is this invocation or one of its ancestors.

func InvalidGlobs added in v0.4.0

func InvalidGlobs(globs []string) []string

InvalidGlobs returns the globs doublestar cannot parse, deduplicated and in the order given. It is what lets a caller SAY that a declaration matches nothing before it silently matches nothing for the rest of the run: an unparsable glob declares an input that can never key, and MGS1028 would then advise declaring a path that is already declared - by a pattern that never matches it.

The error is not returned with it because doublestar has only one (ErrBadPattern, with no position), so the pattern itself is the whole of the information.

func InvocationAncestorsFromContext added in v0.4.0

func InvocationAncestorsFromContext(ctx context.Context) []string

InvocationAncestorsFromContext returns the ancestry WithInvocationAncestors stored, oldest first, or nil when none was set. A library caller that never stamps it reads nil, which disables re-entry detection and restores the plain waiting behavior.

The result is a copy: the daemon derives many contexts from one parent, and handing out the stored slice would let an append by one invocation land in another's.

func IsDevMagusVersion added in v0.3.0

func IsDevMagusVersion(version string) bool

IsDevMagusVersion reports whether version is a dev/unstamped build rather than a clean tagged release. The linker default is "unknown" (the dev sentinel); a git-describe dev build past a tag carries a "-g<sha>" suffix (v0.1.0-5-gabc123); a clean release is a bare tag (v0.1.0). Committed generated files are, by the compatibility contract, produced by the pinned release - so a dev build that finds output drift with unchanged inputs is version skew (environmental), not the developer's change.

func IsMagusMaintained added in v0.4.0

func IsMagusMaintained(path string) bool

IsMagusMaintained reports whether path is one magus's own core writes and expects committed, rather than a target output or anything a project declares. The path is workspace-relative and slash-separated, as FileEntry.Path and StagingPlan carry it.

func IsReservedCharm

func IsReservedCharm(name string) bool

IsReservedCharm reports whether name — in any casing or separator form — is one of magus's reserved built-in charms.

func MagusVersionFromContext added in v0.3.0

func MagusVersionFromContext(ctx context.Context) string

MagusVersionFromContext returns the version WithMagusVersion stored, or "" when none was set.

func MatchTargetPatterns added in v0.4.0

func MatchTargetPatterns(names, patterns []string) []string

MatchTargetPatterns selects target names against ctx.glob's pattern list, sorted and deduplicated so a name matched by two patterns runs once and the order never depends on map iteration.

Three pattern forms, and the third is why this function exists at all:

  • "build" suffix shorthand: every name ending in "-build" (^.*-build$). It does NOT match a target named exactly "build".
  • "*-generate" glob: "*" is the only metacharacter, anchored end to end.
  • "!site-generate" negation: subtracts from whatever the other patterns matched.

Negation exists because the alternative was to rename around the matcher. A target that legitimately ends in "-generate" but must stay out of `ctx.needs(ctx.glob("*-generate"))` previously had exactly one remedy - do not name it that - and the reason was invisible at both the glob and the definition.

A negation takes a NAME or a GLOB, never suffix shorthand: "!site-generate" excludes the target actually called site-generate. Making it suffix shorthand instead would have it compile to ^.*-site-generate$ and silently exclude nothing, which is the one outcome a subtraction must never produce. Excluding a whole family is still available, spelled the same way it is included: "!*-generate". The include shorthand is deliberately left alone - widening it to match bare names too would make ctx.glob("generate") match the `generate` target that contains it, turning a convenience into self-recursion.

Exclusions apply to the union of the includes regardless of order, so ("*-generate", "!site-generate") and ("!site-generate", "*-generate") are the same set. Patterns that are ONLY negations select nothing: subtracting from an empty set is empty, not "everything else" - a glob that silently grew to the whole workspace because its one positive pattern was deleted is a worse failure than matching nothing.

One implementation, deliberately. The runtime binding, the dry-run tracer, and the static describe extractor each carried their own copy of the compile step, each commented as mirroring the others - three places to update in lockstep for a matcher whose whole job is that the traced, described, and executed edge sets agree.

func MatchesAnyGlob added in v0.4.0

func MatchesAnyGlob(globs []string, path string) bool

MatchesAnyGlob reports whether a workspace-relative path matches any of the workspace-rooted globs - the question every consumer of Project.DeclaredGlobs asks, so it lives beside the rooting rather than once per caller. The three callers (affected attribution, doctor's standing check, `magus describe file`) would otherwise be three places for the matcher family to drift from the cache's.

It TOLERATES an unparsable pattern, which then matches nothing - the same thing the cache walk does with one. Tolerance is the right default (a bad glob must not fail a build that never depended on it) but it is silent, so the pattern is worth reporting where the glob set is assembled: see InvalidGlobs.

func Normalize added in v0.4.0

func Normalize(name string) string

Normalize canonicalizes any magus entity name - a target, a charm, a spell, a spell op - to kebab-case, so go_build, goBuild and go-build all name the same thing. Applied at BOTH registration and lookup; a name normalized on only one side is a silent miss, not an error.

One function rather than the TargetNameNormalizer interface it replaces. That interface had a single implementation, and its injection seam (magus.WithTargetNameNormalizer) had zero callers anywhere in the tree - including tests - so `run.Normalizer` was always nil and the seam only ever installed this same kebab-casing. Meanwhile sixteen call sites skipped the interface and reached for the package-level default directly, which is what an injected normalizer would have had to fight. The indirection bought nothing and hid that spell and op names were not going through it at all.

func NormalizeExitCode added in v0.4.0

func NormalizeExitCode(code int) int

NormalizeExitCode maps a requested code onto the range a process exit status can actually carry. A wait status holds 8 bits and os.Exit truncates silently, so os.exit(256) reported SUCCESS and `magus run deploy && ship` shipped. A nonzero request that truncates to zero becomes 1 instead: the intent was to fail, and the only thing worse than the wrong nonzero code is a zero. Negatives fold the same way a shell folds them, so exit(-1) is 255.

func PeakRSS added in v0.4.0

func PeakRSS(ctx context.Context) int64

PeakRSS returns the highest peak reported under ctx, or 0 when nothing was collected. RecordPeakRSS drops non-positive figures, so 0 already means "no process reported one" and a second return value would distinguish nothing.

func ProjectDisplayName added in v0.4.0

func ProjectDisplayName(path, name, dir string) string

ProjectDisplayName returns an explicit display name when available, otherwise derives the shared never-dot label from the project path and directory. Used where a project carries a declared human name (a name: annotation in magusfile) that should win over the path-derived default.

func ProjectLabel

func ProjectLabel(path, dir string) string

ProjectLabel is a convenience for the Display form when the caller only has a path and a dir, not a ProjectRef value. Routes through the struct so the two forms share one definition; add new rendering rules to Display, and ProjectLabel picks them up automatically.

func ProjectOptionKeys added in v0.4.0

func ProjectOptionKeys() []string

ProjectOptionKeys returns just the key names, for the unknown-key rejection both the engine and the dry-run host perform.

func ProjectOptionSince added in v0.4.0

func ProjectOptionSince(key string) (string, bool)

ProjectOptionSince returns the release that first understood key, and whether the key is recognized at all. An empty version for a recognized key means it predates floors.

func RecordPeakRSS added in v0.4.0

func RecordPeakRSS(ctx context.Context, bytes int64)

RecordPeakRSS reports one process's peak resident memory in bytes. Calls with a non-positive value are ignored: the platforms that cannot report this (windows, wasm) and a process that never started both yield zero, and zero means UNKNOWN rather than "used nothing" - a planner that averaged it in would read an unmeasurable target as a free one.

func RecordReturn added in v0.4.0

func RecordReturn(ctx context.Context, project, target string, v any)

RecordReturn records what project's target invocation returned, if a sink is present. It is a no-op outside a captured run.

A nil value is dropped rather than stored, and that is load-bearing twice over. It keeps a `> void` target - nearly every target that exists - from making "returned null" indistinguishable from "returned nothing at all", which is the difference between a field being absent and being present-but-null in the rendered result. It also lets the several spells serving one target fan in: the spell that returned something wins over the ones that returned nothing, whatever order they finish in. Do NOT "fix" this into clearing the key - that would have the last void spell erase the value a sibling produced. Staleness across DIFFERENT targets is prevented by the key, not by clearing.

func ReservedCharmDoc added in v0.2.0

func ReservedCharmDoc(name string) string

ReservedCharmDoc returns a one-line description of a reserved built-in charm, or "" for a name that is not reserved. It is the single source `magus describe charm` reads, so the built-in summaries cannot drift from the reserved set.

func ReservedCharms

func ReservedCharms() []string

ReservedCharms returns magus's built-in charm names as a fresh slice.

func ReturnFor added in v0.4.0

func ReturnFor(ctx context.Context, project, target string) (any, bool)

ReturnFor reads back what project's target invocation recorded, for the cache to store alongside the entry it is snapshotting.

The cache needs this because a HIT never invokes the target: without the value on the entry, a target would print its result on the first run and nothing on the second, which is worse than not returning values at all. Store on snapshot, re-capture on replay, and the two runs agree.

func RootGlob added in v0.4.0

func RootGlob(projectPath, glob string) string

RootGlob roots a glob declared against projectPath at the WORKSPACE, which is the frame every consumer of a declaration matches in: the cache walks from the workspace root and yields workspace-relative paths, and DeclaredGlobs and `magus describe file` compare against the same.

It CLEANS the join rather than concatenating, and that is the whole point. A project-wide source glob may legitimately reach out of its own tree ("../proto/**" declared by docs/) - reaching across a boundary is what the affordance is FOR - and plain concatenation leaves "docs/../proto/**", which matches nothing, because ".." is an ordinary path segment to doublestar and no walked path ever contains one. That is a declaration that keys nothing and attributes nothing while reading as supported: an input that never invalidates. Cleaning resolves it to "proto/**", the spelling the walk actually produces.

A glob reaching PAST the workspace root is rejected where it is declared (workspace.WithSources), not here: this is a pure path operation with one answer, and only the declaration site can name the option that wrote it.

func SourceProjects added in v0.4.0

func SourceProjects(files []FileEntry) map[string]bool

SourceProjects returns every project whose declared SOURCE glob claims one of these paths. It is how both halves of the explained/unexplained question are computed - the dirty set and the changed-since-base set - so the two cannot answer it differently.

func SourcesChangedSinceBase added in v0.4.0

func SourcesChangedSinceBase(ctx context.Context, insp Inspector, res VCSResolution, root string) map[string]bool

SourcesChangedSinceBase returns every project whose declared SOURCE changed in THIS CHANGE - the branch measured against its base ref, not the working tree.

Base-relative is the whole point, and the working tree cannot substitute for it: on a CI runner the checkout is clean, so a dirtiness probe reports nothing and would report every author innocent of everything. A source change that is already COMMITTED explains its output exactly as well as an uncommitted one.

nil means COULD NOT TELL - no VCS, no base, an unreadable diff - and is not the same answer as "nothing changed". A caller deciding whether to blame someone must treat it as no evidence rather than as evidence of innocence.

One function for the three callers that ask this (the engine's drift gate, `magus vcs add`, and doctor's generated-drift check), because they were three copies of it and a staging decision that disagreed with a gate would be invisible until it mattered.

func SplitExplainedOutputs added in v0.4.0

func SplitExplainedOutputs(files []FileEntry, alsoChangedIn map[string]bool) (explained, unexplained []string)

SplitExplainedOutputs divides the dirty declared outputs into the ones this change accounts for and the ones it does not.

It backs both `magus vcs add` and doctor's generated-drift check. Classifying by declared GLOB alone answers "is this path generated" and nothing else, so `vcs add` staged whatever bytes sat at an output path while claiming to stage "the generated outputs a source change produced" - a causal claim it never checked.

An output is EXPLAINED when some project whose output glob claims it also has a dirty declared SOURCE in this change. That is MGS4006's shape. An output with no dirty input behind it is the other branch: a different magus version produced the committed form (MGS4005), or a generator is not deterministic (MGS4003).

ROLE, not the raw glob sets, decides what counts as a dirty input: a committed generated file frequently also matches its project's source globs, and letting that count would make every such file explain itself. FileEntry.Role reports "output" whenever both match, for exactly this reason.

func StampedSchemaVersion added in v0.4.0

func StampedSchemaVersion(b []byte) (int, bool)

StampedSchemaVersion reports the knowledge-schema version stamped into generated output, and whether one was found. The first stamp wins, so a document quoting a version later in its prose cannot move the answer.

It reads a stamp rather than trusting a filename because the question a caller asks is what BUILT these bytes. A binary that regenerates output stamped newer than KnowledgeSchemaVersion is downgrading it, which is silent data loss when the generator runs as a composed target where no drift gate compares the result.

func Tracing

func Tracing(ctx context.Context) bool

Tracing reports whether ctx is a dry-run tracing context (see WithTrace).

func ValidDriftPolicy added in v0.4.0

func ValidDriftPolicy(s DriftPolicy) bool

ValidDriftPolicy reports whether s is a policy magus knows. A typo must not read as the default: "of" or "warm" would silently gate or silently not, and the author would find out from a merge rather than from the load.

func ValidLeaseID added in v0.4.0

func ValidLeaseID(id string) bool

ValidLeaseID reports whether id may be stamped as a lease: letters, digits and the separators -_./: a ledger row or a branch-shaped lease name uses, never empty, at most MaxLeaseIDLen characters.

The narrowness is a security property, not a naming preference. A lease id is EXEMPT from the redaction internal/trail applies to every other event field, so every channel that can stamp one - a lease marker, the BAGGAGE environment channel, a producer's own field - has to pass its candidate through here first, or the exemption becomes a way to carry a credential onto an event line.

It lives beside Lease rather than in the package that redacts, because the ledger and the trail are two readers of one id: a validator owned by either would leave the other free to accept an id the first would refuse.

func ValidateCharmName

func ValidateCharmName(name string) error

ValidateCharmName reports whether name is a well-formed charm name. Charms share the target-name charset (letters, digits, '-' and '_').

func ValidateTargetName

func ValidateTargetName(name string) error

ValidateTargetName reports whether name is a well-formed target name. Allowed characters are letters, digits, '-' and '_'; a non-nil error describes the violation.

func WithActiveDispatch

func WithActiveDispatch(ctx context.Context, a *ActiveDispatch) context.Context

WithActiveDispatch installs a shared ActiveDispatch for one run.

func WithCharms

func WithCharms(ctx context.Context, charms []string) context.Context

WithCharms returns a context carrying the active execution charms, normalized (see NormalizeCharmName) so the stored set is canonical and HasCharm only has to normalize the query. An empty set leaves the context unchanged, so it never clobbers existing charms. Callers pass the full accumulated set (e.g. the charms in a "name:a,b" suffix).

func WithDiagnosticSink added in v0.2.0

func WithDiagnosticSink(ctx context.Context, s DiagnosticSink) context.Context

WithDiagnosticSink returns ctx carrying s, so a deep emission site can reach the sink without threading it through every signature.

func WithExitCapture

func WithExitCapture(ctx context.Context) (context.Context, func() (int, bool))

WithExitCapture returns a context that captures an exit code requested via CaptureExit during execution, plus a reader for it. The interpreter wraps each target run with this so an os.exit / magus.fatal code survives even when a VM stringifies the ExitError on the way out: an engine that raises host errors as plain strings drops the Go type, so reading the code out-of-band here is what makes os.exit's code engine-independent.

func WithInvocationAncestors added in v0.4.0

func WithInvocationAncestors(ctx context.Context, refs []string) context.Context

WithInvocationAncestors carries the invocations this one is running underneath, oldest first, with this invocation itself last. Each entry is an opaque reference minted by AppendInvocationAncestor; a caller only ever compares them.

It exists so a nested magus can tell a lock its OWN ancestor holds - which can never be released, because the ancestor is blocked waiting on this process to exit - from a lock an unrelated concurrent magus holds, which will be released shortly. The two look identical to flock, and treating the first as the second is what made a nested `magus run` hang forever instead of failing.

It is a context value rather than package state because the daemon runs many invocations concurrently in ONE process: a global would report the union of every in-flight run's ancestry and refuse work that is merely concurrent.

func WithMagusVersion added in v0.3.0

func WithMagusVersion(ctx context.Context, version string) context.Context

WithMagusVersion carries the running magus binary's display version (main.version, linker-injected) on ctx so a host method that needs the release-vs-dev distinction - the drift classifier - can read it without importing package main. The CLI stamps it once on the root context at startup; a bare library caller that never stamps it reads "" (treated as a dev build, the conservative default).

func WithPeakRSS added in v0.4.0

func WithPeakRSS(ctx context.Context) context.Context

WithPeakRSS returns a context that collects the peak resident memory of every process executed under it. Install it once per unit of work you want a figure for; nested installs are independent, so an inner one does not feed its outer.

func WithReturnCapture added in v0.4.0

func WithReturnCapture(ctx context.Context) (context.Context, func(target string) Returns)

WithReturnCapture installs a sink for target return values and returns a reader that narrows the collected set to one target.

This mirrors WithExitCapture, with one difference that matters: a run fans out across projects concurrently, so the sink is mutex-guarded and the reader hands back a copy. The exit capture needs neither, because it is scoped to a single target invocation on one goroutine.

Callers that only need the sink installed - every engine entry point, so that the cache snapshots a value regardless of who dispatched the run - want EnsureReturnCapture instead.

func WithTrace

func WithTrace(ctx context.Context) context.Context

WithTrace marks ctx as a dry-run tracing context. Effectful host operations (subprocess exec, filesystem writes, network requests, environment mutation) detect it via Tracing and trace their intent then return a benign result rather than performing the side effect, so a dry run never touches the system. Reads are left alone, so a dry run can still inspect the workspace to compute its plan.

Caveat: because the magusfile body still evaluates, a target that branches on a command's output or a network response sees a stubbed (empty) result, so the traced plan can diverge from a real run. That is inherent to dry-run-by-evaluation.

func WithWorkspace

func WithWorkspace(ctx context.Context, ws WorkspaceRepository) context.Context

WithWorkspace returns a context carrying ws for downstream code (e.g. audit). The workspace rides on the context rather than a parameter because it must cross the spell-invocation boundary (spells.Driver.Invoke takes only ctx + InvokeRequest), reaching host bindings the run engine cannot thread it to directly. WorkspaceFromContext callers must handle a nil result.

func WorkspaceRef deprecated added in v0.4.0

func WorkspaceRef(path string) string

WorkspaceRef is a convenience for the WorkspaceURI form when the caller only has a path. The dir is intentionally absent: a URI is path-only, and Display's dir-based root naming has no place in the machine-readable form.

Deprecated: the workspace:// spelling is retired; render the bare workspace-relative path instead (Display or ProjectLabel). Magus no longer emits this form itself.

Types

type ActiveDispatch added in v0.4.0

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

ActiveDispatch records which projects are running a target of their OWN during this run, so the descendant-write audit can tell a child's writes from a parent reaching across a boundary.

It is filled as dispatch happens rather than derived from declarations, because the dependency that reaches a descendant is often not on the target being audited: the root's build needs go-build, and go-build is what needs the nested project. Only the live dispatch sees the whole chain. Entries are project dirs, which is what the dispatcher knows and what the audit already carries per descendant.

func ActiveDispatchFromContext

func ActiveDispatchFromContext(ctx context.Context) *ActiveDispatch

ActiveDispatchFromContext returns the run's ActiveDispatch, or nil.

func (*ActiveDispatch) Has added in v0.4.0

func (a *ActiveDispatch) Has(dir string) bool

Has reports whether dir was marked.

func (*ActiveDispatch) Mark added in v0.4.0

func (a *ActiveDispatch) Mark(dir string)

Mark records dir as running its own target. Safe for concurrent use: cross-project dependencies dispatch in parallel.

type AffectedComputer

type AffectedComputer interface {
	Affected(ctx context.Context, base string) (*AffectedResult, error)
	AffectedFromPaths(ctx context.Context, paths []string) (*AffectedResult, error)
}

AffectedComputer computes the VCS-impacted project set.

type AffectedPath

type AffectedPath struct {
	Seed  string   // project that contained a changed file
	Chain []string // [seed, ..., target]
}

AffectedPath is one dependency chain from a seed to a target project.

type AffectedResult

type AffectedResult struct {
	Base        string              // ref used for the diff
	Changed     []string            // repo-relative changed paths
	Seed        []string            // project paths that contain changed files
	FilesBySeed map[string][]string // seed → changed files within it
	Affected    []string            // transitive reverse closure of Seed, sorted
	// UndeclaredBySeed is the subset of FilesBySeed that NO project's declared globs
	// name. Those files reached their seed through directory containment alone (the
	// root catch-all included), so they rerun that project's targets without moving
	// any cache key: the run is real work whose result was already correct.
	//
	// Carried rather than recomputed because every consumer of FilesBySeed asks the
	// same follow-up - `--impact` qualifies its "seeded by N changed files" with it,
	// and MGS1028 reports it - and the declarations it is derived from are not in
	// reach once the result has crossed out of the workspace.
	UndeclaredBySeed map[string][]string
}

AffectedResult is the outcome of a workspace affected-set computation.

func (AffectedResult) BuzzObject added in v0.4.0

func (v AffectedResult) BuzzObject() BuzzObject

type AffinityOutput

type AffinityOutput struct {
	Definition string     `json:"definition" yaml:"definition"`
	Commits    int        `json:"commits"    yaml:"commits"`
	Since      string     `json:"since,omitempty" yaml:"since,omitempty"`
	Pairs      []CoChange `json:"pairs"      yaml:"pairs"`
}

AffinityOutput reports projects that change together (temporal coupling). Hidden pairs are the interesting ones: they co-change but no dependency edge connects them.

func (AffinityOutput) BuzzObject added in v0.4.0

func (v AffinityOutput) BuzzObject() BuzzObject

type ArchiveEntry added in v0.4.0

type ArchiveEntry struct {
	Name  string
	Size  int
	IsDir bool `buzz:"is_dir"`
}

ArchiveEntry mirrors one element of archive.list's result: an entry's name as the archive itself records it, its uncompressed size, and whether it is a directory.

Name is a plain str, not a Path, and that is deliberate: it is a name INSIDE an archive, which resolves against nothing on disk until something extracts it. A Path would invite fs.read_file on a file that is not there. UncompressResult hands back Paths precisely because by then the entries do exist.

Size is the UNCOMPRESSED size, which is what a caller checking "will this fit" needs; a zip's compressed size is an implementation detail of the container and tar has no equivalent field at all, so reporting one would be inconsistent across the formats this module treats alike.

func (ArchiveEntry) BuzzObject added in v0.4.0

func (v ArchiveEntry) BuzzObject() BuzzObject

type Binding

type Binding struct {
	Name string // spell identifier
}

Binding is the per-spell registration state attached to a project. One Binding is created per WithSpell call.

type BisectOptions

type BisectOptions struct {
	Bad        string // commit known bad (default "HEAD")
	Good       string // commit known good; if empty, GoodBefore is used
	GoodBefore time.Time
	// TestCmd is passed to `sh -c` by the bisect runner; it must be operator-trusted.
	TestCmd string
}

BisectOptions configures a VCSDriver.Bisect call.

type BranchChange added in v0.4.0

type BranchChange struct {
	// Ref is the branch as a reader would name it, with any remote-tracking prefix removed:
	// "feat/audience", not "refs/remotes/origin/feat/audience".
	Ref   string   `json:"ref"`
	Paths []string `json:"paths"`
	// Local reports whether Ref is a branch in this repository rather than a remote-tracking copy
	// of somebody else's.
	//
	// It decides what the answer is AS OF, which the two kinds do not share: a local branch is
	// current, and a remote-tracking one is exactly as fresh as the reader's last fetch. A surface
	// that rendered both with one caption would be overstating half of them.
	Local bool `json:"local,omitempty"`
}

BranchChange is one other line of work and the repo-relative paths it changes.

type BranchChangeReporter added in v0.4.0

type BranchChangeReporter interface {
	// BranchChanges returns up to limit branches other than the current one, most recently
	// updated first, each with the paths it changes relative to base.
	//
	// Local branches AND remote-tracking ones. Remote-tracking alone was the shape of the
	// question when the other line of work belonged to a colleague, and it goes blind exactly
	// where agents fan out: worktrees of one repository, on local branches nobody has pushed. A
	// backend that answered about only half the branches that exist would leave the reader an
	// empty list, and an empty list here reads as "nothing competes".
	//
	// It reads what the repository already has rather than fetching: a remote-tracking answer is
	// as fresh as the reader's last fetch, which BranchChange.Local lets a caller say out loud
	// instead of implying the whole answer is live.
	//
	// limit is the backend's to apply, not the caller's to trim afterwards, so a backend can push
	// it down to the ref listing and never materialise a diff it was going to discard.
	BranchChanges(ctx context.Context, dir, base string, limit int) ([]BranchChange, error)
}

BranchChangeReporter is an optional capability for VCSDriver implementations that can report what OTHER branches are changing, so a reader can be told a file in front of them is also being edited elsewhere before the merge conflict tells them.

Callers type-assert for it and degrade gracefully. Degrading here means saying NOTHING rather than "no branch competes": a backend that cannot answer and a repository where nothing overlaps are different facts, and only one of them is reassuring.

type BuildInfo added in v0.2.0

type BuildInfo struct {
	Version string `json:"version" yaml:"version"`
	Commit  string `json:"commit" yaml:"commit"`
	Date    string `json:"date" yaml:"date"`
}

BuildInfo is the running binary's linker-stamped identity (version, commit, date). Distinct from BuildStatus, which reports cache/build activity.

func (BuildInfo) Fingerprint added in v0.2.0

func (b BuildInfo) Fingerprint() string

Fingerprint is the full human identity, matching what `magus --version` prints.

type BuildStats

type BuildStats struct {
	Nodes    int
	Edges    int
	Duration time.Duration
}

BuildStats is emitted once per successful dependency-graph build.

type BuildStatus added in v0.2.0

type BuildStatus struct {
	SelfUpdate bool `json:"selfupdate" yaml:"selfupdate"`
}

BuildStatus reports optional features compiled into the magus binary via build tags. Populated by the caller so the bridge (internal/service/console) does not need to import the build-tag constants from cmd/magus.

type BuzzObject added in v0.4.0

type BuzzObject map[string]any

BuzzObject is the Buzz `object` a host method's return crosses the boundary as: the map a magusfile sees when it annotates a result (`> FileInfo`, `> HttpResponse`, ...). Named in Buzz's OWN vocabulary, not magus-internal jargon - Buzz's type system has ObjectType, and cmd/magus-utils types emits `export object Foo` for each mirror, so a Buzz author never has to translate what they typed into some other word this codebase prefers. Named rather than a bare map[string]any so every signature says which projection it is - this is NOT the JSON shape and deliberately differs from it (camelCase keys, `buzz:"-"` omissions, timestamps as RFC3339 text).

type CacheStatus added in v0.2.0

type CacheStatus struct {
	Immutable bool   `json:"immutable" yaml:"immutable"`
	Dir       string `json:"dir,omitempty" yaml:"dir,omitempty"`
	SizeMB    int    `json:"size_mb,omitempty" yaml:"size_mb,omitempty"`
}

CacheStatus reports the current cache configuration.

type ChainStep added in v0.4.0

type ChainStep struct {
	Project string `json:"project,omitempty" yaml:"project,omitempty"`
	Target  string `json:"target"            yaml:"target"`
}

ChainStep is one target a composed target invokes, in source order. Project is empty for a same-project step (the common case, `ctx.needs(build)`) and carries the other project's path for a cross-project one (`ctx.needs(<alias>.build)`) - the same empty-means-this-project convention InputRef uses, and it stays empty here even after resolution so a reader can tell the two apart at a glance. Deliberately its own type rather than a reused CrossTargetRef: that one names a target in ANOTHER project by definition, and a chain is mostly local steps.

func ChainSkipCacheSteps added in v0.4.0

func ChainSkipCacheSteps(p *Project, target string, lookup func(path string) *Project) []ChainStep

ChainSkipCacheSteps is the skip_cache targets a target composes with ctx.needs that a caller must run itself before replaying it, in invocation order, each carrying its owning project path.

skip_cache says a target always runs. ctx.needs runs a composed target inside the parent's body, which a cache hit never executes, so the policy stopped holding the moment the target was reached through a chain rather than named on the command line. A caller replaying the parent runs these to make it hold again.

The set is narrower than "every composed skip_cache target", and ChainSkipCacheOutputs is what narrows it: a target qualifies only when it maintains an artifact that is already in the parent's key. That is what the parent needs before it can trust a replay, and it leaves out the skip_cache targets that opted out for a reason a replay cannot invalidate. `image-build` pushes a signed digest per invocation, so it composes into `ci` and belongs nowhere near a hit path; eight minutes of docker build measured the difference.

Running a gate runs everything it composes, so a gate the caller can reach from another gate is already covered and is left out. One rule covers both shapes that produces: `generate` composing `index-generate` directly, and root `ci` reaching `generate` through `lint` and again through `security`.

func (ChainStep) BuzzObject added in v0.4.0

func (v ChainStep) BuzzObject() BuzzObject

func (ChainStep) Ref added in v0.4.0

func (s ChainStep) Ref() string

Ref spells the step the way the CLI takes a target ref: "target" for a same-project step, "project:target" for a cross-project one.

type ChangeStatus added in v0.4.0

type ChangeStatus string

ChangeStatus is what a commit did to one path. It exists so churn attribution can tell a rename from a delete-plus-add: without that distinction a file's history splits across every name it ever had, and each fragment ranks as a separate, quieter file than the one thing actually being rewritten.

const (
	ChangeAdded    ChangeStatus = "added"
	ChangeModified ChangeStatus = "modified"
	ChangeDeleted  ChangeStatus = "deleted"
	ChangeRenamed  ChangeStatus = "renamed"
)

type Charm

type Charm struct {
	Name         string             `json:"name"                   yaml:"name"`
	Builtin      bool               `json:"builtin,omitempty"      yaml:"builtin,omitempty"`
	Default      bool               `json:"default,omitempty"      yaml:"default,omitempty"`
	Doc          string             `json:"doc,omitempty"          yaml:"doc,omitempty"`
	Declarations []CharmDeclaration `json:"declarations,omitempty" yaml:"declarations,omitempty"`
}

Charm is one charm in the inverse index: its name, whether it is a reserved built-in or a workspace default, its built-in doc (empty for a spell-defined charm), and every target that declares a patch for it.

type CharmDeclaration added in v0.2.0

type CharmDeclaration struct {
	Project string   `json:"project"          yaml:"project"`
	Target  string   `json:"target"           yaml:"target"`
	Spell   string   `json:"spell"            yaml:"spell"`
	Before  []string `json:"before,omitempty" yaml:"before,omitempty"`
	After   []string `json:"after,omitempty"  yaml:"after,omitempty"`
}

CharmDeclaration is one target's declaration of a charm: the spell that owns the command and the before/after argv the charm's patch produces for that target. Before == After marks a declaration whose patch changes nothing for this target.

type CharmReport added in v0.4.0

type CharmReport struct {
	Definition string  `json:"definition" yaml:"definition"`
	Count      int     `json:"count"      yaml:"count"`
	Charms     []Charm `json:"charms"     yaml:"charms"`
}

CharmReport is the "describe charm[s]" envelope.

type ChurnReporter

type ChurnReporter interface {
	// ChangesByCommit returns up to commits recent non-merge commits, newest
	// first, each reduced to its author, date, and touched repo-relative paths.
	// since, when non-empty, is a backend-native lower bound on the commit date
	// (a git approxidate / RFC3339); commits still caps the result.
	//
	// A backend that cannot detect renames reports them as a delete and an add,
	// which costs lineage but stays correct: PrevPath is simply never set, and
	// FileHotspots then ranks the two names separately rather than wrongly.
	ChangesByCommit(ctx context.Context, dir string, commits int, since string) ([]CommitChange, error)
}

ChurnReporter is an optional capability for VCSDriver implementations that can report which files recent commits touched, so churn (edit frequency) can be attributed to projects. Like MergeDriverInstaller, callers type-assert for it and degrade gracefully (skip the heatmap) when a backend lacks it.

type CoChange

type CoChange struct {
	A      string `json:"a"      yaml:"a"`
	AName  string `json:"a_name" yaml:"a_name"`
	B      string `json:"b"      yaml:"b"`
	BName  string `json:"b_name" yaml:"b_name"`
	Count  int    `json:"count"  yaml:"count"`
	Hidden bool   `json:"hidden,omitempty" yaml:"hidden,omitempty"`
}

CoChange is a pair of projects that changed together, how often, and whether the affinity is "hidden" — i.e. neither project declares a dependency on the other.

func (CoChange) ALabel added in v0.4.0

func (c CoChange) ALabel() string

func (CoChange) BLabel added in v0.4.0

func (c CoChange) BLabel() string

func (CoChange) BuzzObject added in v0.4.0

func (v CoChange) BuzzObject() BuzzObject

type CommentAnchor added in v0.4.0

type CommentAnchor struct {
	// Digest is the hunk's content digest when the remark was written.
	Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
	// Quote is the new-side line the remark sits on, verbatim.
	Quote string `json:"quote,omitempty" yaml:"quote,omitempty"`
	// Before and After are the lines around Quote, up to AnchorContextLines each.
	//
	// They are what make a short quote usable at all. A line of code is often not unique in its
	// own file - a bare closing brace, a `return nil`, a repeated field tag - so a search for the
	// quote alone lands on the first of many. The context is what picks the right one, and it is
	// the same trick the web-annotation model uses under the name prefix/suffix.
	Before []string `json:"before,omitempty" yaml:"before,omitempty"`
	After  []string `json:"after,omitempty"  yaml:"after,omitempty"`
	// Declaration is the enclosing declaration git named in the hunk header: the text after the
	// second @@, which is "func (r Diff) AttachChurn(...)" or "type Diff struct {".
	//
	// GIT'S OWN funcname, not a symbol from the knowledge graph, and the difference is what makes
	// this rung fire at all. A SCIP symbol is a better identifier - it survives a rename of the
	// surrounding file and carries real structure - and it is absent unless somebody has run
	// `magus graph build`, which in this very workspace they have not. An anchor that needs an
	// index nobody built is an anchor that never resolves. This one is in every patch already,
	// in every language git has a funcname pattern for, and costs nothing to keep.
	//
	// It is the WEAKEST of the three and deliberately last: a declaration says which function a
	// remark was about, never which line, so it is what remains when the quote is gone.
	Declaration string `json:"declaration,omitempty" yaml:"declaration,omitempty"`
}

CommentAnchor is what a remark remembers about the code it was written against, so it can be re-found after that code moves.

THREE THINGS, because no single one of them survives every edit, and the mature review tools all converged on storing several and degrading the CLAIM rather than guessing. Digest DETECTS a change and cannot recover from one; Quote RECOVERS a line that moved and cannot tell an unmoved line from a re-typed one. Together they answer both, and Line stops being the answer and becomes a hint - the prior a search starts from rather than the thing it trusts.

The field this replaced was a bare digest, documented as letting a remark "report that the code under it has since changed". No client ever set it and nothing ever read it, so the report it promised was never made.

type CommentAnchorRung added in v0.4.0

type CommentAnchorRung string

CommentAnchorRung is how well a remark still knows where it belongs, worst case named rather than guessed at.

const (
	// AnchorExact is the remembered line, still holding the remembered text.
	AnchorExact CommentAnchorRung = "exact"
	// AnchorMoved is the text found somewhere else in the file. The remark is placed there and
	// says so, because a reader who is not told will read the new position as the original one.
	AnchorMoved CommentAnchorRung = "moved"
	// AnchorDeclaration is the quoted line gone, but the DECLARATION it sat in still present. The
	// remark keeps its path and moves to that declaration's hunk, saying it lost the exact line -
	// which is the rung Gerrit spells "file level", one step narrower.
	AnchorDeclaration CommentAnchorRung = "declaration"
	// AnchorLost is the text gone from the file. The remark keeps its path and loses its line,
	// which is the degradation Gerrit's comment porter makes for the same reason: a remark that
	// lands on the wrong code is worse than one that admits it lost the thread.
	AnchorLost CommentAnchorRung = "lost"
	// AnchorUnknown is a remark carrying no quote to look for - written before anchors were
	// captured, or on a hunk whose text magus never held. Distinct from AnchorExact because
	// "still in place" and "nobody checked" are different facts.
	AnchorUnknown CommentAnchorRung = ""
)

type Commit

type Commit struct {
	// ID is the content/revision identifier: git SHA, hg node, jj commit_id.
	ID    string `buzz:"id"`
	Short string // abbreviated ID
	// Author wrote the change.
	Author Person
	// Date is when the revision was recorded in the repository (git/jj commit
	// date, hg's date): the reproducible "when", distinct from any author date.
	// Zero if the VCS reported no timestamp.
	Date time.Time
	// Subject is the message's first line; Body is the remainder.
	Subject string
	Body    string
	// Parents are parent IDs; more than one for a merge.
	Parents []string
}

Commit is a VCS-agnostic snapshot of one revision. Every field is meaningful for every backend (git, hg, sl, jj); concepts a single VCS lacks (jj's change id, git's author/committer split) are deliberately not modeled here. Reach for vcs.exe() for VCS-specific work.

func (Commit) BuzzObject added in v0.4.0

func (c Commit) BuzzObject() BuzzObject

BuzzObject is the Buzz boundary map vcs.commit / vcs.history entries return: {id, short, author {name, email}, date, subject, body, parents}. date is RFC3339, empty when the VCS reported no timestamp.

type CommitAuthor

type CommitAuthor struct {
	Name  string
	Email string
}

CommitAuthor is the boundary mirror of the {name, email} author object a vcs.commit / vcs.history result carries. The Buzz `object CommitAuthor` mirror is generated from this struct by cmd/magus-utils types; keep them in lockstep.

func (CommitAuthor) BuzzObject added in v0.4.0

func (v CommitAuthor) BuzzObject() BuzzObject

type CommitChange

type CommitChange struct {
	ID     string
	Author string
	Date   time.Time
	Files  []FileChange
}

CommitChange reduces one commit to who made it, when, and the repo-relative paths it touched: the input to churn attribution (no message or diff content).

type CommitRecord

type CommitRecord struct {
	ID      string `buzz:"id"`
	Short   string
	Author  CommitAuthor
	Date    time.Time
	Subject string
	Body    string
	Parents []string
}

CommitRecord is the boundary mirror of the object vcs.commit / vcs.history return: the serializable, every-field-present view of a Commit. A magusfile annotates `> Commit` to get compile-checked field access on a commit object; the runtime value is the matching map (see Commit.BuzzObject), never this struct directly - it exists so cmd/magus-utils types has something to reflect over. Date stays time.Time, same as Commit.Date: buzzType (in cmd/magus-utils/types.go) special-cases time.Time to the Buzz `str` type mirroring Commit.BuzzObject's RFC3339 formatting, so the two can share a type without the generated mirror changing shape. The Buzz `object Commit` mirror is generated from this struct by cmd/magus-utils types (go:generate -type Commit).

func (CommitRecord) BuzzObject added in v0.4.0

func (v CommitRecord) BuzzObject() BuzzObject

type CompressResult added in v0.4.0

type CompressResult struct {
	// Files are based at the SOURCE directory - the files that went in - where
	// UncompressResult's are based at the destination. Each is based where it exists.
	Files    []Path
	BytesIn  int `buzz:"bytes_in"`
	BytesOut int `buzz:"bytes_out"`
}

CompressResult mirrors archive.compress's {files, bytes_in, bytes_out} object.

Deliberately NOT the same type as UncompressResult. The two look alike and are not: compressing reports what went in AND what came out, because the ratio is the thing you asked for, while uncompressing has only one size to report. Sharing a type would mean a bytes field that means different things depending on which call produced it.

func (CompressResult) BuzzObject added in v0.4.0

func (v CompressResult) BuzzObject() BuzzObject

type Conflict added in v0.4.0

type Conflict struct {
	// Path is relative to the root passed to Conflicts, using forward slashes.
	Path string
	// Kind is why the path is unresolved, and so what can settle it.
	Kind ConflictKind
}

Conflict is one unresolved path in an in-progress merge, rebase, or cherry-pick.

type ConflictKind added in v0.4.0

type ConflictKind string

ConflictKind classifies why a path is unresolved in an in-progress merge.

const (
	// ConflictKindContent is both sides changing the same file. The VCS has written
	// conflict markers into the working tree.
	ConflictKindContent ConflictKind = "content"
	// ConflictKindDeleted is one side deleting a file the other changed. No content
	// merge is possible, and no VCS invokes a merge driver for it - which is why a
	// driver alone never settles a workspace whose generated files moved.
	ConflictKindDeleted ConflictKind = "deleted"
	// ConflictKindBothDeleted is both sides deleting the file. No content on either
	// side, so recording the removal settles it.
	ConflictKindBothDeleted ConflictKind = "both-deleted"
)

func (ConflictKind) String added in v0.4.0

func (v ConflictKind) String() string

String renders v for an error message: the value, or "unset" when empty.

func (ConflictKind) Valid added in v0.4.0

func (v ConflictKind) Valid() bool

Valid reports whether v is a declared ConflictKind. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (ConflictKind) Values added in v0.4.0

func (v ConflictKind) Values() []string

Values lists the ConflictKind values a caller may choose, excluding the zero value.

type ConflictResolver added in v0.4.0

type ConflictResolver interface {
	// Conflicts returns the unresolved paths of the in-progress operation. No
	// operation in progress is not an error: it returns none.
	Conflicts(ctx context.Context, root string) ([]Conflict, error)
	// KeepIncoming clears the conflict markers by taking the INCOMING side wholesale -
	// git's "theirs", the commit being replayed during a rebase - falling back to the
	// surviving side where the incoming side has none. The side is named here so two
	// backends cannot disagree about which change survives.
	//
	// It marks nothing resolved. A caller regenerates between KeepIncoming and
	// MarkResolved, so the regenerated content is what gets recorded.
	KeepIncoming(ctx context.Context, root string, paths []string) error
	// MarkResolved records paths as resolved with their current working-tree content
	// (git's staging, hg's `resolve --mark`).
	MarkResolved(ctx context.Context, root string, paths []string) error
	// RemoveConflicts resolves paths by deleting them from both the working tree and
	// the recorded state.
	RemoveConflicts(ctx context.Context, root string, paths []string) error
	// IgnoredPaths reports which paths the VCS's ignore RULES cover, tracked or not.
	// Resolution needs it to tell a generated file that is still tracked from one since
	// ignored: both are declared outputs, but only the first survives a
	// ConflictKindDeleted. Paths absent from the result are not ignored.
	IgnoredPaths(ctx context.Context, root string, paths []string) (map[string]bool, error)
}

ConflictResolver is an optional capability (sibling of MergeDriverInstaller) for VCSDriver implementations that can report and settle an in-progress merge's unresolved paths in bulk.

A merge driver is the wrong shape for generated files: the VCS invokes one per conflicted path inside its own index manipulation, so cost scales with the conflict count and a regeneration cannot run there. Deciding every path first, regenerating once, then staging inverts that, and is the only way to settle ConflictKindDeleted, which a driver is never called for.

Callers type-assert and degrade to "resolve by hand" when a backend lacks it.

Every method takes the repository root and root-relative slash paths: a VCS reports conflict paths from the top level but reads pathspecs from the process directory, so mixing the two addresses the wrong files instead of failing.

type CrossTargetRef

type CrossTargetRef struct {
	Project string `json:"project" yaml:"project"`
	Target  string `json:"target"  yaml:"target"`
}

CrossTargetRef names one target in another project: a target-level cross-project dependency. Project is workspace-relative (resolved from the dot-/repo-relative path written in the magusfile); Target is the kebab-normalized target name.

func (CrossTargetRef) BuzzObject added in v0.4.0

func (v CrossTargetRef) BuzzObject() BuzzObject

func (CrossTargetRef) Ref added in v0.4.0

func (r CrossTargetRef) Ref() string

Ref spells the reference the way the CLI takes a target ref, "project:target" - the same method ChainStep carries, so a caller printing either kind of reference asks for it the same way. Project is never empty on this type (a cross-project ref names another project by definition), so there is no bare-name form.

type Culprit

type Culprit struct {
	// ID is the offending revision, matching Commit.ID and VCSMeta.ID. It was SHA,
	// which is git's alone - hg reports a node, jj a commit id - and bisect is not a
	// git-only operation (hg has its own).
	ID   string
	Info string // one-line subject, author, and date
}

Culprit is the outcome of a successful VCSDriver.Bisect call.

type DefaultRefReporter added in v0.4.0

type DefaultRefReporter interface {
	// DefaultRef returns the repo's primary line of development - git's default
	// branch, hg's "default", jj's trunk() - for the repo containing dir, or ""
	// with ErrVCSUnsupported when it cannot be determined.
	DefaultRef(ctx context.Context, dir string) (string, error)
}

DefaultRefReporter is an optional capability (sibling of RemoteReporter) for VCSDriver implementations that can report the repository's default branch, e.g. "main", independent of whatever branch is currently checked out. Committed artifacts (MAGUS.md's forge links) use it so their URLs stay stable no matter which feature branch or worktree generated them. Callers type-assert for it and degrade gracefully when a backend lacks it.

type DepGraphRepository added in v0.2.0

type DepGraphRepository interface {
	TopoSort() []string
	ReverseClosure(seeds []string) []string
	NearCycles(ctx context.Context, maxDepth int) []NearCycle
	BlastRadius() map[string]int
	NCCD() float64
	PathsFromSeeds(seeds []string, target string) []AffectedPath
	Successors(path string) []string
	Predecessors(path string) []string
	Nodes() []string
}

DepGraphRepository is the interface that internal/graph/dependency implements.

type Diagnostic added in v0.4.0

type Diagnostic struct {
	Code    string `json:"code" yaml:"code"`
	Message string `json:"message" yaml:"message"`
	// The buzz tag pins the Buzz name explicitly. LowerFirstWord would derive `url` from
	// URL anyway, but the tag is what a reader and a rename both see - the mirror name is
	// part of this type's contract, not a side effect of a casing rule.
	URL string `json:"url,omitempty" yaml:"url,omitempty" buzz:"url"`
}

Diagnostic is the shape a coded failure takes when it crosses into Buzz: the fields diagnostics.Error.BuzzError already produces, declared as a type rather than left as an undeclared map convention.

It exists because `catch` hands back an untyped value - Buzz has no union types, and a throw can carry a str, an int, or this - so the caller narrows it at the boundary the way errors.As does in Go:

catch (e) {
    final d: Diagnostic = e;
    if (d.code == "MGS2001") { ... }
}

Url is omitted when the domain captured none, so a caller testing it is asking "did this code come with docs", not reading an empty string that might mean either.

type DiagnosticCode

type DiagnosticCode = diagnostics.Code

DiagnosticCode identifies a stable diagnostic (MGS#### code). It aliases the framework's Code type, so every consumer keeps referring to types.DiagnosticCode while the machinery is shared.

const (
	NoCITarget               DiagnosticCode = "MGS1001"
	SpellShadowed            DiagnosticCode = "MGS1002"
	BespokePhaseFragmentName DiagnosticCode = "MGS1003"
	UnreachedFootprintDecl   DiagnosticCode = "MGS1004"
	RedundantFootprintGlob   DiagnosticCode = "MGS1005"
	UnknownTarget            DiagnosticCode = "MGS1006"
	TargetDependencyCycle    DiagnosticCode = "MGS1007"
	TargetMissingContext     DiagnosticCode = "MGS1008"
	TargetNeverReplays       DiagnosticCode = "MGS1009"
	AffectedSetUncomputable  DiagnosticCode = "MGS1010"
	CrossOutputOwnerUnknown  DiagnosticCode = "MGS1011"
	CrossOutputCycle         DiagnosticCode = "MGS1012"
	CrossOutputGlobEscapes   DiagnosticCode = "MGS1013"
	CrossOutputNotProduced   DiagnosticCode = "MGS1014"
	CrossDepOwnerUnknown     DiagnosticCode = "MGS1015"
	GoModReplaceDrift        DiagnosticCode = "MGS1016"
	MagusfileIsNotASpell     DiagnosticCode = "MGS1017"
	DeadOutputGlob           DiagnosticCode = "MGS1018"
	SelfStalingOutput        DiagnosticCode = "MGS1019"
	OutputOwnedByTwoTargets  DiagnosticCode = "MGS1020"
	WorkspaceNeedsNewerMagus DiagnosticCode = "MGS1021"
	MagusfileOnlyMember      DiagnosticCode = "MGS1022"
	ProviderPathRejected     DiagnosticCode = "MGS1023"
	ProviderProjectShadowed  DiagnosticCode = "MGS1024"
	MagusfileAPIRemoved      DiagnosticCode = "MGS1025"
	CacheableSecretRead      DiagnosticCode = "MGS1026"
	// SecretGrantInvalid covers every way a secret grant is unusable: a missing field, a
	// wildcard or non-ASCII host, a header that is not a legal field name. ONE code
	// rather than one per rule, because the resolution is the same in every case - fix
	// the declaration the error names - and a caller branching on it wants "this grant
	// is malformed", not which clause caught it. The message carries the specifics.
	SecretGrantInvalid DiagnosticCode = "MGS1027"
	// UndeclaredSeedingFile is a changed file no project declares that still pulled a
	// project into the affected set through directory containment. It reruns targets
	// while moving no cache key, so the work is real and its result was already
	// correct - the expensive half of an under-declaration, with the silent half
	// (nothing reruns when the file DOES matter) waiting behind it.
	UndeclaredSeedingFile DiagnosticCode = "MGS1028"
	// UnmatchableSourceGlob is a source or read declaration whose static directory
	// prefix lands inside a pruned tree (project.IgnoreDirs: gen, vendor, node_modules,
	// target). The expansion walk skips those directories wholesale, so the pattern
	// matches nothing and silently contributes no cache key - the target replays while
	// the files it named change underneath it.
	//
	// The mirror of MGS1014, which catches a declared OUTPUT that no run produces. Both
	// are a declaration disconnected from reality, and both are invisible without a
	// check: `describe target` lists the glob under sources either way.
	//
	// An EXACT path is not reported: a wildcard-free declaration names one file, so it
	// is resolved by stat rather than the walk and reaches the key normally. Only a
	// pattern is unmatchable, and only because letting one reach into a pruned tree is
	// what pruning exists to prevent (a bare **/*.js would hash all of node_modules).
	UnmatchableSourceGlob DiagnosticCode = "MGS1029"
	// OutputIsAnotherProjectsSource is a file one project declares as an OUTPUT that
	// another project's source glob also claims. Nothing is wrong until the file's
	// content changes: then the generating project rewrites it, the claiming project
	// sees a declared source move underneath a target it is running, and reports the
	// write as an undeclared mutation (MGS4007) against a file that is generated by
	// definition.
	//
	// Static, from the declarations alone - no run required, and it holds whether or not
	// the content happens to be drifting today. That is the point: this repository
	// carried the conflict on every project's MAGUS.md for as long as the markdown
	// spell has claimed **/*.md, and only the one whose bytes went stale ever surfaced.
	//
	// EXACT output paths only, against the other project's globs. The same decidability
	// line MGS4002 draws: glob-vs-glob overlap is not decidable in general, but asking
	// whether a pattern matches one literal path is.
	OutputIsAnotherProjectsSource DiagnosticCode = "MGS1031"
	// MemoryDeclarationDrift is a target whose memory_mb disagrees with the peak
	// resident memory magus has actually measured for it, in either direction, or
	// that declares nothing while measurably taking a material share of the machine.
	//
	// This is what keeps memory_mb honest. A declared figure is only as good as its
	// declarations, and declarations rot silently: a target written at 2GB grows to
	// 9GB over a year and the gate quietly stops protecting anything, while a target
	// declared far above what it uses refuses peers that would have fit. magus already
	// records a peak per target, so the disagreement is a fact it holds rather than a
	// question for the author.
	//
	// Advice, never a failure. The measurement is a maximum over recent runs on
	// whatever machines happened to run them, and the author is entitled to declare a
	// figure that differs deliberately - a ceiling for a target whose peak varies with
	// its input, say. magus reports what it measured; the number in the magusfile
	// stays a human's to write.
	MemoryDeclarationDrift    DiagnosticCode = "MGS1030"
	PathReadDenied            DiagnosticCode = "MGS2001"
	PathWriteDenied           DiagnosticCode = "MGS2002"
	EnvStripped               DiagnosticCode = "MGS2003"
	AllowlistUnresolved       DiagnosticCode = "MGS2004"
	SandboxUnsupported        DiagnosticCode = "MGS2005"
	PathShimSuspected         DiagnosticCode = "MGS2006"
	ExecDenied                DiagnosticCode = "MGS2007"
	DaemonSocketWithheld      DiagnosticCode = "MGS2008"
	SandboxPolicyMismatch     DiagnosticCode = "MGS2010"
	SecretTooShortToMask      DiagnosticCode = "MGS2011"
	DescendantBoundaryCrossed DiagnosticCode = "MGS3001"
	VCSUnavailable            DiagnosticCode = "MGS3002"
	ToolNotOnPath             DiagnosticCode = "MGS3003"
	// ToolNotReady is ToolNotOnPath one level deeper: the binary IS present, but the
	// service it talks to is not reachable. Same category - the environment, not the
	// code - so it sits beside it rather than in a family of its own.
	ToolNotReady DiagnosticCode = "MGS3004"
	// ToolTooOld is the fourth question about a tool: it exists, it reports a version,
	// it is usable - and that version is below the declared minimum.
	ToolTooOld DiagnosticCode = "MGS3005"
	// ToolTooNew is ToolTooOld's other side: the version is at or above a ceiling the
	// spell or the workspace excludes. A separate code rather than a shared "version
	// rejected" because the remediation is the opposite one, and because folding both
	// into MGS3005 is exactly the defect this pair replaced - a too-new binary being
	// told it was too old.
	ToolTooNew DiagnosticCode = "MGS3006"
	// ProjectLockHeldByAncestor is a magus run that cannot proceed because a project it
	// must lock is already locked by one of its OWN ancestor invocations, which cannot
	// release it until this run exits. It sits in the environment family beside the tool
	// codes: nothing in the workspace is wrong, the process context the run was started in
	// makes it impossible. Named for the condition it detects, not for a "re-entrant lock"
	// magus does not offer.
	ProjectLockHeldByAncestor DiagnosticCode = "MGS3007"
	// NoWorkspaceRoot is the most common first-run failure: no ancestor directory
	// declares a workspace (magus.yaml) or a contiguous run of projects (magusfile.buzz,
	// magusfiles/, go.mod) reaching one. `magus init` is the fix in both cases, so the
	// message names it directly rather than leaving the reader to find the command.
	NoWorkspaceRoot DiagnosticCode = "MGS3008"
	// MachineBudgetExhausted is a step magus did not start because the concurrency and
	// declared memory it needs do not fit alongside what every other magus on this
	// machine holds. It joins MGS3007 in the environment family for the same reason: the
	// workspace is correct and the code is fine, the machine cannot seat the work.
	//
	// Ordinarily a queue rather than an error - the daemon that owns the budget tells a
	// waiter when its turn comes. It surfaces as this code in the two cases a wait
	// cannot fix: MAGUS_NO_WAIT asked to fail fast, or the declaration does not fit in
	// the whole budget, so an idle machine would refuse it too. Both exit 75.
	MachineBudgetExhausted    DiagnosticCode = "MGS3009"
	RaceDetected              DiagnosticCode = "MGS4001"
	OutputOverlapDetected     DiagnosticCode = "MGS4002"
	NondeterministicOutput    DiagnosticCode = "MGS4003"
	MissingDependencyDetected DiagnosticCode = "MGS4004"
	EnvironmentalDrift        DiagnosticCode = "MGS4005"
	StaleGeneratedOutput      DiagnosticCode = "MGS4006"
	// UndeclaredSourceModified is a target that rewrote a file it declared as a SOURCE
	// and did not declare as an update. The key that identified the result no longer
	// describes the inputs that produced it, so the entry is unreproducible by
	// construction.
	//
	// Deliberately charm-agnostic. Keying on the charm was considered and rejected: a
	// charm patches args, so it cannot say whether the tool writes, and a rule keyed on
	// whether the caller TYPED the charm exempts a mutating custom charm merely because
	// someone spelled it out. ctx.modifiesExistingFiles is the declaration that answers
	// this, which is why a formatter names its edits rather than earning an exemption.
	UndeclaredSourceModified DiagnosticCode = "MGS4007"
	NearDuplicateServices    DiagnosticCode = "MGS5001"
	ServiceOpDetached        DiagnosticCode = "MGS5002"
	CommandOpNeverExits      DiagnosticCode = "MGS5003"
	DaemonRequired           DiagnosticCode = "MGS5004"
	CharmPatchInvalid        DiagnosticCode = "MGS6001"
	UnresolvableBuzzImport   DiagnosticCode = "MGS7001"
	DanglingDocReference     DiagnosticCode = "MGS7002"
	OutputRefMissing         DiagnosticCode = "MGS8001"
	OutputRefAmbiguous       DiagnosticCode = "MGS8002"
	OutputRefMalformed       DiagnosticCode = "MGS8003"
	OutputRefForeignMachine  DiagnosticCode = "MGS8004"
	BearerRejected           DiagnosticCode = "MGS9001"
	InsecureTokenPermissions DiagnosticCode = "MGS9002"
	ConnectorStoreTooNew     DiagnosticCode = "MGS9003"
	NoAuthToken              DiagnosticCode = "MGS9004"
	ConnectorNameExists      DiagnosticCode = "MGS9005"
	ConnectorNotFound        DiagnosticCode = "MGS9006"

	// VCSCapabilityMissing fires when the configured version-control backend does not implement
	// a lookup a feature needs, so the answer is reported as unavailable rather than as empty.
	//
	// It heads the CAPABILITY family: a magus feature exists, and the backend or provider wired
	// here has not implemented the piece it needs. These are not errors in the ordinary sense and
	// mostly do not fail a command - the reader did nothing wrong, and the surface still works
	// without the missing piece. They exist because the alternative is silence, and silence is
	// indistinguishable from the good news. "No other branch touches these files" and "this
	// backend cannot tell you about other branches" lead a reader to opposite decisions, and only
	// one of them is reassurance.
	//
	// A subset is legitimate by design - the review contract says so outright, and a VCS backend
	// answers what its host can answer - so a gap is a fact to report, never a spell to fix.
	VCSCapabilityMissing DiagnosticCode = "MGS1101"
	// ReviewOpMissing fires when the wired review spell does not export one of the four reserved
	// ops. On a READ that is a silence worth naming; on a WRITE it is an error, because a
	// tolerated no-op would mark every draft published and lose the remarks permanently.
	ReviewOpMissing DiagnosticCode = "MGS1102"
	// ReviewAuthorshipUnknown fires when a provider names neither the review's author nor the
	// credential holder, so magus cannot tell a self-review from a colleague's and will not
	// approve on the reader's behalf. Not knowing is not permission.
	ReviewAuthorshipUnknown DiagnosticCode = "MGS1103"
)

func AllDiagnosticCodes added in v0.2.0

func AllDiagnosticCodes() []DiagnosticCode

AllDiagnosticCodes returns every registered diagnostic code in ascending MGS order. The returned slice is a copy; callers may mutate it freely.

func ClassifyDrift added in v0.4.0

func ClassifyDrift(inputDirty bool, magusVersion string) (DiagnosticCode, string)

ClassifyDrift names the cause of a declared output that moved, and the sentence to show for it - the one place that fork is decided, so the generate gate and `magus vcs add` cannot describe one condition two ways.

The fork is on WHY, not on how bad it is:

  • a declared input moved too (MGS4006): regeneration is expected, and the output belongs in the same commit as the source that moved it;
  • inputs unchanged, running a DEV build (MGS4005): the committed form is produced by the pinned release, so this is version skew and not the developer's change;
  • inputs unchanged, running a RELEASE build (MGS4003): same inputs, same generator version, different bytes - a reproducibility bug.

magusVersion is the running binary's version, empty when unknown.

type DiagnosticError

type DiagnosticError = diagnostics.Error

DiagnosticError is a typed error carrying an MGS code and message (the framework's Error). It implements error, and a DiagnosticCode is itself an errors.Is sentinel, so a caller matches one idiomatically: errors.Is(err, types.ExecDenied).

func DiagnosticErrorf

func DiagnosticErrorf(c DiagnosticCode, format string, args ...any) *DiagnosticError

DiagnosticErrorf builds a DiagnosticError with an MGS code and formatted message, capturing the code's docs URL for rendering.

func WrapDiagnostic added in v0.3.0

func WrapDiagnostic(c DiagnosticCode, cause error, format string, args ...any) *DiagnosticError

WrapDiagnostic builds a DiagnosticError that carries an MGS code AND wraps cause, so errors.Is(err, cause) keeps matching while the error gains a lookupable code. Use it when a sentinel already drives control flow (e.g. ErrUnknownTarget) and must keep matching.

type DiagnosticEvent added in v0.2.0

type DiagnosticEvent = diagnostics.Event

DiagnosticEvent is one diagnostic fired during a run (the framework's Event).

type DiagnosticSink added in v0.2.0

type DiagnosticSink = diagnostics.Sink

DiagnosticSink records diagnostics fired during a run (the framework's Sink).

type Diff added in v0.4.0

type Diff struct {
	// Base is the ref the diff was taken against, or "working" for the uncommitted tree.
	Base string `json:"base" yaml:"base"`
	// Files carries one entry per changed path, in the order magus recommends READING them:
	// consequence first. See SortForReading. It is the changeset's primary collection, so
	// it is what -o jsonl streams.
	Files []DiffFile `json:"files,omitempty" yaml:"files,omitempty" jsonl:"primary"`
	// SeedProjects are the projects a changed file lands in directly - the ones the author
	// actually edited, as opposed to the ones that merely rebuild.
	SeedProjects []string `json:"seed_projects,omitempty" yaml:"seed_projects,omitempty"`
	// AffectedProjects is the full reverse closure. The gap between its length and
	// SeedProjects' is the whole "why is docs in my build" question, so both ship.
	AffectedProjects []ImpactProject `json:"affected_projects,omitempty" yaml:"affected_projects,omitempty"`
	// Notes are magus-authored caveats about what could NOT be computed (no symbol index, no
	// coverage run). They are surfaced rather than swallowed: a reader who sees no reach
	// numbers must be able to tell "nothing depends on this" from "nothing was measured".
	Notes []string `json:"notes,omitempty" yaml:"notes,omitempty"`
	// Reviewed is the earlier pass this reader already made over these files, when there was one.
	Reviewed DiffReviewed `json:"reviewed,omitzero" yaml:"reviewed,omitzero"`
}

Diff is the whole annotated changeset.

func (Diff) AttachChurn added in v0.4.0

func (r Diff) AttachChurn(files []FileHotspot, projects []TrendEntry)

AttachChurn folds the VCS-history lenses onto the review, in place.

It is a separate step from building the review, and the caller supplies the lens data, because the two have very different costs and very different freshness needs. The annotations are cheap and must be current; the history lenses are a bounded git-log scan that the daemon already caches for everyone. Computing them inside Review would either make every review pay for a scan or bake a cache into a function that has no business owning one. So the daemon passes its cached scan and the CLI passes a fresh one, and this is the single definition of how the numbers land on a file either way.

A file with no hotspot entry gets Churn only when its project's trend actually MOVED, so "quiet file in an accelerating project" is still expressible; a file with neither is left nil, because nil is what says nobody measured. A zero delta is not movement - it is the same absence of evidence a missing trend entry is - and such a row carries no hotspot counts either, only the trend it was built from.

func (Diff) AttachReadState added in v0.4.0

func (r Diff) AttachReadState(byPath map[string]string)

AttachReadState folds recorded read receipts onto the review, in place.

Supplied by the caller for the same reason AttachReplay's data is: the receipts live beside the cache dir, and fingerprinting each file to check one is a cost the caller decides to pay while the fold stays defined once. A file the map does not name keeps DiffReadUnknown, which is why an empty map returns early rather than marking everything unread.

func (Diff) AttachReplay added in v0.4.0

func (r Diff) AttachReplay(byPath map[string][]DiffTouch)

AttachReplay folds the agent trail onto the review, in place.

Supplied by the caller for the same reason AttachChurn's data is: the trail lives beside the daemon's cache dir and reading it is a different cost from computing annotations, so who pays and how much they read is the caller's decision while the fold stays defined once.

func (*Diff) AttachReviewed added in v0.4.0

func (r *Diff) AttachReviewed(at VCSCheckpoint, files int)

AttachReviewed records the earlier pass a reader made over these files.

A pointer receiver, unlike its neighbours: this writes a field on the Diff itself rather than on the elements of a slice it holds, and a value receiver would drop it silently.

func (Diff) BuzzObject added in v0.4.0

func (v Diff) BuzzObject() BuzzObject

func (Diff) GeneratedCount added in v0.4.0

func (r Diff) GeneratedCount() int

GeneratedCount reports how many files are declared outputs - the ones a reader can fold away. It is the headline of the noise-collapse affordance.

func (Diff) Ranked added in v0.4.0

func (r Diff) Ranked() bool

Ranked reports whether the ordering had a ranking key to work with at all.

False means every file's reach is unmeasured, so SortForReading had nothing to sort on and the result is path order wearing a ranking's clothes. Every renderer MUST say so before showing the list. The review's Note about an absent symbol index does not cover this: it names the missing OVERLAYS (callers, coverage) and never the missing ORDER, and it prints after the list - by which point the reader has already formed the belief that the first file is the most dangerous one.

func (Diff) SortForReading added in v0.4.0

func (r Diff) SortForReading()

SortForReading orders Files into the sequence magus recommends reading them in. It sorts in place and is the one definition of "review order", so the console, the CLI, and a Buzz advisor writing a pull-request comment all agree on what to read first.

The rule, in order:

  1. Generated output goes LAST, always, whatever its reach. Its diff is a machine's restatement of a change made somewhere else, so reading it before the source that caused it is reading the answer before the question.
  2. Then widest reach first: the file whose changed symbols are referenced from the most other files is the one most able to break something.
  3. Then unclaimed and maintained files after ordinary sources - they invalidate no cache key and affect no target, so nothing downstream turns on them.
  4. Path, last, purely so the order is deterministic. It is a TIEBREAK and never a ranking: alphabetical order is what this exists to replace.

A file whose reach was NOT MEASURED sorts above one measured at zero and below any measured positive. Unknown is not zero: a measured zero is a promise that nothing references the file, and an unmeasured file has made no promise. Ranking the two together is what let an unindexed workspace render pure path order while the header still claimed a ranking - see Ranked, which is how a caller is supposed to notice.

type DiffAuthor added in v0.4.0

type DiffAuthor string

DiffAuthor says which kind of client produced a comment or a suggestion.

It is STAMPED BY THE DAEMON from the transport the write arrived on, and never read from the payload. That is the whole integrity of a paired review: an agent holds an MCP session and a person holds a console tab, the daemon can tell them apart, and so an agent cannot post as the person. The notes store settled the same question the same way - "a self-attested author is forgeable by whatever wrote the file" - and this is that reasoning applied to a store an agent IS allowed to write.

const (
	// DiffAuthorHuman is a write from the console or an interactive CLI.
	DiffAuthorHuman DiffAuthor = "human"
	// DiffAuthorAgent is a write from the MCP surface.
	DiffAuthorAgent DiffAuthor = "agent"
)

type DiffChurn added in v0.4.0

type DiffChurn struct {
	// Commits is how many commits in the window touched this file.
	Commits int `json:"commits" yaml:"commits"`
	// Authors is how many distinct people did. One author on a hot file is a bus-factor
	// problem; many on a hot file is a coordination one. Both are worth knowing, neither is
	// worth magus deciding.
	Authors int `json:"authors,omitempty" yaml:"authors,omitempty"`
	// Score is commits x complexity - the hotspot ranking's own metric.
	Score int `json:"score" yaml:"score"`
	// Rank is this file's 1-based position in the workspace's hotspot ranking; 0 when it did
	// not rank at all, which is the common and unremarkable case.
	Rank int `json:"rank,omitempty" yaml:"rank,omitempty"`
	// ProjectTrend is the owning project's churn delta across the window's two halves.
	// Positive is accelerating. It is PROJECT level because that is the granularity the trend
	// lens measures; a file-level trend would be inventing precision the data does not have.
	ProjectTrend int `json:"project_trend,omitempty" yaml:"project_trend,omitempty"`
}

DiffChurn is how often this file has been changing, and whether that is accelerating.

It answers a question the diff itself cannot: not "is this change correct" but "is this file being rewritten over and over". A file edited repeatedly is frequently a design problem wearing a series of small fixes - the circle you notice only in hindsight, after the fifth visit. Surfacing it AT review time is the whole point, because that is the one moment somebody is already looking at the file and could still decide to fix the cause instead.

Every field is a measurement over a bounded commit window, not a judgment. Rank is the useful one to render: "third-hottest file in the workspace" means something to a reader in a way a raw score never will.

func (DiffChurn) BuzzObject added in v0.4.0

func (v DiffChurn) BuzzObject() BuzzObject

func (DiffChurn) NotableRank added in v0.4.0

func (c DiffChurn) NotableRank() bool

NotableRank reports whether this file ranks high enough for its position to be worth showing. A file outside the cutoff still reports its commit count, which is the part that remains meaningful on its own - and which is what tells a reader that churn WAS measured here, so a missing rank means "outside the top NotableRankCutoff" rather than "not measured".

func (DiffChurn) Rising added in v0.4.0

func (c DiffChurn) Rising() bool

Rising reports whether this file is both hot and getting hotter - the combination worth interrupting a reader for. Either signal alone is ordinary: plenty of files are hot because they are big, and plenty of projects are accelerating for good reasons.

type DiffCommandHints

type DiffCommandHints struct {
	CLI string
	GUI string
}

DiffCommandHints holds shell commands for inspecting a diff.

type DiffComment added in v0.4.0

type DiffComment struct {
	ID     string     `json:"id" yaml:"id"`
	Path   string     `json:"path" yaml:"path"`
	Hunk   int        `json:"hunk" yaml:"hunk"`
	Author DiffAuthor `json:"author" yaml:"author"`
	// AgentName is the opaque host label an MCP client passed, empty for a human. Attribution
	// only: nothing branches on it, matching the hook's treatment of the same field.
	AgentName string `json:"agent_name,omitempty" yaml:"agent_name,omitempty"`
	Body      string `json:"body" yaml:"body"`
	// Anchor is what this remark remembers about the code it was written against.
	Anchor CommentAnchor `json:"anchor,omitzero" yaml:"anchor,omitzero"`
	// Rung is how the remark's Line was arrived at, recomputed whenever the changeset is tracked
	// and never stored. Output-only: a client renders it, and nothing accepts one.
	Rung     CommentAnchorRung `json:"rung,omitempty" yaml:"rung,omitempty"`
	Resolved bool              `json:"resolved" yaml:"resolved"`

	// Published records that this remark has left the machine. False for a draft, which is
	// every comment until someone publishes.
	//
	// A published comment is no longer editable here: it exists somewhere a teammate may have
	// already replied to, and a local edit that silently diverged from what they are reading
	// would be worse than no edit at all.
	//
	// What the HOST called it is deliberately not recorded. A review posts as one request and
	// its per-comment ids come back in a shape no provider is obliged to return, so a field
	// for them would have been a field nothing fills.
	Published bool `json:"published,omitempty" yaml:"published,omitempty"`

	// Line is the position in the file's NEW side, which is what a host anchors an inline
	// comment to. Hunk cannot serve: it is an index into this changeset's hunks, a coordinate
	// that means nothing outside the session that produced it.
	//
	// Zero means the writer did not pin one, and a publisher drops such a comment rather than
	// guessing. A remark that lands on the wrong line is worse than one that never left.
	Line int `json:"line,omitempty" yaml:"line,omitempty"`
}

DiffComment is one remark attached to a hunk.

Distinct from a note, and deliberately not stored with one. A note is durable workspace knowledge whose only provenance is the person who wrote it; a comment is addressed to one author about one change and is dead once the change lands. Folding them together would either poison the notes store with ephemeral chatter or force review through an editor and a commit, which nobody will do. A comment worth keeping graduates into a note by hand, which re-attributes it to a person and a commit.

type DiffCursor added in v0.4.0

type DiffCursor struct {
	Path string `json:"path,omitempty" yaml:"path,omitempty"`
	// Hunk is the 0-based index of the hunk within the file, or -1 for the file heading.
	Hunk int `json:"hunk" yaml:"hunk"`
}

DiffCursor is where a client is looking: a file and, within it, a hunk.

The human's cursor is the one that matters and the one an agent READS. An agent has no cursor of its own, on purpose - see DiffSuggestion for why it cannot move this one.

type DiffFile added in v0.4.0

type DiffFile struct {
	Path string `json:"path" yaml:"path"`
	// Project is the owning project, empty when no project directory contains the file.
	Project string `json:"project,omitempty" yaml:"project,omitempty"`
	// Role is one of the DiffRole constants. It is the single most useful fact about a
	// changed file, because it answers "must I read this" before any of the rest.
	Role string `json:"role" yaml:"role"`
	// Hint is magus's own sentence about the role, reused verbatim from describe file so the
	// console and the CLI never drift into two explanations of one classification.
	Hint string `json:"hint,omitempty" yaml:"hint,omitempty"`
	// Coverage is the file's observed coverage, nil when none was measured. Nil is DISTINCT
	// from zero: "no coverage run has happened" must not render as "this code is untested".
	Coverage *ImpactCoverage `json:"coverage,omitempty" yaml:"coverage,omitempty"`
	// Symbols are the changed symbols this file defines, each carrying how widely it is
	// referenced. Empty when no symbol index covers the file.
	Symbols []DiffSymbol `json:"symbols,omitempty" yaml:"symbols,omitempty"`
	// Surface is one of the DiffSurface constants: whether any changed symbol here is
	// referenced from another project. It is the semver-relevant fact, and it is evidence
	// rather than a verdict - see DiffSurface.
	Surface string `json:"surface" yaml:"surface"`
	// Touches are the agent sessions that wrote this file and what they had READ first.
	// Empty when no guard hook is wired, which is the common case and not a fault.
	Touches []DiffTouch `json:"touches,omitempty" yaml:"touches,omitempty"`
	// ReadState is one of the DiffRead constants: whether a person recorded reading
	// this file at its current content. Empty is DiffReadUnknown - nobody checked - and is
	// deliberately not "unread".
	ReadState string `json:"read_state,omitempty" yaml:"read_state,omitempty"`
	// Churn is how often this file has been changing, nil when no history lens was attached.
	// Nil is DISTINCT from zero: "nobody measured" and "this file is quiet" are different
	// facts, and a review that renders the first as the second is lying quietly.
	Churn *DiffChurn `json:"churn,omitempty" yaml:"churn,omitempty"`
	// NoHistory reports that the history lens RAN and found this file in none of the commits
	// it walked - a file added in this change, or one untouched for the whole window.
	//
	// It is a ranking signal because absence of history is not the same as absence of risk,
	// and without it the ordering conflates the two. Every other annotation here is derived
	// from a file's past, so a brand-new file collects no churn, no hotspot rank, no authors
	// and no coverage, and sinks to the bottom on the strength of having no evidence. The one
	// file in a real changeset whose tests did not compile was exactly that file. Nothing has
	// exercised this code and nobody has reviewed it before, which is a reason to read it
	// sooner rather than later.
	//
	// False when the lens did not run at all: that is "nobody looked", and it must not render
	// as "this file has history".
	NoHistory bool `json:"no_history,omitempty" yaml:"no_history,omitempty"`
	// Reach is the widest FileCount among Symbols: how many files reference the most-referenced
	// thing this file changed. It is the ranking key, and it is deliberately a COUNT OF FILES
	// rather than of references - one file calling a function forty times is one file that
	// breaks, and ranking by reference count would put a hot loop above a widely-used API.
	//
	// NIL when no symbol index was loaded, and nil is DISTINCT from zero for the same reason
	// Coverage and Churn are: "nothing references this" and "nobody looked" are different
	// facts. As a plain int an unindexed workspace serves `reach: 0` on every file - a
	// valid-looking number that a fleet dashboard reads as "no change touches widely used
	// code". DiffSurfaceUnknown already refuses that collapse; the pointer is the same refusal
	// applied to the field the ordering actually turns on.
	Reach *int `json:"reach" yaml:"reach"`
}

DiffFile is one changed file, annotated.

func (DiffFile) BuzzObject added in v0.4.0

func (v DiffFile) BuzzObject() BuzzObject

func (DiffFile) Generated added in v0.4.0

func (f DiffFile) Generated() bool

Generated reports whether reviewing this file's diff is reading generated output.

func (DiffFile) ReachOr added in v0.4.0

func (f DiffFile) ReachOr(def int) int

ReachOr returns the reach, or def when it was not measured. For rendering and comparison only - never use it to decide whether reach is KNOWN, which is what the nil is for.

type DiffReviewed added in v0.4.0

type DiffReviewed struct {
	// At is the revision the reader last got through, oldest where receipts disagree.
	At VCSCheckpoint `json:"at,omitzero" yaml:"at,omitzero"`
	// Files is how many of this changeset's files that revision covers.
	Files int `json:"files,omitempty" yaml:"files,omitempty"`
}

DiffReviewed is what a reader already got through on an earlier pass over this changeset.

A CHANGESET-level fact rather than a per-file one, because it answers a question about the reader's history rather than about any file: "where did I leave off". The per-file half is DiffFile.ReadState, and the two are not redundant - ReadState says whether THIS file still matches what was read, and this says which revision to diff from to see everything that moved.

The zero value means there is no earlier pass to subtract: nobody has reviewed these files, or the reviewing was done against a working tree, which has no revision to name. Neither is "nothing changed", and a surface must not render it as reassurance.

func (DiffReviewed) BuzzObject added in v0.4.0

func (v DiffReviewed) BuzzObject() BuzzObject

type DiffSession added in v0.4.0

type DiffSession struct {
	ID   string `json:"id" yaml:"id"`
	Base string `json:"base" yaml:"base"`
	// AsOf is the digest of the patch this changeset was computed from - the session's
	// snapshot identity.
	//
	// Without it a client cannot tell a current answer from a frozen one, and the party least
	// able to notice is the one it hurts: an agent cannot see the tree, so it acted with
	// confidence on a changeset that had stopped existing. It also makes "9 of 12 read"
	// meaningful, because the denominator can be shown to be the one those marks were made
	// against.
	AsOf string `json:"as_of,omitempty" yaml:"as_of,omitempty"`
	// Review is the annotated changeset. Recomputed when the working tree moves.
	Diff Diff `json:"diff" yaml:"diff"`
	// Cursor is where the HUMAN is looking.
	Cursor DiffCursor `json:"cursor" yaml:"cursor"`
	// Viewed holds the content digests of hunks the human has marked read. Digests rather
	// than paths-and-line-numbers so the mark survives a rebase that did not touch the hunk -
	// the failing of every viewed-checkbox that resets on force-push.
	Viewed []string `json:"viewed,omitempty"      yaml:"viewed,omitempty"`
	// SeenThreads holds the ids of the review's threads the human has actually had on screen.
	// It is the watermark that decides what counts as NEW, and it belongs to the reader for the
	// same reason Viewed does: a mark nobody made is a claim nobody can stand behind.
	//
	// ONE watermark, deliberately. The obvious alternative - letting the job that watches the
	// forge record what it has reported - means everything is already marked seen by the time
	// the reader opens the diff, so the surface could never show them what arrived. The job
	// reads this instead and reports what lies outside it.
	SeenThreads []string         `json:"seen_threads,omitempty" yaml:"seen_threads,omitempty"`
	Comments    []DiffComment    `json:"comments,omitempty"     yaml:"comments,omitempty"`
	Suggestions []DiffSuggestion `json:"suggestions,omitempty"  yaml:"suggestions,omitempty"`
}

DiffSession is the shared object a console tab, an MCP agent, and the CLI all read.

One object rather than three implementations: the daemon already multiplexes those three transports over one workspace, so a review they each rebuilt privately would be three diverging opinions of the same changeset. Sharing it is what makes pairing real - the agent can see where the human is and be useful about it rather than narrating blindly.

func (DiffSession) UnseenThreads added in v0.4.0

func (s DiffSession) UnseenThreads(threads []ReviewThread) []string

UnseenThreads returns the ids in threads the reader has not had on screen, in the order given.

Ids rather than a COUNT, because a count is wrong in the case that matters: a comment deleted and another added nets zero, and the new one is then never reported.

type DiffSuggestion added in v0.4.0

type DiffSuggestion struct {
	ID        string `json:"id" yaml:"id"`
	Path      string `json:"path" yaml:"path"`
	Hunk      int    `json:"hunk" yaml:"hunk"`
	AgentName string `json:"agent_name,omitempty" yaml:"agent_name,omitempty"`
	// Reason is the one line shown beside the affordance. It has to earn the interruption.
	Reason string `json:"reason" yaml:"reason"`
	// Accepted records that the human took it, so the agent can tell "not yet seen" from
	// "seen and declined" instead of repeating itself.
	Accepted bool `json:"accepted" yaml:"accepted"`
	Declined bool `json:"declined" yaml:"declined"`
}

DiffSuggestion is an agent asking for the human's attention somewhere.

It is a PROPOSAL and never an action, which is the load-bearing decision in the whole paired-review design. A tool that lets an agent move the human's viewport is an agent that yanks the screen around mid-read; the reviewer stops trusting their own scroll position and the tool starts feeling possessed. So the agent explains and the human navigates: a suggestion renders as a peripheral affordance the human accepts with one key, and ignoring it costs nothing.

This is the review-surface reading of the guard's own rule - deny only what cannot be undone, explain everything else. Yanking a viewport cannot be undone, because the reader's place in the diff was in their head.

type DiffSymbol added in v0.4.0

type DiffSymbol struct {
	ID    string `json:"id" yaml:"id"`
	Label string `json:"label,omitempty" yaml:"label,omitempty"`
	// RefCount and FileCount are occurrences and distinct referencing files.
	RefCount  int `json:"ref_count"  yaml:"ref_count"`
	FileCount int `json:"file_count" yaml:"file_count"`
	// ExternalProjects are the OTHER projects that reference this symbol, sorted. Non-empty
	// is what makes a file's surface public, and naming them answers the reader's actual next
	// question - who breaks - rather than only how many.
	ExternalProjects []string `json:"external_projects,omitempty" yaml:"external_projects,omitempty"`
	// ExternalFileCount is how many referencing files sit outside the defining project.
	ExternalFileCount int `json:"external_file_count" yaml:"external_file_count"`
	// ModuleAPI reports that this symbol is exported from the MODULE - reachable by a
	// consumer outside the workspace entirely.
	//
	// It is a separate question from ExternalProjects and neither implies the other, which is
	// the whole reason both exist. A symbol can be referenced by no other project and still be
	// public API that a downstream module imports; measured on this repository, every referent
	// of the root package sits in the root project, so cross-project exposure alone reported
	// the published SDK surface as internal. Conflating the two answers the wrong question:
	// "who in this workspace breaks" is not "who in the world breaks".
	ModuleAPI bool `json:"module_api,omitempty" yaml:"module_api,omitempty"`
}

DiffSymbol is one changed symbol with its exposure.

func (DiffSymbol) BuzzObject added in v0.4.0

func (v DiffSymbol) BuzzObject() BuzzObject

type DiffTouch added in v0.4.0

type DiffTouch struct {
	Host       string   `json:"host,omitempty"       yaml:"host,omitempty"`
	Session    string   `json:"session,omitempty"    yaml:"session,omitempty"`
	Transcript string   `json:"transcript,omitempty" yaml:"transcript,omitempty"`
	Read       []string `json:"read,omitempty"       yaml:"read,omitempty"`
	// Ran are the PROGRAMS the session ran, never their arguments - see trail.Touch.Ran for
	// the leak that shape exists to prevent. This payload is served to every MCP client.
	Ran []string `json:"ran,omitempty" yaml:"ran,omitempty"`
}

DiffTouch is one agent session's contact with a changed file: that it wrote the file, and what it was looking at immediately before.

This is the part of a review no forge can produce. A guard hook observes every path an agent reaches, so magus can answer "what was it reading when it decided to write this" - the closest thing to the author's reasoning that any tool can recover without asking them. A diff shows what changed; this shows what the change was made in response to.

Transcript is a POINTER and magus never opens it. The trail stays a record of paths and timings, and a reader who wants what was actually said opens the host's own log themselves - which is what lets a whole session's reach be carried cheaply while the expensive and sensitive detail stays where the host already put it.

func (DiffTouch) BuzzObject added in v0.4.0

func (v DiffTouch) BuzzObject() BuzzObject

type Direction

type Direction int

Direction is the traversal direction for dependency-graph rendering.

const (
	Downstream Direction = iota // dependencies of each project
	Upstream                    // dependents of each project
)

type DoctorCheck added in v0.4.0

type DoctorCheck struct {
	Name    string            `json:"name" yaml:"name"`
	Status  DoctorCheckStatus `json:"status" yaml:"status"`
	Message string            `json:"message,omitempty" yaml:"message,omitempty"`
	Details []string          `json:"details,omitempty" yaml:"details,omitempty"`
	// Evidence is what this particular run of the check rests on. A check declares its
	// usual evidence in the registry; a run that could not look sets EvidenceUnknown
	// here, and one that looked harder than usual (tool-readiness under --probe) raises
	// it to EvidenceMeasured.
	Evidence Evidence `json:"evidence,omitempty" yaml:"evidence,omitempty"`
	// Fix is the magus command that remedies this finding, as argv without the leading
	// "magus" - nil when there is nothing mechanical to run. `doctor --fix` runs it; with
	// no --fix it is printed, so the report always names the cure even when it is not
	// applying it.
	//
	// An EXISTING first-class command, never a private repair routine. That is the whole
	// safety property: --fix can only do things you could have typed yourself and can
	// inspect afterwards, and a check whose remedy needs judgment (narrow this glob, or
	// accept the volatility?) simply declares no Fix and stays a report. It is also why a
	// config remedy is `config set ...` rather than a writer of its own - there is exactly
	// one thing in magus that edits config, and this is not a second one.
	Fix []string `json:"fix,omitempty" yaml:"fix,omitempty"`
}

DoctorCheck is one validation and what it found.

func (DoctorCheck) BuzzObject added in v0.4.0

func (v DoctorCheck) BuzzObject() BuzzObject

type DoctorCheckStatus added in v0.4.0

type DoctorCheckStatus string

DoctorCheckStatus is one check's outcome. Advice is deliberately not a failure: it is worth knowing and never a gate, which is the distinction the CI surface and the tool share one word for.

const (
	DoctorOK     DoctorCheckStatus = "ok"
	DoctorFail   DoctorCheckStatus = "fail"
	DoctorAdvice DoctorCheckStatus = "advice"
)

DoctorFail and DoctorAdvice are a deliberate split, and which one a check returns is a statement about whose judgment is involved.

DoctorFail is for a workspace that is WRONG in a way nobody's taste can rescue: a dependency cycle, a magusfile that will not parse, two targets claiming one output, a policy naming a target that does not exist. These are facts, they break the build or corrupt the cache, and failing on them is not an opinion.

DoctorAdvice is for a convention magus RECOMMENDS: how targets are named, whether every project binds a language spell, whether a spell target carries a doc comment. These are conventions that have worked well, documented so you can take them - not requirements, because magus does not get to decide how your repository is laid out. `ci` is the one reserved target, and everything past it is yours.

The distinction is not cosmetic. When doctor had only ok and fail, a convention check had two options: fail (and dictate) or not exist. What actually happened is that each one grew its own private escape hatch - no_language for language coverage, and briefly allow_bespoke_name for target naming - so the config surface grew one key per opinion, and taking magus's advice became mandatory unless you wrote a paragraph explaining yourself. Advice that exits zero needs no escape hatch at all.

There is deliberately no switch that promotes advice to failure. A knob for that would just be the imposition again with an opt-in label on it, and the workspace that wants a convention enforced can enforce it - in its own lint target, with its own tools, on its own terms. magus reports what it noticed and gets out of the way.

func (DoctorCheckStatus) String added in v0.4.0

func (v DoctorCheckStatus) String() string

String renders v for an error message: the value, or "unset" when empty.

func (DoctorCheckStatus) Valid added in v0.4.0

func (v DoctorCheckStatus) Valid() bool

Valid reports whether v is a declared DoctorCheckStatus. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (DoctorCheckStatus) Values added in v0.4.0

func (v DoctorCheckStatus) Values() []string

Values lists the DoctorCheckStatus values a caller may choose, excluding the zero value.

type DoctorReport added in v0.4.0

type DoctorReport struct {
	Workspace string        `json:"workspace" yaml:"workspace"`
	Checks    []DoctorCheck `json:"checks" yaml:"checks"`
	Summary   DoctorSummary `json:"summary" yaml:"summary"`
}

DoctorReport is the full doctor output: every check, and the counts.

func (DoctorReport) BuzzObject added in v0.4.0

func (v DoctorReport) BuzzObject() BuzzObject

type DoctorSummary added in v0.4.0

type DoctorSummary struct {
	OK     int `json:"ok" yaml:"ok" buzz:"ok"`
	Fail   int `json:"fail" yaml:"fail"`
	Advice int `json:"advice" yaml:"advice"`
	// Unknown counts checks that did not run, and is deliberately carved OUT of OK
	// rather than added beside it: "44 ok" that silently included six checks which
	// never looked was the number this field exists to stop reporting. It is not a
	// failure and does not affect the exit status - nothing was found to be wrong,
	// because nothing was looked at.
	Unknown int `json:"unknown" yaml:"unknown"`
}

DoctorSummary counts check outcomes.

OK carries a buzz tag because the mirror generator lowercases only the FIRST rune of a field name, which turns the initialism OK into `oK`. The tag is the sanctioned override (FileInfo.IsDir uses it the same way) and keeps the Go field idiomatic.

func (DoctorSummary) BuzzObject added in v0.4.0

func (v DoctorSummary) BuzzObject() BuzzObject

type DriftPolicy added in v0.4.0

type DriftPolicy string

DriftPolicy says what happens when a target's declared outputs move under a read-only run - the generated file that was never regenerated, and the reason `magus run generate` is a gate at all.

It is a policy about the RESPONSE, never about the diagnosis. magus always separates drift this change caused from drift that arrived with the base, because failing an author for bytes they did not move is a bug in the gate rather than a strictness setting; there is deliberately no way to spell that behavior. See ClassifyDrift.

The zero value gates, which is the point: a target that declares an output has already claimed those bytes are a function of its inputs, and checking a claim the workspace volunteered needs no second declaration. A tool whose correctness depends on every author remembering to switch it on has pushed its own conformance onto its users.

const (
	// DriftDefault gates every target that declares outputs, and no others. Written as
	// the empty string so an undeclared policy IS the default rather than resembling one.
	DriftDefault DriftPolicy = ""
	// DriftFail is DriftDefault stated out loud, for a target whose gating a reader
	// should not have to infer from the presence of an output glob.
	DriftFail DriftPolicy = "fail"
	// DriftWarn reports drift and never fails. The migration path: a workspace adopting
	// magus over a tree with existing drift can see the whole list before it has to be
	// green, which is the difference between adopting the gate and disabling it.
	DriftWarn DriftPolicy = "warn"
	// DriftOff does not check. Requires TargetPolicy.DriftReason.
	DriftOff DriftPolicy = "off"
)

func (DriftPolicy) Fails added in v0.4.0

func (d DriftPolicy) Fails() bool

Fails reports whether drift this change caused should fail the run rather than be reported. Only DriftWarn downgrades it.

func (DriftPolicy) Gates added in v0.4.0

func (d DriftPolicy) Gates(declaresOutputs bool) bool

Gates reports whether this policy checks at all. declaresOutputs carries the default's condition, so the caller does not restate it at each site.

type DriftResult added in v0.4.0

type DriftResult struct {
	// Drifted is false with every other field zero when the outputs are clean, so a
	// caller reads the fields unconditionally instead of testing for absent keys.
	Drifted bool
	// Code is the MGS diagnostic: which of the three causes this was.
	Code string
	// Message is the sentence to show, already naming the remedy.
	Message string
	// URL is Code's explainer page.
	URL string
	// Files are the drifted outputs, based at the repository root like Status.Files.
	Files []Path
}

DriftResult is why a generate gate's declared outputs drifted: not merely THAT they did, which a status call already answers, but which of three causes it was. A gate fires when its reader is looking at a CI log rather than the tree, so the verdict carries the diagnostic code, the sentence to print, its explainer URL, and the files.

It lives in types (not std) for the same reason Commit does: the shape crosses the Buzz boundary, so it needs a mirror. The FUNCTION that produces it belongs to the magus module rather than vcs - deciding that unchanged inputs plus a dev build means MGS4005 is magus policy, and vcs only supplies the dirty-file probe underneath it.

func (DriftResult) BuzzObject added in v0.4.0

func (d DriftResult) BuzzObject() BuzzObject

BuzzObject is the Buzz boundary map magus.diagnoseDrift returns.

type DriftResultRecord added in v0.4.0

type DriftResultRecord struct {
	Drifted bool
	Code    string
	Message string
	URL     string
	Files   []Path
}

DriftResultRecord is the boundary mirror cmd/magus-utils types reflects over; see CommitRecord for why it is separate from the type it mirrors.

func (DriftResultRecord) BuzzObject added in v0.4.0

func (v DriftResultRecord) BuzzObject() BuzzObject

type EdgeDirection added in v0.4.0

type EdgeDirection string

EdgeDirection says which end of an edge the focus node sits on. Distinct from types.Direction, the iota used for graph RENDERING order: this one is serialized, so it is string-backed and its values are what a reader sees.

const (
	// EdgeOut is the focus node as the edge's source; EdgeIn as its target.
	EdgeOut EdgeDirection = "out"
	EdgeIn  EdgeDirection = "in"
)

type EvaluatedProject added in v0.4.0

type EvaluatedProject struct {
	ProjectEntry
	ResolvedSpells []EvaluatedSpell  `json:"resolved_spells,omitempty" yaml:"resolved_spells,omitempty"`
	TargetPolicies map[string]Target `json:"target_policies,omitempty"  yaml:"target_policies,omitempty"`
}

EvaluatedProject is the fully-resolved view of a project: every ProjectEntry fact (Name and Spell included - embedding rather than restating them makes "evaluated = declared + resolution" a compile-time fact) plus the resolution fields resolving it adds. ResolvedSpells is deliberately not named Spells: that name means DECLARED spell names on ProjectEntry.Spells and would mean RESOLVED spell steps here - one field name for two different facts.

The embedded ProjectEntry's Sources/Outputs are populated with the RESOLVED, workspace-rooted globs (see the field comment on ProjectEntry.Sources), not the declared project-relative ones ListProjects reports - the one deliberate exception to "same values ListProjects builds" for the rest of the embed.

func (EvaluatedProject) BuzzObject added in v0.4.0

func (p EvaluatedProject) BuzzObject() BuzzObject

BuzzObject is the Buzz boundary map for one evaluated project. Written explicitly rather than left to the BuzzObject ProjectEntry promotes: an embedded ProjectEntry's BuzzObject is promoted onto EvaluatedProject too, which would satisfy host's boundary view (host/helpers.go) while emitting only the declared half and silently dropping ResolvedSpells/TargetPolicies. EvaluatedProject is not on the Buzz mirror allowlist and no std/ host method returns it today, so nothing calls this yet - it exists to keep that latent trap from going live if one ever does. Neither EvaluatedSpell nor Target (whose zero value serves double duty as a per-target policy - see Target's identity-fields comment) has its own BuzzObject, so their fields are read directly rather than through a promoted-in-the-same-way call.

type EvaluatedProjectsOutput

type EvaluatedProjectsOutput struct {
	Definition string             `json:"definition" yaml:"definition"`
	Workspace  string             `json:"workspace"  yaml:"workspace"`
	Count      int                `json:"count"      yaml:"count"`
	Projects   []EvaluatedProject `json:"projects"   yaml:"projects"`
}

EvaluatedProjectsOutput is the top-level result for "describe projects --evaluated".

type EvaluatedSpell added in v0.4.0

type EvaluatedSpell struct {
	Name          string   `json:"name"                        yaml:"name"`
	TargetSources []string `json:"target_sources,omitempty"    yaml:"target_sources,omitempty"`
	// Command is the fork command this spell's op would run for the target, with
	// the requested charms applied (cmd as element 0). Empty for function-op or
	// no-op targets, whose argv isn't statically knowable. Preview only: `magus
	// describe` renders it; nothing is executed.
	Command []string `json:"command,omitempty"           yaml:"command,omitempty"`
	// CharmTrace is the step-by-step application of the active charms over this
	// spell's base argv: element 0 is the base command (no charms), and each
	// subsequent step is the command after one more charm's patch, in the
	// deterministic sorted-name order magus applies them. Populated only when
	// charms are active and change the command; the RFC 6902 patch made legible by
	// `magus describe target ...:charm --explain`.
	CharmTrace []spells.CharmTraceStep `json:"charm_trace,omitempty"       yaml:"charm_trace,omitempty"`
	// Conflicts lists the active charms whose edit is overridden by another active
	// charm on this command (both edit the same argument; the winner is decided by
	// sorted charm name, so the loser has no effect). Empty when the active charms
	// have disjoint edits. `magus describe target ...:a,b` surfaces it before a run.
	Conflicts []spells.CharmConflict `json:"conflicts,omitempty"         yaml:"conflicts,omitempty"`
	// Service is set only when this spell's op is a service (a long-running process
	// magus supervises rather than runs to completion). It carries the static, pre-run
	// facts; Command above is the process itself. Nil for an ordinary command op.
	Service *spells.ServiceView `json:"service,omitempty" yaml:"service,omitempty"`
}

EvaluatedSpell is one spell's contribution to an evaluated target.

type EvaluatedTarget added in v0.4.0

type EvaluatedTarget struct {
	Project string   `json:"project"             yaml:"project"`
	Target  string   `json:"target"              yaml:"target"`
	Dir     string   `json:"dir"                 yaml:"dir"`
	Sources []string `json:"sources,omitempty"    yaml:"sources,omitempty"`
	Outputs []string `json:"outputs,omitempty"    yaml:"outputs,omitempty"`
	// Chain is the targets this one composes, in invocation order; empty when it
	// composes nothing. See TargetGraphNode.Chain, which it is copied from.
	Chain     []ChainStep      `json:"chain,omitempty"      yaml:"chain,omitempty"`
	DependsOn []string         `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
	Charms    []string         `json:"charms,omitempty"     yaml:"charms,omitempty"`
	Spells    []EvaluatedSpell `json:"spells,omitempty"     yaml:"spells,omitempty"`
	Policy    *Target          `json:"policy,omitempty"    yaml:"policy,omitempty"` // only the policy fields of Target are meaningful (SkipCache/Exclusive/Drift/RetryOnVolatile)
	Exclusive bool             `json:"exclusive,omitempty" yaml:"exclusive,omitempty"`
}

EvaluatedTarget is the fully-resolved view of a single path:target pair.

type EvaluatedTargetReport added in v0.4.0

type EvaluatedTargetReport struct {
	Definition string            `json:"definition" yaml:"definition"`
	Count      int               `json:"count"      yaml:"count"`
	Targets    []EvaluatedTarget `json:"targets"    yaml:"targets"`
}

EvaluatedTargetReport is the "describe target <path:target>" envelope.

type Event added in v0.4.0

type Event struct {
	SchemaVersion int            `json:"schema_version"`
	Outcome       EventOutcome   `json:"outcome"`
	Severity      EventSeverity  `json:"severity"`
	Source        EventSource    `json:"source"`
	Where         *EventLocation `json:"where,omitempty"`
	Message       string         `json:"message"`
}

Event is the canonical record something emits when it needs a human's attention. It is shared across magus surfaces - the notify CLI, the activity trail, doctor reports, self-update notices, hints - so every consumer reasons over the same shape.

The fields are categorical, one piece of information each:

  • Outcome: the SITUATION class (waiting, permission, failed, ...)
  • Severity: the URGENCY tier (info, notice, warning, critical)
  • Source: who/what produced this (agent/claude-code, magus/doctor, ...)
  • Where: actionable context (workspace, project, files)
  • Message: the free-form detail

The categorical split mirrors HTTP status classes (1xx/2xx/3xx/4xx/5xx) and Unix exit codes (0/1/2/126/127/128+N): one field carries the high-level class, another carries the specific code within it. The renderer can reason over each axis independently - title from outcome, icon from severity, deep-link from source+where.

logger levels are deliberately NOT a part of this type. slog levels filter output (verbosity gates); event severities drive renderer behavior (icon, priority, action). Same names overlap (info, warning) by coincidence, but the vocabularies serve different purposes. When a call site needs to bridge (a log entry becomes an event), the mapping lives inline at that site.

type EventLocation added in v0.4.0

type EventLocation struct {
	Workspace Path        `json:"workspace"`
	Project   *ProjectRef `json:"project,omitempty"`
	Files     []FileRef   `json:"files,omitempty"`
}

EventLocation is the actionable context - WHERE did this happen? Every field is optional; the renderer uses what is present.

Workspace is the repo root (always a directory). Project names the workspace-relative project the event pertains to, when narrower than the whole workspace. Files names the relevant files - "the file X had a problem" reads differently from "the directory Y had a problem", and Path.IsDir carries that distinction.

type EventOutcome added in v0.4.0

type EventOutcome string

EventOutcome names the situation class - WHAT KIND of attention is needed. Seven canonical values; an unrecognized outcome falls through to other.

const (
	// OutcomeWaiting: blocked on input (agent idle, interactive prompt).
	OutcomeWaiting EventOutcome = "waiting"
	// OutcomePermission: blocked on approval (gated operation, host permission prompt).
	OutcomePermission EventOutcome = "permission"
	// OutcomeFailed: exited non-zero or errored (run, affected ci, spell op).
	OutcomeFailed EventOutcome = "failed"
	// OutcomeFinished: completed successfully (rare for notifications; success is usually silent).
	OutcomeFinished EventOutcome = "finished"
	// OutcomeDiagnostic: a workspace problem was detected (doctor, merge-driver conflict).
	OutcomeDiagnostic EventOutcome = "diagnostic"
	// OutcomeUpdate: a new release or upgrade is available (self update).
	OutcomeUpdate EventOutcome = "update"
	// OutcomeOther: catch-all for situations outside the canonical set.
	OutcomeOther EventOutcome = "other"
)

func (EventOutcome) String added in v0.4.0

func (v EventOutcome) String() string

String renders v for an error message: the value, or "unset" when empty.

func (EventOutcome) Valid added in v0.4.0

func (v EventOutcome) Valid() bool

Valid reports whether v is a declared EventOutcome. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (EventOutcome) Values added in v0.4.0

func (v EventOutcome) Values() []string

Values lists the EventOutcome values a caller may choose, excluding the zero value.

type EventSeverity added in v0.4.0

type EventSeverity string

EventSeverity names the urgency tier - HOW URGENT is the attention. Four canonical tiers; aligned by name with slog's info/warning (those overlap intentionally) and adding notice (between info and warning) and critical (above error) which have no slog equivalent.

const (
	// SeverityInfo: FYI, non-urgent. Same word as slog.LevelInfo; different intent.
	SeverityInfo EventSeverity = "info"
	// SeverityNotice: saw something, not actionable yet. No slog equivalent.
	SeverityNotice EventSeverity = "notice"
	// SeverityWarning: action likely needed. Same word as slog.LevelWarn; different intent.
	SeverityWarning EventSeverity = "warning"
	// SeverityCritical: action required. Above slog.LevelError in urgency.
	SeverityCritical EventSeverity = "critical"
)

func (EventSeverity) String added in v0.4.0

func (v EventSeverity) String() string

String renders v for an error message: the value, or "unset" when empty.

func (EventSeverity) Valid added in v0.4.0

func (v EventSeverity) Valid() bool

Valid reports whether v is a declared EventSeverity. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (EventSeverity) Values added in v0.4.0

func (v EventSeverity) Values() []string

Values lists the EventSeverity values a caller may choose, excluding the zero value.

type EventSource added in v0.4.0

type EventSource struct {
	// Kind is the producer category. Producers set this; consumers match on it.
	Kind string `json:"kind"`
	// Sub is the component within Kind. Empty when there is no meaningful component.
	Sub string `json:"sub,omitempty"`
	// ID: opaque instance identity (session id, run id, process id). Empty
	// when the producer does not have one.
	ID string `json:"id,omitempty"`
}

EventSource identifies who or what produced the event. Kind names the broad producer category; Sub names the producer's local component. ID is opaque instance identity (a session id, a run id, a process id) when available.

type Evidence added in v0.4.0

type Evidence string

Evidence says what a verdict RESTS ON, which is a different question from what the verdict is.

magus already draws this distinction in one place and it is the best behavior in the tool: `magus refs` answering "not indexed" rather than "no matches", because those are different facts and collapsing them turns an absence of knowledge into a claim. Everywhere else a verdict arrived bare, so a check that could not run rendered identically to one that ran and found nothing wrong.

A reader deciding whether to trust an `ok` needs this more than they need the `ok`.

const (
	EvidenceMeasured Evidence = "measured"
	EvidenceDeclared Evidence = "declared"
	EvidenceInferred Evidence = "inferred"
	EvidenceUnknown  Evidence = "unknown"
)

The four are ordered by how much they entitle you to believe.

EvidenceMeasured means magus observed the actual state: it walked the tree, dialed the socket, parsed the file, ran the probe. EvidenceDeclared means magus read what the workspace ASSERTS - a magusfile declaration, a config key - and took it at its word without confirming it, so the finding is only as true as the declaration. Those two differ exactly where it matters: a declared memory_mb and a measured peak disagreeing is what checkMemoryDeclarations exists to report.

EvidenceInferred means the answer came from a derived model - the knowledge graph, static extraction, a near-miss heuristic - which can be stale, partial, or wrong in ways the source it models is not.

EvidenceUnknown means the check did not run. It is not a pass. A skipped check rendering as ok is the single most common way a green report has lied here.

type ExecResult

type ExecResult struct {
	Stdout string
	Stderr string
	Code   int
	OK     bool `buzz:"ok"`
}

ExecResult is the serializable {stdout, stderr, code, ok} shape every magus exec surface returns (proc.exec, magus.cmd, a captured spell op); ok is code == 0. It is the boundary mirror of the richer internal run.ExecResult.

The Buzz `object ExecResult` mirror is generated from this struct by cmd/magus-utils types (go:generate); keep them in lockstep through the generator.

func (ExecResult) BuzzObject added in v0.4.0

func (v ExecResult) BuzzObject() BuzzObject

type ExitError

type ExitError struct {
	Code int
}

ExitError aborts the current magus run with a specific process exit code, raised by os.exit(code) from a magusfile and propagated up like any other target error.

It deliberately does NOT call os.Exit: a target can run inside a long-lived daemon serving multiple workspaces (see internal/proc), where os.Exit would kill unrelated in-flight work. Instead the CLI maps this error to its process exit status, and the daemon to the per-run reply code.

func (ExitError) Error

func (e ExitError) Error() string

Error reports the exit code. The message is incidental: the CLI/daemon recover ExitError via errors.As and use Code, not the string.

type FileChange added in v0.4.0

type FileChange struct {
	Path     string
	PrevPath string
	Status   ChangeStatus
}

FileChange is one path a commit touched. Path is the name AFTER the commit; PrevPath is set only on a rename and carries the name before it, which is the edge a reader follows to reassemble a file's lineage.

type FileClaim added in v0.4.0

type FileClaim struct {
	Project string `json:"project" yaml:"project"`
	// Target is empty for a project-wide or spell-supplied glob, which every
	// target of that project carries.
	Target string `json:"target,omitempty" yaml:"target,omitempty"`
	// Role is "output" (ctx.writesFiles or a project/spell output glob), "source"
	// (ctx.readsFiles or a project/spell source glob), or "update"
	// (ctx.modifiesExistingFiles). The first two are also FileEntry.Role values;
	// "update" has no FileEntry.Role counterpart - see FileEntry.Claims.
	Role string `json:"role" yaml:"role"`
	Glob string `json:"glob" yaml:"glob"`
	// Paths is populated only on FileReport.Overlaps, where one claim is reported
	// once for the whole request and lists every path in it that the declaration
	// covers. It is empty on a FileEntry's own claims, where the path is the
	// entry's.
	Paths []string `json:"paths,omitempty" yaml:"paths,omitempty"`
}

FileClaim is one declaration that names a path: the project whose magusfile declared it, the target that did, and the workspace-rooted glob that matched.

A cross-project write is attributed to the DECLARING project, not the tree it lands in - the opposite of FileEntry.OutputOf, which follows Project.AllOutputs and counts it on the owner. Both are true and neither is redundant: the owner says whose tree the file appears in, the declarer says whose target puts it there, and only the second one can regenerate it.

func (FileClaim) BuzzObject added in v0.4.0

func (v FileClaim) BuzzObject() BuzzObject

type FileEntry added in v0.2.0

type FileEntry struct {
	Path string `json:"path" yaml:"path"`
	// Project is the owning project by directory containment (longest project
	// path prefixing the file), empty when no project dir contains it.
	Project string `json:"project,omitempty" yaml:"project,omitempty"`
	// Role summarizes the strongest claim: "output" (a declared output glob
	// matches - the file is generated), "source" (a declared source glob
	// matches), "maintained" (no project declares it, but magus's own core writes
	// it - see IsMagusMaintained), or "unclaimed" (no declared glob matches; it
	// invalidates no cache key and affects no target). An unclaimed path may
	// still carry Claims: an in-place edit is a declaration none of these roles
	// rank, and the Hint says so when that is what happened.
	//
	// maintained is a REFINEMENT of unclaimed, not a rank above source: both are
	// invisible to the cache and the affected set. It is separate because the
	// handling rule inverts. An unclaimed path may be residue to ignore, so its
	// hint says to check the ignore rules; a maintained path is one magus wrote
	// and expects committed, and telling someone to consider ignoring it is
	// advice to drop magus's own bookkeeping.
	Role string `json:"role" yaml:"role"`
	// OutputOf and SourceOf list the projects whose declared output/source globs
	// match the path. A path can be both (a committed generated file is often a
	// source of downstream targets); Role reports output in that case because
	// the regeneration rule dominates how the file should be treated.
	OutputOf []string `json:"output_of,omitempty" yaml:"output_of,omitempty"`
	SourceOf []string `json:"source_of,omitempty" yaml:"source_of,omitempty"`
	// Claims are the individual declarations that name the path, each with the
	// target that made it and the glob that matched. OutputOf/SourceOf are the
	// project-level summary of the same facts; the declaration is the finer unit,
	// and it is the one that answers "which target rewrites this" for a caller
	// splitting work across agents.
	//
	// The set is wider than Role ranks: it also carries the in-place edits of
	// ctx.modifiesExistingFiles ("update"), which is a write nobody replays or
	// cleans, so Role deliberately leaves such a file reading as its source or
	// unclaimed self.
	Claims []FileClaim `json:"claims,omitempty" yaml:"claims,omitempty"`
	// DependsOn is the owning project's DIRECT declared dependencies, verbatim
	// from Project.DependsOn - carried here so one classification call answers
	// both "who owns this path" and "what does that owner run behind". It is not
	// the transitive closure; `magus graph deps` computes that.
	DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
	// Hint is the one-line handling rule for the role, ready to surface to a
	// human or an agent.
	Hint string `json:"hint,omitempty" yaml:"hint,omitempty"`
	// Exists reports whether the path is present on disk.
	//
	// Classification is pure glob matching, which is what makes "where would a new
	// file land" answerable - so it cannot be an error for a path that is not there
	// yet. Without this field it is also not DISTINGUISHABLE: a mistyped or invented
	// path came back byte-identical to the real one beside it, declared and owned.
	//
	// Never omitempty: false is the whole signal, and a field that vanishes when it
	// is the interesting value is the one shape this cannot take.
	Exists bool `json:"exists" yaml:"exists"`
}

FileEntry classifies one workspace-relative path.

func (FileEntry) BuzzObject added in v0.4.0

func (v FileEntry) BuzzObject() BuzzObject

type FileHotspot

type FileHotspot struct {
	Path       string    `json:"path"                  yaml:"path"`
	Commits    int       `json:"commits"               yaml:"commits"`
	Complexity int       `json:"complexity"            yaml:"complexity"`
	Score      int       `json:"score"                 yaml:"score"` // commits × complexity
	Authors    int       `json:"authors"               yaml:"authors"`
	LastCommit time.Time `json:"last_commit,omitempty" yaml:"last_commit,omitempty"`
	// Moves is how many times the file changed path inside the window: the count of
	// distinct names its history folded in, minus the one it ends under. A file that
	// keeps changing address is churning architecturally rather than just textually,
	// which is a different thing to know than its edit count and is not derivable
	// from Path alone.
	Moves int `json:"moves,omitempty" yaml:"moves,omitempty"`
}

FileHotspot is one file's hotspot score: edit frequency weighted by complexity.

func (FileHotspot) BuzzObject added in v0.4.0

func (v FileHotspot) BuzzObject() BuzzObject

type FileInfo

type FileInfo struct {
	Size  int64
	Mtime float64
	Mode  int64
	IsDir bool `buzz:"is_dir"`
}

FileInfo mirrors fs.stat's {size, mtime, mode, is_dir} object: size in bytes, mtime as Unix milliseconds, mode as the integer permission bits.

func (FileInfo) BuzzObject added in v0.4.0

func (v FileInfo) BuzzObject() BuzzObject

type FileRef added in v0.4.0

type FileRef struct {
	Value string `json:"value"`
	IsDir bool   `json:"is_dir,omitempty"`
}

FileRef is one file path with its kind. Mirrors Path but lives on the event record so the encoder does not need to reach into the types.Path API for every consumer; Path.Value + Path.IsDir flattened to two named fields reads more clearly at the call site.

When a renderer wants to display "the file X had a problem" vs "the directory Y had a problem", FileRef.IsDir tells it which.

func FileRefFromPath added in v0.4.0

func FileRefFromPath(p Path) FileRef

FileRefFromPath is the inverse of PathFromRef, for producers that have a types.Path in hand and need to emit a FileRef.

Resolve first: a FileRef has nowhere to put a base, so emitting p.Value raw would ship a relative path stripped of what it was relative to - readable, and wrong.

func (FileRef) PathFromRef added in v0.4.0

func (r FileRef) PathFromRef() Path

PathFromRef builds a types.Path from a FileRef, so consumers that already work with Path can move between the two without a helper at every site.

Field-by-field, not a struct conversion. The two types happened to share a layout, so Path(r) compiled - and that made every future field on Path a silent compile break here, which is exactly what adding Path.Base did. A FileRef is an event payload naming a file; a Path is a lexical reference measured from a base. They are not the same idea and should not be coupled by their field order.

Base is empty: a FileRef carries no base, and inventing one (the workspace root, say) would assert something the event never said.

type FileReport added in v0.4.0

type FileReport struct {
	Definition string      `json:"definition" yaml:"definition"`
	Count      int         `json:"count"      yaml:"count"`
	Files      []FileEntry `json:"files"      yaml:"files"`
	// Overlaps are the declarations that cover MORE THAN ONE of the classified
	// paths, each listing the paths it covers. Grouped by declaration rather than
	// emitted per pair: a hundred paths under one glob is a hundred rows here and
	// five thousand as pairs, and the declaration is the shared thing anyway.
	//
	// A fact, not a verdict. Two paths under one glob mean one target rewrites
	// both, which may be a collision between two authors or may be exactly what a
	// single author intends; nothing here decides which.
	Overlaps []FileClaim `json:"overlaps,omitempty" yaml:"overlaps,omitempty"`
}

FileReport is the "describe file <path>..." envelope.

func NewFileReport added in v0.4.0

func NewFileReport(files []FileEntry) FileReport

NewFileReport wraps a classification in the wire envelope. A constructor rather than a literal at each render edge, because Overlaps is derived from files and two call sites (the CLI and the MCP tool) building it by hand is the same forgotten-line hazard the Count field above already documents.

func (FileReport) BuzzObject added in v0.4.0

func (v FileReport) BuzzObject() BuzzObject

type Graph

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

Graph is the project dependency DAG; cycles are caught at construction. The DepGraphRepository it wraps (the query engine) lives in repository.go.

func NewGraph

func NewGraph(repo DepGraphRepository, projects map[string]*Project) *Graph

NewGraph constructs a Graph from a repository and a project map.

func (*Graph) BlastRadius

func (g *Graph) BlastRadius() map[string]int

BlastRadius returns a map from project path to the count of projects affected by a change.

func (*Graph) NCCD

func (g *Graph) NCCD() float64

NCCD returns the Normalized Cumulative Component Dependency: the graph's CCD over that of a balanced binary tree of the same size (>1 means more coupling than a balanced tree). Named to match DepGraphRepository.NCCD() and the internal engines rather than spelling the acronym out only on this wrapper.

func (*Graph) NearCycles

func (g *Graph) NearCycles(ctx context.Context, maxDepth int) []NearCycle

NearCycles returns pairs where adding From→To would create a cycle of length ≤ maxDepth. depth=0 disables the check. Partial results on ctx cancellation.

func (*Graph) Nodes

func (g *Graph) Nodes() []string

func (*Graph) PathsFromSeeds

func (g *Graph) PathsFromSeeds(seeds []string, target string) []AffectedPath

PathsFromSeeds returns the shortest chain from each seed to target.

func (*Graph) Predecessors

func (g *Graph) Predecessors(path string) []string

func (*Graph) Project

func (g *Graph) Project(path string) *Project

func (*Graph) ReverseClosure

func (g *Graph) ReverseClosure(seeds []string) []string

ReverseClosure returns every project that transitively depends on any seed (seeds included).

func (*Graph) Successors

func (g *Graph) Successors(path string) []string

func (*Graph) TopoSort

func (g *Graph) TopoSort() []string

TopoSort returns project paths in topological order (dependents before dependencies - see View's doc comment for the measured example).

func (*Graph) View added in v0.4.0

func (g *Graph) View() GraphView

View flattens the graph into the boundary object above.

Edges in this graph run DEPENDENT -> DEPENDENCY, so Successors(web) is what web depends on and TopoSort yields dependents before dependencies. A caller wanting a build order needs the reverse, which is what Nodes gives. (Measured, not assumed: for web depending on api, TopoSort returns [. web api] and Successors(web) is [api].)

type GraphConfig

type GraphConfig struct {
	Obs Observer
}

GraphConfig carries configuration for a graph build.

func NewGraphConfig

func NewGraphConfig(initial Observer) *GraphConfig

NewGraphConfig returns a GraphConfig seeded with an initial observer.

type GraphOption

type GraphOption func(*GraphConfig)

GraphOption configures a call to dependency.Build (or (*Magus).Graph).

func WithGraphObserver

func WithGraphObserver(o Observer) GraphOption

WithGraphObserver attaches an observer; multiple calls compose via FanOut.

type GraphOutput

type GraphOutput struct {
	Direction string   `json:"direction" yaml:"direction"`
	SpellName string   `json:"spell_name,omitempty" yaml:"spell_name,omitempty"`
	Roots     []string `json:"roots,omitempty" yaml:"roots,omitempty"`
	Nodes     []Node   `json:"nodes" yaml:"nodes"`
}

GraphOutput is the full structured graph for JSON/YAML serialization or rendering. Named to sit alongside the other *Output result types (e.g. StatusOutput, TargetGraphOutput) rather than the bare, ungrounded Output.

type GraphView added in v0.4.0

type GraphView struct {
	Nodes     []string
	DependsOn map[string][]string
	// BlastRadius is how many projects each node can affect, transitively. It is
	// the number a magusfile branches on to decide whether a change is safe to
	// batch, and it is already computed by the graph.
	BlastRadius map[string]int
}

GraphView is the boundary object magus.graph returns: the project dependency DAG flattened to plain data a magusfile can walk.

Nodes are in topological order, so a caller that just iterates gets a valid build order without sorting anything itself - which is the question a magusfile asks the graph most often. dependsOn is the direct-predecessor set per node, so the caller can still reconstruct the edges.

func (GraphView) BuzzObject added in v0.4.0

func (v GraphView) BuzzObject() BuzzObject

type HTTPResponse

type HTTPResponse struct {
	Status  int
	Body    string
	Headers map[string]string
}

HTTPResponse mirrors http.get/post/request's {status, body, headers} object. headers maps each response header name to its first value.

func (HTTPResponse) BuzzObject added in v0.4.0

func (v HTTPResponse) BuzzObject() BuzzObject

type HTTPRetry added in v0.4.0

type HTTPRetry struct {
	// Attempts is the TOTAL number of tries, not the number of retries after the
	// first. 0 and 1 both mean "run it once"; 3 means one attempt plus two
	// retries. Counting total attempts rather than curl's --retry N avoids the
	// off-by-one that spelling invites.
	Attempts int
	// DelayMs is the pause before the second attempt, in milliseconds. It doubles
	// per attempt unless Fixed is set. Zero uses a 500ms default whenever
	// Attempts is greater than 1.
	DelayMs float64 `buzz:"delay_ms"`
	// MaxDelayMs caps the exponential growth, so a long backoff cannot stretch a
	// single request past anything useful. Zero means uncapped.
	MaxDelayMs float64 `buzz:"max_delay_ms"`
	// MaxElapsedMs is a wall-clock ceiling across all attempts including the
	// waits. It is the honest way to bound retrying: a count alone says nothing
	// about how long a build will sit there. Zero means no ceiling beyond the
	// per-request timeout.
	MaxElapsedMs float64 `buzz:"max_elapsed_ms"`
	// Fixed keeps the delay constant instead of doubling it, matching curl's
	// --retry-delay. Use it when an endpoint documents a fixed cooldown.
	Fixed bool
	// AllErrors retries every failure, including 4xx statuses that normally mean
	// the request itself is wrong. Off by default because retrying a 401 or a 404
	// cannot succeed and only delays the report.
	AllErrors bool `buzz:"all_errors"`
	// ConnRefused treats a refused connection as retryable, which it is not by
	// default. Set it when waiting for a service that is still coming up.
	ConnRefused bool `buzz:"conn_refused"`
}

HTTPRetry is the retry policy an http\get / post / request / download call applies. It mirrors the Buzz `HttpRetry` object.

WHY IT IS A DECLARED OBJECT and not five keys in the opts map: retrying is the setting most likely to be wrong in a way nothing reports. A misspelled "retry_delayy" in an untyped map is silently ignored, and the only symptom is a build that hammers a flaky endpoint at the wrong cadence - or does not retry at all when the author believed it would. As a declared object the checker catches the typo at load.

WHY THE ZERO VALUE MEANS NO RETRYING: an omitted policy has to be the safe reading, and for a build tool the safe reading is "run the request once". magus retried three times by default for a long while, which is wrong in two directions at once - it silently triples the cost of a genuinely failing request (and the wait before the failure is reported), and it can mask a real outage long enough that a build looks merely slow. Retrying is a decision about a specific endpoint's behavior, so the caller states it.

type HotspotOutput

type HotspotOutput struct {
	Definition string        `json:"definition" yaml:"definition"`
	Commits    int           `json:"commits"    yaml:"commits"`
	Since      string        `json:"since,omitempty" yaml:"since,omitempty"`
	Nodes      []Node        `json:"nodes"      yaml:"nodes"`
	Files      []FileHotspot `json:"files,omitempty" yaml:"files,omitempty"`
}

HotspotOutput ranks where churn meets complexity — the canonical "fix this first" view. Nodes is the project-level heatmap (reusing the dependency-graph nodes, with churn/authors/recency/blast-radius/CI-duration); Files is the per-file ranking.

func (HotspotOutput) BuzzObject added in v0.4.0

func (v HotspotOutput) BuzzObject() BuzzObject

type IgnorePattern

type IgnorePattern struct {
	Type    PatternType `json:"type" yaml:"type" validate:"required,oneof=glob regex literal"`
	Pattern string      `json:"pattern" yaml:"pattern" validate:"required"`
}

IgnorePattern is one watch ignore rule ("glob", "regex", or "literal").

type IgnoredFileReporter added in v0.4.0

type IgnoredFileReporter interface {
	// IgnoredFiles returns the subset of paths the VCS ignores, as given. Paths are
	// interpreted relative to dir. An empty paths slice returns no results.
	IgnoredFiles(ctx context.Context, dir string, paths []string) ([]string, error)
}

IgnoredFileReporter is an optional capability (sibling of TrackedFileReporter) for VCSDriver implementations that can report which paths the VCS ignores.

Different from TrackedFiles, and the difference is the point: "untracked" lumps a file nobody has committed YET together with one nothing should ever commit, and only the ignore rules distinguish them. A derived artifact that must be reproducible from a clean checkout filters on IGNORED, or it drops work in progress.

Callers type-assert and skip the question when a backend lacks it, so treating an unknown answer as "not ignored" keeps such a backend behaving as before.

type ImpactCoverage added in v0.4.0

type ImpactCoverage struct {
	Ratio   float64 `json:"ratio"         yaml:"ratio"`
	Covered int     `json:"covered_stmts" yaml:"covered_stmts"`
	Total   int     `json:"total_stmts"   yaml:"total_stmts"`
}

ImpactCoverage is a covered/total statement tally and its ratio (0..1), mirrored from the knowledge graph's @coverage overlay so the impact report carries the raw counts.

func (ImpactCoverage) BuzzObject added in v0.4.0

func (v ImpactCoverage) BuzzObject() BuzzObject

type ImpactFileCoverage added in v0.4.0

type ImpactFileCoverage struct {
	File     string         `json:"file"     yaml:"file"`
	Coverage ImpactCoverage `json:"coverage" yaml:"coverage"`
}

ImpactFileCoverage is one changed file's observed file-level coverage.

func (ImpactFileCoverage) BuzzObject added in v0.4.0

func (v ImpactFileCoverage) BuzzObject() BuzzObject

type ImpactProject added in v0.4.0

type ImpactProject struct {
	Path string `json:"path" yaml:"path"`
	// Seed is true when a changed file lands directly in this project (it is a root
	// of the closure, not only reached transitively).
	Seed bool `json:"seed" yaml:"seed"`
	// Files are the changed files inside this project, present only for seeds.
	Files []string `json:"files,omitempty" yaml:"files,omitempty"`
	// UndeclaredFiles is the subset of Files that no project declares: they seeded this
	// project by directory containment alone, so they rerun its targets without moving
	// a cache key. See types.AffectedResult.UndeclaredBySeed and MGS1028.
	UndeclaredFiles []string `json:"undeclared_files,omitempty" yaml:"undeclared_files,omitempty"`
	// Spells are the project's bound spells (its toolchains).
	Spells []string `json:"spells,omitempty" yaml:"spells,omitempty"`
	// Targets is the project's target vocabulary: the spell-contributed ops plus any
	// custom magusfile targets that name it, sorted and deduplicated.
	Targets []string `json:"targets,omitempty" yaml:"targets,omitempty"`
}

ImpactProject is one project in the blast radius.

func (ImpactProject) BuzzObject added in v0.4.0

func (v ImpactProject) BuzzObject() BuzzObject

type ImpactResult added in v0.4.0

type ImpactResult struct {
	// Base is the ref the VCS diff was taken against ("paths" when computed from an
	// explicit path set rather than a diff).
	Base string `json:"base" yaml:"base"`
	// ChangedFileCount and ChangedFiles are the full changed-file set. Files outside
	// any project still count here (they just seed nothing).
	ChangedFileCount int      `json:"changed_file_count"      yaml:"changed_file_count"`
	ChangedFiles     []string `json:"changed_files,omitempty" yaml:"changed_files,omitempty"`
	// SeedProjects are the projects that directly contain a changed file, sorted.
	SeedProjects []string `json:"seed_projects,omitempty" yaml:"seed_projects,omitempty"`
	// AffectedProjects is the transitive reverse closure of the seeds (seeds
	// included), sorted by path. Each carries its target vocabulary and whether a
	// changed file lands in it directly.
	AffectedProjects []ImpactProject `json:"affected_projects,omitempty" yaml:"affected_projects,omitempty"`
	// ChangedSymbols is the changed-symbol caller overlay: every symbol defined in a
	// changed source file, with how widely it is referenced repo-wide. It is what a
	// plain difftool structurally cannot show - the reach of an edited definition.
	// Populated by Enrich when a symbol index is loaded; empty (with a Note) otherwise.
	// Flattened across files and sorted by descending reference count so the
	// widest-reach change leads.
	ChangedSymbols []ImpactSymbol `json:"changed_symbols,omitempty" yaml:"changed_symbols,omitempty"`
	// ChangedFileCoverage is the coverage overlay: the observed statement coverage of
	// each changed file the local coverage profile covers. Empty (with a Note) when no
	// `magus run coverage` profile is loaded. Go-only and observed, never extracted.
	ChangedFileCoverage []ImpactFileCoverage `json:"changed_file_coverage,omitempty" yaml:"changed_file_coverage,omitempty"`
	// Notes carries graceful-degradation messages (deferred overlays, missing data).
	// It never blocks a report; a formatter prints it verbatim.
	Notes []string `json:"notes,omitempty" yaml:"notes,omitempty"`
}

ImpactResult is the typed impact report for a changeset. Counts sit alongside the backing lists so a formatter can lead with the count and expand the detail (the `magus graph explain` house style).

func (ImpactResult) BuzzObject added in v0.4.0

func (v ImpactResult) BuzzObject() BuzzObject

type ImpactSymbol added in v0.4.0

type ImpactSymbol struct {
	File      string         `json:"file"       yaml:"file"`
	Symbol    string         `json:"symbol"     yaml:"symbol"`
	Label     string         `json:"label,omitempty" yaml:"label,omitempty"`
	RefCount  int            `json:"ref_count"  yaml:"ref_count"`
	FileCount int            `json:"file_count" yaml:"file_count"`
	Coverage  ImpactCoverage `json:"coverage"   yaml:"coverage"`
}

ImpactSymbol is one changed symbol's caller spread (and coverage, when observed): the file that defines it, its identity, how many references and distinct referencing files the symbol index recorded, and its covered-statement ratio if a coverage profile is loaded.

Coverage is a VALUE, not a pointer, so a caller reads sym.coverage.ratio without a nil guard and the Buzz mirror declares it non-optional. Total == 0 is what "no coverage was observed" means: a symbol the profile covers always has statements to count.

func (ImpactSymbol) BuzzObject added in v0.4.0

func (v ImpactSymbol) BuzzObject() BuzzObject

type InputRef added in v0.3.0

type InputRef struct {
	Project string `json:"project,omitempty" yaml:"project,omitempty"`
	Glob    string `json:"glob" yaml:"glob"`
}

InputRef names one file input a target declares via ctx.readsFiles, in a single shape that carries the owning project for both a same-project glob and a cross-project file - maximally explicit: a local input's project is simply itself. Project is the owning project's path; Glob is the doublestar glob (or exact file) relative to that root. For a same-project input (ctx.readsFiles("glob")) Project is empty at extraction, meaning "this target's own project", and is filled to the project's path when resolved. For a cross-project input (ctx.readsFiles(<alias>.file("rel"))) Project names the imported project (dot-/repo-relative as written in the magusfile until resolved to workspace-relative, mirroring CrossTargetRef). Folding into the cache key, the affected-tracking depends_on edge, and the consumes edge all read this one shape.

func (InputRef) BuzzObject added in v0.4.0

func (v InputRef) BuzzObject() BuzzObject

type InsightAnalyzer added in v0.4.0

type InsightAnalyzer interface {
	Hotspots(ctx context.Context, opts InsightOptions) (HotspotOutput, error)
	Affinity(ctx context.Context, opts InsightOptions) (AffinityOutput, error)
	Ownership(ctx context.Context, opts InsightOptions) (OwnershipOutput, error)
	Trend(ctx context.Context, opts InsightOptions) (TrendOutput, error)
	Volatility(ctx context.Context) (VolatilityReport, error)
	Unreferenced(ctx context.Context) (UnreferencedOutput, error)
}

InsightAnalyzer is the optional capability a workspace implements to answer the insight lenses. It is the sibling of IgnoredFileReporter and ConflictResolver: callers type-assert for it and degrade when it is absent rather than requiring every WorkspaceRepository to carry analytics it may have no history to compute.

It exists so both entry points agree on ONE vocabulary. The CLI declared this shape privately and the Buzz surface could not see it, so `magus\insight` forked a whole magus - a process spawn, a second workspace load, a JSON encode and a Buzz-side parse - to reach methods the calling process already had.

Volatility and Unreferenced take no options because they read their whole source workspace-wide: the run-history file and the symbol index have no commit window to narrow, which is the distinction the lens docs draw for a reader too.

type InsightOptions

type InsightOptions struct {
	Dir     string
	Commits int
	Since   string
	Files   bool
}

InsightOptions configures an insight scan. One scan of recent history feeds every lens; Dir scopes it to a subtree, Since bounds it by date, Files switches the hotspots lens from project to file granularity.

type InsightReport

type InsightReport struct {
	Hotspots   HotspotOutput    `json:"hotspots"    yaml:"hotspots"`
	Affinity   AffinityOutput   `json:"affinity"    yaml:"affinity"`
	Ownership  OwnershipOutput  `json:"ownership"   yaml:"ownership"`
	Trend      TrendOutput      `json:"trend"       yaml:"trend"`
	Volatility VolatilityReport `json:"volatility"  yaml:"volatility"`
	// Unreferenced is the knowledge-graph axis. Like Volatility it is a value, not a
	// pointer: the report always renders the section, and an empty list with a verdict
	// says more than an omitted section would.
	Unreferenced UnreferencedOutput `json:"unreferenced" yaml:"unreferenced"`
}

InsightReport bundles every lens for the combined report (the committable Markdown doc and its -o json form). The VCS axis only: `magus graph stats` is the structural one, and nothing here reads the knowledge graph.

Volatility is a VALUE, not a pointer, so the Buzz mirror declares it non-optional and a caller reads report.volatility.targets without a nil guard. An empty Targets list is what "the run-outcome axis had nothing to say" means, which is the same test every consumer already made. Deliberately unlike InsightView's pointer above - see the note there for why the console shape keeps a distinction this one does not need.

func (InsightReport) BuzzObject added in v0.4.0

func (v InsightReport) BuzzObject() BuzzObject

type InsightView added in v0.2.0

type InsightView struct {
	Hotspots  HotspotOutput   `json:"hotspots"   yaml:"hotspots"`
	Affinity  AffinityOutput  `json:"affinity"   yaml:"affinity"`
	Ownership OwnershipOutput `json:"ownership"  yaml:"ownership"`
	Trend     TrendOutput     `json:"trend"      yaml:"trend"`
	// Volatility stays a POINTER here while InsightReport's is a value, and the two
	// are not carelessly out of step: this is the console's wire shape, where absent
	// and empty say different things. docs/reference/api/insight.md commits to it -
	// null renders "no runs recorded yet", an empty report renders "no volatile
	// targets" - and volatilityToProto maps nil to a nil message to preserve it.
	// InsightReport has no such reader: it crosses into Buzz, where the mirror
	// declares the field non-optional so a magusfile reaches .volatility.targets
	// without a nil guard.
	Volatility *VolatilityReport `json:"volatility" yaml:"volatility"`
}

InsightView bundles the four VCS-history lenses plus the run-outcome volatility lens, without the knowledge-graph axis. It is what the console serves at GET /api/v1/insight: the same per-lens outputs the CLI produces. The four git lenses come from one bounded git-log scan (cached by the service); Volatility is a fresh runtime-history file read folded into the same response, so the dashboard reads one endpoint for every lens.

type Inspector added in v0.4.0

type Inspector interface {
	// ListCharms builds the inverse charm index: every charm name a target in the
	// workspace declares, plus the reserved built-ins and the workspace's own
	// default_charms set (read from the receiver's config), and for each the
	// project/target/spell declarations that give it a patch.
	ListCharms(ctx context.Context) ([]Charm, error)
	ListTargets(ctx context.Context) ([]TargetEntry, error)
	ListProjects(ctx context.Context) (ProjectsOutput, error)
	// EvaluateProjects returns the fully-resolved project inventory: every
	// ListProjects fact plus each project's resolved spells and target policies.
	EvaluateProjects(ctx context.Context) (EvaluatedProjectsOutput, error)
	// EvaluateTarget returns the fully-evaluated dispatch plan for t.
	EvaluateTarget(ctx context.Context, t Target) ([]EvaluatedTarget, error)
	ClassifyFiles(ctx context.Context, paths []string) ([]FileEntry, error)
	// TargetGraph returns the ctx.needs dependency DAG of each project's targets,
	// read statically from the magusfile source. A different graph, at a different
	// granularity, from WorkspaceReader.Graph: that one is the PROJECT dependency
	// graph (project -> project, from depends_on); this one is the TARGET graph
	// within and across projects (target -> target, from ctx.needs).
	TargetGraph(ctx context.Context) (TargetGraphOutput, error)
	// Workspace returns the single-entry view of this workspace: a *Magus is
	// always exactly one workspace. The CLI's `describe workspaces` merges these
	// across the daemon's declared roots when daemon.workspaces is set.
	Workspace(ctx context.Context, cfg WorkspaceConfig) (WorkspaceEntry, error)
}

Inspector reads the structured facts of a workspace without evaluating anything: what projects, targets, charms, and files exist, and how a specific target resolves. Organized on one axis - List* enumerates a declaration (cheap), Evaluate* resolves one (it costs), Classify* and TargetGraph say what they do.

type KnowledgeAnswer added in v0.4.0

type KnowledgeAnswer struct {
	Verdict      KnowledgeVerdict       `json:"verdict"                 yaml:"verdict"`
	Reason       KnowledgeUnknownReason `json:"reason,omitempty"        yaml:"reason,omitempty"`
	Gaps         []KnowledgeSymbolGap   `json:"gaps,omitempty"          yaml:"gaps,omitempty"`
	StaleIndexes []string               `json:"stale_indexes,omitempty" yaml:"stale_indexes,omitempty"`
}

KnowledgeAnswer rides every graph lookup's structured output so a consumer can branch on the verdict instead of inferring it from an empty list. Verdict has no omitempty: `absent` must be positively asserted, or a reader cannot tell a verified absence from an older magus that had no verdict at all.

StaleIndexes is the caveat the text arm has always printed under an answer and the structured arms silently dropped: workspace-relative paths of the projects whose built symbol index predates the sources it covers. It rides the answer rather than the console so `-o json` and MCP cannot lose it - a machine consumer reading only stdout got an unqualified `absent` where a human reading the same lookup was told the index was behind.

func ClassifyAnswer added in v0.4.0

func ClassifyAnswer(matched bool, reason KnowledgeUnknownReason, gaps []KnowledgeSymbolGap) KnowledgeAnswer

ClassifyAnswer classifies a lookup's result against what magus was actually able to search.

matched reports whether the lookup returned anything. reason is empty when the symbol layer was searched (or was irrelevant to the question); set it when the lookup could not consult it, or when the coverage probe itself failed. gaps are the projects whose declared index could not be read.

A stated reason or any gap makes the answer unknown WHETHER OR NOT the lookup matched. That is deliberate and it is the difference from a plain emptiness check: a populated list drawn from a half-indexed workspace is as misleading as an empty one, because the projects it omits are invisible either way.

func (KnowledgeAnswer) BuzzObject added in v0.4.0

func (v KnowledgeAnswer) BuzzObject() BuzzObject

type KnowledgeDocCoverage added in v0.2.0

type KnowledgeDocCoverage struct {
	Kind         string   `json:"kind"                   yaml:"kind"`
	Total        int      `json:"total"                  yaml:"total"`
	Documented   int      `json:"documented"             yaml:"documented"`
	Percent      int      `json:"percent"                yaml:"percent"`
	Undocumented []string `json:"undocumented,omitempty" yaml:"undocumented,omitempty"`
}

KnowledgeDocCoverage is doc coverage for one documentable kind: how many of its nodes have a doc pointing at them.

type KnowledgeEdge added in v0.2.0

type KnowledgeEdge struct {
	Source     string  `json:"source"               yaml:"source"`
	Target     string  `json:"target"               yaml:"target"`
	Relation   string  `json:"relation"             yaml:"relation"`
	Confidence string  `json:"confidence"           yaml:"confidence"`
	Score      float64 `json:"score"                yaml:"score"`
	Provenance string  `json:"provenance,omitempty" yaml:"provenance,omitempty"`
}

KnowledgeEdge is one directed relation with provenance. Source and Target are node IDs. The JSON keys (source/target) match the node-link convention that external graph tools consume, so an exported graph opens in Gephi/yEd/etc.

type KnowledgeEdgeRef added in v0.2.0

type KnowledgeEdgeRef struct {
	Relation   string        `json:"relation"             yaml:"relation"`
	Direction  EdgeDirection `json:"direction"            yaml:"direction"`
	Other      string        `json:"other"                yaml:"other"`
	OtherKind  string        `json:"other_kind"           yaml:"other_kind"`
	OtherLabel string        `json:"other_label"          yaml:"other_label"`
	Provenance string        `json:"provenance,omitempty" yaml:"provenance,omitempty"`
}

KnowledgeEdgeRef is one edge seen from a focus node: the relation, the node on the other end (with kind + label for readability), the direction relative to the focus, and the edge's provenance.

type KnowledgeExplainOutput added in v0.2.0

type KnowledgeExplainOutput struct {
	Definition    string             `json:"definition"     yaml:"definition"`
	SchemaVersion int                `json:"schema_version" yaml:"schema_version"`
	Node          KnowledgeNode      `json:"node"           yaml:"node"`
	BlastRadius   int                `json:"blast_radius"   yaml:"blast_radius"`
	Out           []KnowledgeEdgeRef `json:"out,omitempty"  yaml:"out,omitempty"`
	In            []KnowledgeEdgeRef `json:"in,omitempty"   yaml:"in,omitempty"`
}

KnowledgeExplainOutput is a single node's context card: its data, grouped out/in edges with provenance, and a blast-radius count (how many nodes can transitively reach it).

type KnowledgeGodNode added in v0.2.0

type KnowledgeGodNode struct {
	ID     string `json:"id"     yaml:"id"`
	Kind   string `json:"kind"   yaml:"kind"`
	Label  string `json:"label"  yaml:"label"`
	Degree int    `json:"degree" yaml:"degree"` // in + out
	In     int    `json:"in"     yaml:"in"`
	Out    int    `json:"out"    yaml:"out"`
}

KnowledgeGodNode is a highly-connected node - where structural risk concentrates.

type KnowledgeGraphDiff added in v0.2.0

type KnowledgeGraphDiff struct {
	Definition    string                `json:"definition"     yaml:"definition"`
	SchemaVersion int                   `json:"schema_version" yaml:"schema_version"`
	Base          string                `json:"base"           yaml:"base"` // the base revision or baseline label
	NodesAdded    []KnowledgeNode       `json:"nodes_added,omitempty"    yaml:"nodes_added,omitempty"`
	NodesRemoved  []KnowledgeNode       `json:"nodes_removed,omitempty"  yaml:"nodes_removed,omitempty"`
	NodesChanged  []KnowledgeNodeChange `json:"nodes_changed,omitempty"  yaml:"nodes_changed,omitempty"`
	EdgesAdded    []KnowledgeEdge       `json:"edges_added,omitempty"    yaml:"edges_added,omitempty"`
	EdgesRemoved  []KnowledgeEdge       `json:"edges_removed,omitempty"  yaml:"edges_removed,omitempty"`
}

KnowledgeGraphDiff is the result of `magus graph diff`: the node/edge deltas between a base graph and the current one. Slices are sorted (by node ID, then edge key) so the diff is deterministic and reviewable.

type KnowledgeGraphOutput added in v0.2.0

type KnowledgeGraphOutput struct {
	Definition    string `json:"definition"    yaml:"definition"`
	SchemaVersion int    `json:"schema_version" yaml:"schema_version"`
	Directed      bool   `json:"directed"      yaml:"directed"`
	Multigraph    bool   `json:"multigraph"    yaml:"multigraph"`
	NodeCount     int    `json:"node_count"    yaml:"node_count"`
	EdgeCount     int    `json:"edge_count"    yaml:"edge_count"`
	// SourceBaseURL is the workspace's repo blob base (e.g.
	// "https://github.com/owner/repo/blob/main"), derived from the VCS remote, so a
	// viewer can turn a node's relative `source` path into a link to the RIGHT repo.
	// Empty when there is no remote or the forge is unrecognized. Additive; omitted
	// when empty, so it never bumps the schema version.
	SourceBaseURL string `json:"source_base,omitempty" yaml:"source_base,omitempty"`
	// CatalogFingerprint identifies the binary that produced this export. Generated output
	// depends on the binary's catalogs as well as the tree, but only the tree shows in a
	// diff, so regenerating with a foreign build reads as ordinary drift (MGS4005).
	// Additive and omitted when empty, so it never bumps the schema version.
	CatalogFingerprint string `json:"catalog_fingerprint,omitempty" yaml:"catalog_fingerprint,omitempty"`
	// Nodes is the export's primary collection: what -o jsonl streams, one node per line.
	Nodes []KnowledgeNode `json:"nodes"         yaml:"nodes" jsonl:"primary"`
	Links []KnowledgeEdge `json:"links"         yaml:"links"`
}

KnowledgeGraphOutput is the merged node-link export produced by "magus graph export -o json". It is node-link compatible (nodes have an "id"; links have "source"/"target"), so external graph UIs read it directly; the extra magus fields (definition, schema_version, counts) are additive and ignored by strict node-link readers. Directed and non-multigraph by construction.

type KnowledgeMatch added in v0.2.0

type KnowledgeMatch struct {
	ID    string `json:"id"    yaml:"id"`
	Kind  string `json:"kind"  yaml:"kind"`
	Label string `json:"label" yaml:"label"`
	Score int    `json:"score" yaml:"score"`
	// Staleness and OutrunDays travel with a prose match that ranked DOWN because the
	// thing it describes moved on without it. They are the evidence for the weight, and
	// they exist so the weight is never silent: a reader can see "ranked down: 400 days
	// behind its subject" instead of wondering why a doc sank. Empty on anything that was
	// not penalized, including prose with no history to measure.
	Staleness  string `json:"staleness,omitempty"   yaml:"staleness,omitempty"`
	OutrunDays int    `json:"outrun_days,omitempty" yaml:"outrun_days,omitempty"`
}

KnowledgeMatch is one ranked node from a query.

type KnowledgeNode added in v0.2.0

type KnowledgeNode struct {
	ID     string            `json:"id"               yaml:"id"`
	Kind   string            `json:"kind"             yaml:"kind"`
	Label  string            `json:"label"            yaml:"label"`
	Doc    string            `json:"doc,omitempty"    yaml:"doc,omitempty"`
	Source string            `json:"source,omitempty" yaml:"source,omitempty"` // path or path:line provenance
	Attrs  map[string]string `json:"attrs,omitempty"  yaml:"attrs,omitempty"`  // kind-specific (charm pointer, MGS URL, ...)
}

KnowledgeNode is one vertex: a magus-domain entity with stable identity and provenance. ID is "<kind>:<qualified-name>" (e.g. "target:pkg/foo:build"), stable across builds so external consumers and agent memory can key on it.

type KnowledgeNodeChange added in v0.2.0

type KnowledgeNodeChange struct {
	ID     string        `json:"id"     yaml:"id"`
	Fields []string      `json:"fields" yaml:"fields"` // kind|label|doc|source|attrs
	Before KnowledgeNode `json:"before" yaml:"before"`
	After  KnowledgeNode `json:"after"  yaml:"after"`
}

KnowledgeNodeChange is one node present in both graphs whose data differs: the before/after nodes plus the names of the fields that changed.

type KnowledgeNote added in v0.4.0

type KnowledgeNote struct {
	Name    string
	Title   string
	Path    string
	Tags    []string
	Anchors []string
}

KnowledgeNote is one human-authored note from the declared notes store (an assembly input, not a wire type). Path is workspace-relative and is what the @vcs shard joins on to attribute the note to whoever wrote it - the reason notes live in the checkout at all.

Anchors are the entities the note attaches to, already resolved to node IDs by the caller: assembly emits an edge only for an anchor that resolves, because an edge to a node that does not exist is dangling in the graph and `magus notes verify` is the right place to report that instead.

type KnowledgeOccurrencesOutput added in v0.4.0

type KnowledgeOccurrencesOutput struct {
	Definition    string `json:"definition"     yaml:"definition"`
	SchemaVersion int    `json:"schema_version" yaml:"schema_version"`
	Symbol        string `json:"symbol"         yaml:"symbol"`
	Label         string `json:"label"          yaml:"label"`
	// Name is the identifier a rename would replace: the first of Names. It is derived from
	// the symbol's own descriptor, not from the index's display name - for a package those
	// differ, and the descriptor's last segment is what call sites write.
	Name string `json:"name" yaml:"name"`
	// Names is every spelling an occurrence was allowed to hold, in the order they were
	// tried. A symbol can legitimately be written more than one way - a package's import
	// statement holds its full path while its call sites hold the bare identifier - so a
	// site is checked against this whole set, not against Name alone. It is surfaced so a
	// consumer can reproduce the verdict instead of having to trust it.
	Names           []string `json:"names,omitempty"  yaml:"names,omitempty"`
	FileCount       int      `json:"file_count"       yaml:"file_count"`
	OccurrenceCount int      `json:"occurrence_count" yaml:"occurrence_count"`
	// VerifiedCount is how many of OccurrenceCount are safe to edit. A caller comparing
	// the two learns, in one subtraction, whether a rewrite would be complete - which is
	// the question that decides whether to proceed at all.
	VerifiedCount int `json:"verified_count"   yaml:"verified_count"`
	// StaleFiles is how many of Files carry a stale marker. Non-zero means a rewrite would
	// be acting on an index that no longer matches the tree, and the honest move is to
	// re-index rather than to edit around the bad sites.
	StaleFiles int                    `json:"stale_files,omitempty" yaml:"stale_files,omitempty"`
	Files      []SymbolOccurrenceFile `json:"files,omitempty"       yaml:"files,omitempty"`
	Answer     KnowledgeAnswer        `json:"answer"                yaml:"answer"`
}

KnowledgeOccurrencesOutput is the result of `magus refs <symbol> --occurrences`: every site the symbol appears, with ranges verified against the working tree.

It is an ANSWER, not an action. magus reports where the symbol is and whether each site is safe to touch; applying the edit is the caller's job, the same division `magus affected` keeps between naming what a change reaches and doing anything about it.

Applying edits within a file requires walking occurrences BACK TO FRONT: replacing a name with one of a different length shifts every later column on the same line, so front-to-back application corrupts each subsequent site. Files are independent.

type KnowledgeOrphan added in v0.2.0

type KnowledgeOrphan struct {
	ID     string `json:"id"     yaml:"id"`
	Kind   string `json:"kind"   yaml:"kind"`
	Label  string `json:"label"  yaml:"label"`
	Reason string `json:"reason" yaml:"reason"`
}

KnowledgeOrphan is a node missing the connection its kind implies (a doc that documents nothing, a spell no target uses), with a plain-English reason.

type KnowledgeOutputRef added in v0.2.0

type KnowledgeOutputRef struct {
	Project string
	Target  string
	Ref     string
	OK      bool
}

KnowledgeOutputRef is one target's most recent captured-output reference, gathered from the local output store (see internal/cache OutputStore) and folded onto the target node as observed attrs in the @runtime shard (non-deterministic, never remote-shared). Like KnowledgeTiming it is an assembly input, not a wire type: Project and Target name the node, Ref is the output reference id (the "ref1a2b3c" token) minted for that run, and OK is whether that run succeeded. It lets an agent go query -> target node -> the last captured output in two hops. The forecast history the timing attrs ride does not (and by its cache-safety lock must not) record refs, so the output store is the source.

type KnowledgePackage added in v0.4.0

type KnowledgePackage struct {
	Manager string
	Name    string
	Version string
	// Indirect marks a transitive dependency (go.mod's `// indirect`). Kept because
	// "do we depend on this directly" changes the answer to whether a version is ours
	// to bump, and because a direct dependency is the one worth reading docs about.
	Indirect bool
	// Replaced marks a dependency a replace directive redirects. Its Version is the
	// replacement's, which is what actually builds - so the node stays truthful about
	// what is on disk - and this flag is what stops a reader concluding the manifest's
	// original requirement is what shipped.
	Replaced bool
}

KnowledgePackage is one third-party dependency read out of a project's manifest: which package manager governs it, its name in that manager's namespace, and the version the manifest resolves to.

Manager is not decoration and not derivable from Name. It is what keeps the npm package `foo` and the Go module `foo` from colliding on one node - the same collision internal/symbols/scip.go's parseMoniker already folds the manager into its key to avoid, and for the same reason.

Version is what the manifest RESOLVES to, never a range. Go states exact versions in go.mod, so the manifest is the resolved list; ecosystems whose manifest holds a range (npm's ^4.2.0) must read their lockfile instead, which is what spells.Manifest.LockCandidates leads to. A record here always carries a pin - an unresolvable dependency is omitted rather than recorded with a range, because a range presented as a version is exactly the version skew this exists to end.

type KnowledgePathOutput added in v0.2.0

type KnowledgePathOutput struct {
	Definition    string              `json:"definition"     yaml:"definition"`
	SchemaVersion int                 `json:"schema_version" yaml:"schema_version"`
	From          string              `json:"from"           yaml:"from"`
	To            string              `json:"to"             yaml:"to"`
	Found         bool                `json:"found"          yaml:"found"`
	Steps         []KnowledgePathStep `json:"steps,omitempty" yaml:"steps,omitempty"`
}

KnowledgePathOutput is the result of `magus path a b`: the resolved endpoints and the shortest connecting path (edges treated as bidirectional), if any.

type KnowledgePathStep added in v0.2.0

type KnowledgePathStep struct {
	From     string `json:"from"     yaml:"from"`
	To       string `json:"to"       yaml:"to"`
	Relation string `json:"relation" yaml:"relation"`
	Forward  bool   `json:"forward"  yaml:"forward"`
}

KnowledgePathStep is one hop along a path, oriented as walked (From -> To). Forward reports whether the underlying edge's own direction is From -> To (false means the path traversed the edge against its direction).

type KnowledgeQueryOutput added in v0.2.0

type KnowledgeQueryOutput struct {
	Definition    string `json:"definition"     yaml:"definition"`
	SchemaVersion int    `json:"schema_version" yaml:"schema_version"`
	Query         string `json:"query"          yaml:"query"`
	Budget        int    `json:"budget"         yaml:"budget"`
	MatchCount    int    `json:"match_count"    yaml:"match_count"`
	// Offset is the index of the first returned match within the full ranked list;
	// 0 (omitted) for an unpaged query or the first page. Offset alone does not
	// signal paging - page 0 of a paged query and an unpaged query look the same
	// here; the MCP layer's next_cursor is what signals more pages remain.
	Offset  int              `json:"offset,omitempty" yaml:"offset,omitempty"`
	Matches []KnowledgeMatch `json:"matches"        yaml:"matches"`
	Nodes   []KnowledgeNode  `json:"nodes"          yaml:"nodes"`
	Links   []KnowledgeEdge  `json:"links"          yaml:"links"`
	// Answer says whether MatchCount 0 is a verified absence or a blind spot. Most
	// queries never seed the symbol shards, so a bare term matching nothing says nothing
	// about whether a code symbol by that name exists - this is where that is stated.
	Answer KnowledgeAnswer `json:"answer" yaml:"answer"`
}

KnowledgeQueryOutput is the result of `magus query`: the ranked seed matches plus the induced subgraph (neighborhood) collected up to the node budget. The Nodes/Links carry the node-link keys so the subgraph is itself a valid export. MatchCount is the TOTAL matches; when a page is requested (Offset > 0 or a smaller Matches slice than MatchCount) the caller pages via Offset + len(Matches).

type KnowledgeRefSite added in v0.2.0

type KnowledgeRefSite struct {
	File  string `json:"file"            yaml:"file"`
	Count int    `json:"count,omitempty" yaml:"count,omitempty"`
	Lines []int  `json:"lines,omitempty" yaml:"lines,omitempty"`
}

KnowledgeRefSite is one file that defines or references a symbol, with the occurrence count and the (capped) lines where it appears.

type KnowledgeRefsOutput added in v0.2.0

type KnowledgeRefsOutput struct {
	Definition    string             `json:"definition"     yaml:"definition"`
	SchemaVersion int                `json:"schema_version" yaml:"schema_version"`
	Symbol        string             `json:"symbol"         yaml:"symbol"`
	Label         string             `json:"label"          yaml:"label"`
	FileCount     int                `json:"file_count"     yaml:"file_count"`
	RefCount      int                `json:"ref_count"      yaml:"ref_count"`
	Defs          []KnowledgeRefSite `json:"defs,omitempty" yaml:"defs,omitempty"`
	Refs          []KnowledgeRefSite `json:"refs,omitempty" yaml:"refs,omitempty"`
	// Answer says whether an empty Refs list is a verified absence or a blind spot. A
	// named field rather than an embedded struct: the template renderer skips anonymous
	// fields, so `-o template` with no body would not list it, and yaml would nest what
	// json flattens.
	Answer KnowledgeAnswer `json:"answer" yaml:"answer"`
}

KnowledgeRefsOutput is the result of `magus refs <symbol>`: the resolved symbol, its definition site(s), and every referencing file with the per-file occurrence count and (capped) line list. Occurrence-shaped, not node-link.

type KnowledgeRouting added in v0.2.0

type KnowledgeRouting struct {
	SchemaVersion int `json:"schema_version" yaml:"schema_version"`
	// CatalogFingerprint identifies the binary that rendered this index; see the field of
	// the same name on KnowledgeGraphOutput. Carried here so MAGUS.md can show it without
	// the renderer taking another parameter. Empty when no graph was available.
	CatalogFingerprint string `json:"catalog_fingerprint,omitempty" yaml:"catalog_fingerprint,omitempty"`
	NodeCount          int    `json:"node_count"     yaml:"node_count"`
	// EdgeCount counts the edges this summary ranked on, so it excludes runtime edges and
	// is NOT KnowledgeGraphOutput's or KnowledgeStats' count. Unrendered (writeRouting
	// drops totals), kept so a renderer that adds one cannot reintroduce the dependence.
	EdgeCount int                       `json:"edge_count"     yaml:"edge_count"`
	Kinds     []KnowledgeRoutingKind    `json:"kinds"          yaml:"kinds"`
	Projects  []KnowledgeRoutingProject `json:"projects"       yaml:"projects"`
}

KnowledgeRouting is the compact "query first" summary rendered into MAGUS.md's header: per-kind and per-project entry points so a reader's (human or agent) next action is a magus query, not a grep. It routes - counts, the field to query, and a few high-degree anchor nodes - and never dumps graph data, so it stays diff-stable across routine edits.

type KnowledgeRoutingKind added in v0.2.0

type KnowledgeRoutingKind struct {
	Kind    string   `json:"kind"              yaml:"kind"`
	Count   int      `json:"count"             yaml:"count"`
	Anchors []string `json:"anchors,omitempty" yaml:"anchors,omitempty"`
}

KnowledgeRoutingKind is one row of the domain routing table: a node kind, how many exist, and up to a few highest-degree "anchor" nodes to start from.

type KnowledgeRoutingProject added in v0.2.0

type KnowledgeRoutingProject struct {
	Path        string   `json:"path"                  yaml:"path"`
	TargetCount int      `json:"target_count"          yaml:"target_count"`
	KeyTargets  []string `json:"key_targets,omitempty" yaml:"key_targets,omitempty"`
}

KnowledgeRoutingProject is one per-project routing row: its path, target count, and a few key (highest-degree) targets.

type KnowledgeStats added in v0.2.0

type KnowledgeStats struct {
	Definition string `json:"definition"          yaml:"definition"`
	NodeCount  int    `json:"node_count"          yaml:"node_count"`
	EdgeCount  int    `json:"edge_count"          yaml:"edge_count"`
	// Gods is the headline finding and so the primary collection -o jsonl streams.
	Gods     []KnowledgeGodNode     `json:"gods"                yaml:"gods" jsonl:"primary"`
	Orphans  []KnowledgeOrphan      `json:"orphans,omitempty"   yaml:"orphans,omitempty"`
	Coverage []KnowledgeDocCoverage `json:"coverage,omitempty"  yaml:"coverage,omitempty"`
	// Connectivity is the data-quality lens: how fragmented the graph is. A high isolated count or many
	// weakly-connected components means the builder has not linked everything it could, which hurts
	// discoverability. IsolatedCount is every node with no edge at all (Orphans lists a capped sample by
	// kind); ComponentCount is the number of weakly-connected components (1 = fully reachable);
	// LargestComponentSize is the biggest component's node count (the "main" graph most nodes should
	// belong to).
	IsolatedCount        int `json:"isolated_count"         yaml:"isolated_count"`
	ComponentCount       int `json:"component_count"        yaml:"component_count"`
	LargestComponentSize int `json:"largest_component_size" yaml:"largest_component_size"`
}

KnowledgeStats is the knowledge-graph analytics behind `magus graph stats`: where the workspace concentrates (god nodes), where it neglects (orphans), and where docs are missing (coverage). It is the structural analogue of insight's git-history lenses (insight report embeds it), derived purely from the graph (degree and reachability), so it is deterministic and LLM-free.

type KnowledgeSymbol added in v0.2.0

type KnowledgeSymbol struct {
	Key        string
	Moniker    string
	Label      string
	Language   string
	SymbolKind string
	// Source is "<path>:<line>" of the definition, or empty when only references
	// were seen (the definition lives in another index).
	Source string
	// DefEndLine is the 1-based last line of the definition's BODY, from the SCIP
	// occurrence's enclosing range, or 0 when the indexer emits none (which is the honest
	// answer rather than a guess - see Calls, which makes the same trade).
	//
	// Source alone gives a start with no end, which is enough to point a reader at a
	// definition but not enough to fingerprint one. Pairing them bounds the exact lines a
	// symbol occupies, so a consumer can tell "this symbol still exists" from "this symbol
	// still exists and says the same thing".
	DefEndLine int
	Defs       []string
	Refs       []KnowledgeSymbolRef
	// Calls are the workspace-defined symbols referenced from inside this symbol's own
	// definition body, attributed by the SCIP occurrence's enclosing range. Collapsed per
	// (caller, callee) - the same scale decision Refs makes per (file, symbol) - so a hot
	// callee yields one entry per caller, never one per call site. Empty when the indexer
	// emits no enclosing ranges, which is the honest answer rather than a guess.
	Calls []KnowledgeSymbolCall
}

KnowledgeSymbol is one code symbol ingested from a SCIP index (an assembly input, not a wire type). magus never parses source; a per-language indexer emits the index file and this is the language-agnostic shape the reader distills it to. Key is the version-stripped, stable moniker key (it becomes the node ID via symbolID); Moniker is the original. SymbolKind is the SCIP classifier (function/type/...), distinct from the node's own kind (always "symbol"). Defs are the files that define the symbol (usually one); Refs are the files that use it, one entry per file (never per occurrence, the scale decision) with a count and a capped line list.

type KnowledgeSymbolCall added in v0.4.0

type KnowledgeSymbolCall struct {
	Key   string
	Count int
}

KnowledgeSymbolCall is one callee reached from inside a symbol's definition body: the callee's version-stripped key (the same key that becomes its node ID) and how many occurrences were attributed. It carries no line list on purpose - the call sites are already recorded on the caller file's `references` edge, and duplicating them per pair would be pure shard weight at this edge count.

type KnowledgeSymbolGap added in v0.4.0

type KnowledgeSymbolGap struct {
	Project ProjectRef           `json:"project"          yaml:"project"`
	State   SymbolIndexFreshness `json:"state"            yaml:"state"`
	Detail  string               `json:"detail,omitempty" yaml:"detail,omitempty"`
}

KnowledgeSymbolGap is one project whose declared symbol index magus could not read. State reuses SymbolIndexFreshness so reporting staleness later is additive rather than a second enum; today only SymbolIndexNotBuilt is emitted, because the read verbs deliberately probe with a stat rather than opening the workspace's cache.

func (KnowledgeSymbolGap) BuzzObject added in v0.4.0

func (v KnowledgeSymbolGap) BuzzObject() BuzzObject

func (KnowledgeSymbolGap) Describe added in v0.4.0

func (g KnowledgeSymbolGap) Describe() string

Describe renders one gap as "libs/api (not-indexed)". It lives here so the CLI, the explain text, and the insight report cannot drift into three spellings of one fact.

type KnowledgeSymbolRef added in v0.2.0

type KnowledgeSymbolRef struct {
	Path  string
	Count int
	Lines []int
}

KnowledgeSymbolRef is one referencing file: its path, how many times the symbol appears, and a capped list of the first occurrence lines (bounded so a hot symbol cannot blow up the edge's provenance).

type KnowledgeTiming added in v0.2.0

type KnowledgeTiming struct {
	Project        string
	Target         string
	P75Ms          int64
	Samples        int
	HitRate        float64
	HitRateSamples int
}

KnowledgeTiming is one target's observed run cost, gathered from the local timing history and folded onto the target node in the isolated @runtime shard (observed, non-deterministic, never remote-shared). It is an assembly input, not a wire type: Project and Target name the node, the rest annotate it. Samples is the duration-percentile sample count; HitRateSamples is the hit-rate denominator (hits + misses), so a consumer can tell a cold rate from a settled one.

type KnowledgeUnknownReason added in v0.4.0

type KnowledgeUnknownReason string

KnowledgeUnknownReason says WHY an answer is unknown, because the causes have different fixes: one needs an index built, one needs a different query, and one means magus could not even establish what it had searched.

const (
	// ReasonSymbolIndexMissing: a project declares a SCIP index magus could not read.
	// Fix: build it.
	ReasonSymbolIndexMissing KnowledgeUnknownReason = "symbol-index-missing"
	// ReasonSymbolsNotLoaded: this lookup never merged the symbol shards, so no code
	// symbol could have matched whatever the index holds. Fix: ask a question that seeds
	// symbols, or use the verb that always does.
	ReasonSymbolsNotLoaded KnowledgeUnknownReason = "symbols-not-loaded"
	// ReasonCoverageUnknown: the coverage probe itself failed, so magus cannot say what it
	// searched. Reporting this as `absent` would assert exactly the fact it failed to
	// establish, which is the one outcome this whole verdict exists to prevent.
	ReasonCoverageUnknown KnowledgeUnknownReason = "coverage-unknown"
	// ReasonIndexStale: the symbol index was read, but it predates the sources it covers,
	// so a definition added or moved since the build is not in it. Fix: rebuild the index.
	// Only a lookup whose whole evidence base IS the index reports this - a miss there is
	// unverifiable, while a general query reads layers the index has no bearing on.
	ReasonIndexStale KnowledgeUnknownReason = "index-stale"
)

type KnowledgeVCS added in v0.2.0

type KnowledgeVCS struct {
	Path         string    `json:"path"`
	LastCommit   string    `json:"last_commit"`
	LastModified time.Time `json:"last_modified"`
	LastAuthor   string    `json:"last_author"`
	// Authors is the distinct set of authors who touched the file within the scanned
	// window (sorted), the source for the `author --authored--> file` edges. LastAuthor
	// is one of them (the most recent).
	Authors []string `json:"authors"`
	Commits int      `json:"commits"`
}

KnowledgeVCS is one file's git history metadata (an assembly input, not part of the exported graph), folded onto the file node as attrs in the @vcs shard. It is EXTRACTED from git, not inferred, and deterministic per commit: the same HEAD yields the same values, so the shard is remote-shareable (unlike @runtime). Path is workspace-relative and matches a file node's Source. Commits is the number of commits touching the file within the scanned window; LastCommit/LastModified/LastAuthor are the most recent such commit's short SHA, author time, and author name - the last is the EMERGENT maintainer, comparable against a file's DECLARED CODEOWNERS owner.

The json tags are this type's CACHE format: the scan is expensive enough to persist between builds, so the names on disk are pinned here rather than left to follow Go identifiers.

type KnowledgeVerdict added in v0.4.0

type KnowledgeVerdict string

KnowledgeVerdict classifies an answer that came back empty. The distinction is the point: `absent` is a fact magus verified, `unknown` is magus saying it could not see far enough to know. Without it a caller reads every empty result as proof of absence, which is the most expensive way for a lookup to be wrong.

Naming rule for the whole family, since magus reaches verdicts in several domains: a VERDICT is the scalar judgment, and the thing carrying it is named for the question it answers. So KnowledgeAnswer holds a Verdict, spells.VersionBounds.Check returns a spells.Verdict, and a record of a judgment is a Result or a Plan rather than a Verdict. Two packages both spelling the scalar `Verdict` is not a collision - Go qualifies it, and both genuinely are verdicts. Prefixing this one to KnowledgeVerdictUnknown would only add stutter and break the tie to what the CLI prints and the JSON key says, which is the one-vocabulary rule the README states.

const (
	VerdictFound   KnowledgeVerdict = "found"   // the lookup returned something
	VerdictAbsent  KnowledgeVerdict = "absent"  // nothing matched, and everything that could match was searched
	VerdictUnknown KnowledgeVerdict = "unknown" // nothing matched, but part of the workspace was not searchable
)

type Lease added in v0.4.0

type Lease struct {
	// ID is the lease's identity within the plan, and the key Put upserts on. The
	// console joins its drawer rows to agent activity by this value, so an
	// orchestrator should use the same id it puts in the worker's prompt.
	ID string `json:"id" yaml:"id"`
	// Parent is the id of the lease this one was handed out under, empty for a lease the
	// root spawned. Depth is read off this chain rather than stored, so a mis-stamped depth
	// cannot disagree with the tree.
	Parent string `json:"parent,omitempty" yaml:"parent,omitempty"`
	// Goal is the lease's goal and its observable acceptance criteria, as one block of
	// text. Not split into two fields: the skill requires criteria to be observable and
	// a separate empty Criteria field would read as "none required" rather than as
	// "the author did not write any".
	Goal string `json:"goal,omitempty" yaml:"goal,omitempty"`
	// Checkpoint is the working state this lease was handed, in the form
	// `magus vcs checkpoint -o name` prints: the revision, plus a dirty-patch digest
	// when the tree was not clean. A string rather than an embedded VCSCheckpoint
	// because that is the form an orchestrator has at spawn time and the form a later
	// reader feeds back to `magus graph diff --rev`.
	Checkpoint string `json:"checkpoint,omitempty" yaml:"checkpoint,omitempty"`
	// OwnedPaths and ForbiddenPaths are the declared write boundary. Empty on a
	// read-only lease BY DESIGN (see ReadOnly), which is why neither is required.
	OwnedPaths     []string `json:"owned_paths,omitempty" yaml:"owned_paths,omitempty"`
	ForbiddenPaths []string `json:"forbidden_paths,omitempty" yaml:"forbidden_paths,omitempty"`
	// DependsOn are the ids of leases that must land before this one, so a reader can
	// see the ordering the orchestrator committed to.
	DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
	// Tier is the effort tier the work was matched to (principal, standard, economy in
	// the skill's table). A free string: hosts name their tiers differently and a
	// closed set here would force a lie for the ones that do not fit.
	Tier string `json:"tier,omitempty" yaml:"tier,omitempty"`
	// Validation is the magus target or named check this lease was assigned, e.g.
	// "magus run test internal/ledger".
	Validation string `json:"validation,omitempty" yaml:"validation,omitempty"`
	// State is the row's lifecycle position. See LeaseState for why no_return is
	// its own value.
	State LeaseState `json:"state,omitempty" yaml:"state,omitempty"`
	// ReadOnly marks the abbreviated row the skill describes: a lease that gathers
	// evidence and writes nothing has no write set, so empty OwnedPaths and
	// ForbiddenPaths are correct rather than missing. Without this flag a reader
	// cannot tell an abbreviated row from one whose author forgot the boundary.
	ReadOnly bool `json:"read_only,omitempty" yaml:"read_only,omitempty"`
	// Releases are the paths this lease gave up, each with the content digest the path
	// carried at that moment. Store-computed and output-only, like the timestamps: a
	// worker announces a release by shrinking OwnedPaths, and the digest is what the
	// next agent needs to tell whether it inherited the file the releaser left.
	Releases []LeaseRelease `json:"releases,omitempty" yaml:"releases,omitempty"`
	// Unattributed are paths this lease owns that somebody outside it wrote, newest last,
	// at most MaxUnattributedWrites of them and one row per path.
	//
	// Store-computed and output-only like Releases, and recorded by the AGENT GUARD, which is
	// the only thing positioned to notice: it already grades every write against these declared
	// boundaries and already tells the writer to coordinate. It threw the observation away
	// afterwards, so the lease on the other side - the one whose file moved - was the one
	// party never told.
	Unattributed []LeaseUnattributedWrite `json:"unattributed,omitempty" yaml:"unattributed,omitempty"`
	// ReportedBase is the checkpoint token the lease's WORKER reported it actually landed
	// on, in the same `magus vcs checkpoint -o name` form Checkpoint holds. Checkpoint is
	// what the orchestrator handed out; this is what the worker found. Two fields rather
	// than one overwritten in place, because a single value could never disagree with
	// itself and the disagreement is the fact worth recording.
	ReportedBase string `json:"reported_base,omitempty" yaml:"reported_base,omitempty"`
	// BaseVerdict compares the two, computed by the store at the moment the worker
	// registered and kept as the fact it was then. Empty until a lease registers, which is
	// why there is no vocabulary member for "never registered" - an absent verdict is not
	// a judgment, and inventing one would be the mistake StateNoReturn exists to avoid.
	BaseVerdict LeaseBaseVerdict `json:"base_verdict,omitempty" yaml:"base_verdict,omitempty"`
	// Registered is unix seconds, stamped by the store on the write that recorded
	// ReportedBase, off the same clock read as Updated. No write door accepts it from a
	// caller, for the reason Created and Updated do not: a client-supplied timestamp is a
	// fact about the client's clock.
	Registered int64 `json:"registered,omitempty" yaml:"registered,omitempty"`
	// Created and Updated are unix seconds, stamped by the store on write and
	// output-only to callers - a client-supplied timestamp is a fact about the client's
	// clock, not about when the row was recorded.
	//
	// Updated is the row's heartbeat. A lease that re-puts its row on every state change
	// keeps it moving; a row nobody touches goes stale, and a reader may then judge the
	// lease possibly dead. That judgment is the READER'S - nothing here transitions a row
	// on its own, and silence has no verdict in it.
	Created int64 `json:"created" yaml:"created"`
	Updated int64 `json:"updated" yaml:"updated"`
}

Lease is one row of an orchestrating agent's lease ledger: what that agent DECLARED about a piece of work it handed out, recorded so a human can see the plan the agents are running.

FACTS ONLY, NEVER ENFORCEMENT, and the division is precise rather than a blanket "magus does nothing with these". OwnedPaths and ForbiddenPaths are what an orchestrator said it intended, not a boundary this store checks - nothing here blocks a write, gates a run, or refuses a call. The AGENT GUARD is what consults these facts to grade a write, and it lives outside this package and READS this store; a guard verdict is its own, not the ledger's. BaseVerdict is the shape that division takes on a row: registration computes it, records it, and hands it back, and it refuses nothing - the caller and the orchestrator decide what a divergence is worth.

Why enforcement lives outside rather than here, which is the reason the split exists at all: a store that quietly started refusing would make the ledger something agents route around instead of something they keep honestly, and a ledger nobody keeps honestly grades nothing. The skill that defines this vocabulary says the same thing about the prompt text these rows mirror: ownership is checked by comparing the ledger against the ACTUAL diff since each lease's Checkpoint, which is a job for an agent reading this store rather than for the store itself.

The field set mirrors the ledger table in the magus-multi-agent skill one-for-one, so a row an agent writes down and a row it records here cannot describe the same lease differently.

Registered in cmd/magus-utils/boundary_types.go as a RuntimeObject: magus\ledger.put and magus\ledger.list (bound in internal/interp/bindings/ledger_ns.go, backed by std.MagusPutLedger/MagusListLedger) return one. VCSCheckpoint - the value Checkpoint holds - stays unregistered: Checkpoint is a plain string here, the form an orchestrator has at spawn time, so there is no struct to mirror yet.

func (Lease) BuzzObject added in v0.4.0

func (v Lease) BuzzObject() BuzzObject

func (Lease) Clone added in v0.4.0

func (u Lease) Clone() Lease

Clone returns a deep copy: the slice fields are the only shared state, so copying them is what makes a value handed out of a store safe to keep. slices.Clone preserves nil, so a row that stored null does not come back as [].

type LeaseBaseVerdict added in v0.4.0

type LeaseBaseVerdict string

LeaseBaseVerdict says how the base a worker reported at registration compares with the Checkpoint its lease was handed. A FACT computed at that moment, never a refusal: a diverged worker is registered like any other and told what diverged.

The middle value is why this is not a boolean. A checkpoint is a revision PLUS a dirty-patch digest, so two trees can share a revision and hold different uncommitted work; that is neither agreement nor the kind of divergence a respawn fixes, and folding it into either one sends the worker to the wrong remedy.

const (
	// BaseMatch is a reported base identical to the checkpoint, digest included.
	BaseMatch LeaseBaseVerdict = "match"
	// BaseRevisionMatch is the same revision carrying a different uncommitted patch.
	BaseRevisionMatch LeaseBaseVerdict = "revision-match"
	// BaseDiverged is a different revision: the worker is not on the tree it was handed.
	BaseDiverged LeaseBaseVerdict = "diverged"
	// BaseUnknown is a registration with nothing to compare against, because the lease was
	// declared without a Checkpoint. Distinct from BaseMatch on the same ground
	// StateNoReturn is distinct from StateFail - claiming agreement nobody observed is a
	// judgment the ledger did not make.
	BaseUnknown LeaseBaseVerdict = "unknown"
)

type LeaseOverlap added in v0.4.0

type LeaseOverlap struct {
	// LeaseA and LeaseB are the lease ids, in ledger order - LeaseA was recorded first.
	LeaseA string `json:"lease_a" yaml:"lease_a"`
	LeaseB string `json:"lease_b" yaml:"lease_b"`
	// PathsA and PathsB are the intersecting declarations from each side, deduped and
	// kept apart. They are rarely the same string - "internal/ledger" and
	// "internal/ledger/store.go" intersect - so one merged list left a reader unable to
	// tell which lease claimed which, which is the only thing they can act on.
	PathsA []string `json:"paths_a" yaml:"paths_a"`
	PathsB []string `json:"paths_b" yaml:"paths_b"`
}

LeaseOverlap is two leases whose declared OwnedPaths intersect. A FACT the reader is handed, never a verdict: two leases may share a path because their author meant them to run in sequence, or because nobody noticed. Nothing here blocks, gates, or reorders anything.

func (LeaseOverlap) BuzzObject added in v0.4.0

func (v LeaseOverlap) BuzzObject() BuzzObject

type LeaseRelease added in v0.4.0

type LeaseRelease struct {
	Path   string `json:"path"   yaml:"path"`
	Digest string `json:"digest" yaml:"digest"`
	// ReleasedAt is unix seconds, stamped by the store on the put that dropped the path.
	ReleasedAt int64 `json:"released_at" yaml:"released_at"`
}

LeaseRelease is one path a lease stopped owning, and the version of it the next agent inherits.

The skill has workers release a contested path as soon as they finish EDITING it rather than at exit, so a waiter can start against it during validation. Digest is what makes that safe to act on: it identifies the file the releaser left behind, and a mismatch at verification time means the waiter built on a tree the releaser never saw.

func (LeaseRelease) BuzzObject added in v0.4.0

func (v LeaseRelease) BuzzObject() BuzzObject

type LeaseReport added in v0.4.0

type LeaseReport struct {
	Leases   []Lease        `json:"leases"             yaml:"leases"`
	Overlaps []LeaseOverlap `json:"overlaps,omitempty" yaml:"overlaps,omitempty"`
}

LeaseReport is what a reader of the ledger is served: the recorded rows, plus the overlaps derived from them. A constructor rather than a literal at each read door, because the MCP tool and the console's route must not be able to disagree about whether an overlap exists - the same reason types.NewFileReport exists.

func NewLeaseReport added in v0.4.0

func NewLeaseReport(leases []Lease) LeaseReport

NewLeaseReport wraps the rows and derives the overlaps. Derived on READ and never stored: an overlap is a relation between two rows, so storing it on either one would mean a row that stopped being true when its neighbor changed.

The registration facts take the opposite route and are NOT derived here. ReportedBase, BaseVerdict and Registered describe one row against the checkpoint that row was handed, so they belong on the row, are computed once when the worker registers, and reach every reader of this report - magus_ledger's list op, the console's /api/v1/ledger - by riding the leases. Deriving a second copy at read time would be a duplicate to keep true, which is exactly what the overlap rule above avoids in the other direction.

The single door onto a report, which is why the empty case is normalized HERE: an unwritten ledger serves "leases":[] rather than null, and the MCP tool and the HTTP route would otherwise each have to decide that for themselves.

func (LeaseReport) BuzzObject added in v0.4.0

func (v LeaseReport) BuzzObject() BuzzObject

type LeaseState added in v0.4.0

type LeaseState string

LeaseState is where one lease stands. The three terminal values are the point of the set: a row that never reaches one is a row nobody closed.

NoReturn is deliberately distinct from Fail. A worker that died, stalled, or was killed produced no verdict at all, and folding that into "failed" claims a judgment nobody made - the root agent still has to go look. Silence is not a pass, and it is not a failure either.

const (
	// StateDeclared is a row written before its worker was spawned.
	StateDeclared LeaseState = "declared"
	// StateRunning is a worker in flight.
	StateRunning LeaseState = "running"
	// StatePass is a lease whose acceptance criteria and assigned validation both
	// passed, as judged by the agent that owns it.
	StatePass LeaseState = "pass"
	// StateFail is a lease that returned and did not meet its criteria.
	StateFail LeaseState = "fail"
	// StateNoReturn is a lease that never reported: dead, stalled, or cancelled.
	StateNoReturn LeaseState = "no_return"
)

type LeaseUnattributedWrite added in v0.4.0

type LeaseUnattributedWrite struct {
	Path string `json:"path"   yaml:"path"`
	// Digest is the content AFTER the write, on the same three-marker vocabulary as
	// LeaseRelease.Digest: a hash, or DigestAbsent / DigestDir / DigestUnreadable.
	Digest string `json:"digest" yaml:"digest"`
	// At is unix seconds, stamped by the store.
	At int64 `json:"at" yaml:"at"`
}

LeaseUnattributedWrite is one path a lease owns that somebody outside it wrote, and the content that writer left behind.

The inverse of LeaseRelease, and the half that was missing. A release is a worker saying "I am done with this, here is what I left"; this is magus saying "somebody who is not you changed this, here is what is there now" - so a lease that read the file earlier can find out by ASKING rather than by being told, and a digest that no longer matches what it read is the whole signal.

UNATTRIBUTED is the honest word and the reason this is not called a handback. magus knows only that the writer named no live lease; a person editing in their own checkout and an agent that forgot to export its id are indistinguishable here, and the guard says so in as many words. Naming a human would be a claim magus cannot support.

func (LeaseUnattributedWrite) BuzzObject added in v0.4.0

func (v LeaseUnattributedWrite) BuzzObject() BuzzObject

type LogLevel added in v0.4.0

type LogLevel string

LogLevel names the severity a log\at call emits at.

A named type with a declared case list rather than a bare string, matching SignAlgorithm and PlatformStyle: a level is a closed set, and a typo in one should be a checker error rather than a message that silently never prints.

The cases are magus's own levels, not slog's: magus adds `trace` below debug (see config.LevelTrace) for the `-vvv` tier, so a magusfile can reach every verbosity the CLI exposes.

const (
	// LogTrace is the -vvv tier: detail useful when reconstructing what a run did,
	// and noise at any other time.
	LogTrace LogLevel = "trace"
	// LogDebug is the -v tier.
	LogDebug LogLevel = "debug"
	// LogInfo is the default tier: what a run says when nothing is wrong.
	LogInfo LogLevel = "info"
	// LogWarn reports something the reader should act on eventually.
	LogWarn LogLevel = "warn"
	// LogError reports something that already went wrong. It does NOT fail the
	// target - raising does that; this only records.
	LogError LogLevel = "error"
)

func (LogLevel) String added in v0.4.0

func (v LogLevel) String() string

String renders v for an error message: the value, or "unset" when empty.

func (LogLevel) Valid added in v0.4.0

func (v LogLevel) Valid() bool

Valid reports whether v is a declared LogLevel. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (LogLevel) Values added in v0.4.0

func (v LogLevel) Values() []string

Values lists the LogLevel values a caller may choose, excluding the zero value.

type MCPEndpointStatus added in v0.2.0

type MCPEndpointStatus struct {
	Enabled   bool   `json:"enabled" yaml:"enabled"`
	Address   string `json:"address,omitempty" yaml:"address,omitempty"`
	URL       string `json:"url,omitempty" yaml:"url,omitempty"`
	Reachable bool   `json:"reachable" yaml:"reachable"`
	State     string `json:"state" yaml:"state"`
	Note      string `json:"note,omitempty" yaml:"note,omitempty"`
}

MCPEndpointStatus is the runtime health of the MCP HTTP endpoint agent hosts connect to. State is one of: serving (listening and a workspace is loaded), not-ready (listening but no workspace yet), unreachable (nothing is listening), or disabled (mcp.enabled=false).

type MachineClaim added in v0.4.0

type MachineClaim struct {
	Project    string `json:"project" yaml:"project"`
	Target     string `json:"target" yaml:"target"`
	DeclaredBy string `json:"declared_by,omitempty" yaml:"declared_by,omitempty"` // absent when that is Target itself
	MemoryMB   int    `json:"memory_mb,omitzero" yaml:"memory_mb,omitempty"`
	Slots      int    `json:"slots,omitzero" yaml:"slots,omitempty"`
	PID        int    `json:"pid" yaml:"pid"`
	// Dir is where the claiming run was started. One daemon serves every worktree, so a
	// pid alone does not say which tree to go and look at.
	Dir string `json:"dir,omitempty" yaml:"dir,omitempty"`
	// Invocation is this run's own reference, recorded so a DESCENDANT magus can tell a
	// claim it runs underneath from one competing with it.
	Invocation string `json:"invocation,omitempty" yaml:"invocation,omitempty"`
	// Ancestors is the invocations this claim runs underneath, which do not count
	// against it.
	Ancestors []string `json:"ancestors,omitempty" yaml:"ancestors,omitempty"`
}

MachineClaim is one step's request on the machine-wide admission budget.

It crosses the proc socket AND lands in `magus status`, which is why it lives here rather than in the package that arbitrates it: the engine, the daemon, and every status renderer read one definition. Two shapes projected onto each other drifted their field names within a week of being written.

Numeric fields use omitzero because the jsonv2 codec's omitempty does not omit 0.

type MachineClaimant added in v0.4.0

type MachineClaimant struct {
	Project  string    `json:"project" yaml:"project"`
	Target   string    `json:"target" yaml:"target"`
	PID      int       `json:"pid,omitzero" yaml:"pid,omitempty"`
	MemoryMB int       `json:"memory_mb,omitzero" yaml:"memory_mb,omitempty"`
	Slots    int       `json:"slots,omitzero" yaml:"slots,omitempty"`
	Dir      string    `json:"dir,omitempty" yaml:"dir,omitempty"`
	Since    time.Time `json:"since,omitempty" yaml:"since,omitempty"`
}

MachineClaimant is one step's hold on, or place in the queue for, the machine budget.

One type for both because a reader asks the same question of each - who is this, and where - and the only difference is which list it appears in. Since is when the claim was granted, or when the waiter first asked.

type MachineSnapshot added in v0.4.0

type MachineSnapshot struct {
	BudgetMB    int `json:"budget_mb,omitzero" yaml:"budget_mb,omitempty"`
	HeldMB      int `json:"held_mb,omitzero" yaml:"held_mb,omitempty"`
	BudgetSlots int `json:"budget_slots,omitzero" yaml:"budget_slots,omitempty"`
	HeldSlots   int `json:"held_slots,omitzero" yaml:"held_slots,omitempty"`
	// Holders are the steps running against the budget; Waiters are the ones queued for
	// it, oldest first.
	Holders []MachineClaimant `json:"holders,omitempty" yaml:"holders,omitempty"`
	Waiters []MachineClaimant `json:"waiters,omitempty" yaml:"waiters,omitempty"`
}

MachineSnapshot is the whole machine budget: what it is, what is spent, and the claims spending it. It is what `magus status` reports and what the daemon serves.

type MachineVerdict added in v0.4.0

type MachineVerdict struct {
	// Granted means the claim is recorded and ID releases it.
	Granted bool   `json:"granted" yaml:"granted"`
	ID      string `json:"id,omitempty" yaml:"id,omitempty"`
	// Fits is false when no state of the machine admits this claim, so waiting is a
	// hang rather than a queue.
	Fits        bool              `json:"fits" yaml:"fits"`
	Holders     []MachineClaimant `json:"holders,omitempty" yaml:"holders,omitempty"`
	Ahead       int               `json:"ahead,omitzero" yaml:"ahead,omitempty"` // waiters queued in front of this one
	BudgetMB    int               `json:"budget_mb,omitzero" yaml:"budget_mb,omitempty"`
	HeldMB      int               `json:"held_mb,omitzero" yaml:"held_mb,omitempty"`
	BudgetSlots int               `json:"budget_slots,omitzero" yaml:"budget_slots,omitempty"`
	HeldSlots   int               `json:"held_slots,omitzero" yaml:"held_slots,omitempty"`
}

MachineVerdict is the budget's answer to one admission request.

type MergeDriverInstaller

type MergeDriverInstaller interface {
	InstallMergeDriver(ctx context.Context, root string, outputGlobs []string) error
	CheckMergeDriver(ctx context.Context, root string) (bool, error)
	// EnsureMergeDriver re-installs only when the registration is missing or the
	// declared globs have moved on, reporting whether it changed anything. Callers
	// run it routinely, so it must be cheap and silent in the steady state.
	EnsureMergeDriver(ctx context.Context, root string, outputGlobs []string) (bool, error)
}

MergeDriverInstaller is an optional capability for VCSDriver implementations that can register magus as the merge driver for declared output globs.

type MergeStarter added in v0.4.0

type MergeStarter interface {
	// StartMerge begins a merge of ref into the working tree WITHOUT committing it,
	// leaving any conflicts recorded for ConflictResolver.Conflicts to report. A merge
	// that completes cleanly is not an error, and neither is one that conflicts: both
	// leave an operation in progress for the caller to conclude. A merge that could not
	// be started at all (an unknown ref, an operation already underway) is.
	StartMerge(ctx context.Context, root, ref string) error
	// AbortMerge abandons the in-progress merge, restoring the tree to its pre-merge
	// state. Callers start from a clean tree so this cannot discard uncommitted work.
	AbortMerge(ctx context.Context, root string) error
}

MergeStarter is an optional capability for VCSDriver implementations that can BEGIN a merge against a ref without committing it, and abandon one they began. Callers type-assert for it and degrade gracefully when a backend lacks it.

It exists because conflict resolution was reactive-only: ConflictResolver.Conflicts reads an operation already in progress, so settling a branch against its base meant the caller ran the merge itself first. That put the tricky half - which merge, with what flags, and how to back out - in whatever shell script was driving, which is exactly where CI has no good place to put it.

type ModuleEntry

type ModuleEntry struct {
	Name    string              `json:"name"              yaml:"name"`
	Doc     string              `json:"doc,omitempty"     yaml:"doc,omitempty"`
	Fields  []ModuleFieldEntry  `json:"fields,omitempty"  yaml:"fields,omitempty"`
	Methods []ModuleMethodEntry `json:"methods,omitempty" yaml:"methods,omitempty"`
}

ModuleEntry is a module's summary; Fields/Methods are populated only for the detail view.

func (ModuleEntry) BuzzObject added in v0.4.0

func (v ModuleEntry) BuzzObject() BuzzObject

type ModuleFieldEntry

type ModuleFieldEntry struct {
	Name string `json:"name"          yaml:"name"`
	Type string `json:"type"          yaml:"type"`
	Doc  string `json:"doc,omitempty" yaml:"doc,omitempty"`
}

ModuleFieldEntry is one static, table-level value on a module (e.g. vcs.name).

func (ModuleFieldEntry) BuzzObject added in v0.4.0

func (v ModuleFieldEntry) BuzzObject() BuzzObject

type ModuleMethodEntry

type ModuleMethodEntry struct {
	Name       string `json:"name"                  yaml:"name"`
	Doc        string `json:"doc,omitempty"         yaml:"doc,omitempty"`
	Buzz       string `json:"buzz"                  yaml:"buzz"`
	BuzzStdlib string `json:"buzz_stdlib,omitempty" yaml:"buzz_stdlib,omitempty"`
}

ModuleMethodEntry is one method of a module, with its Buzz call form.

func (ModuleMethodEntry) BuzzObject added in v0.4.0

func (v ModuleMethodEntry) BuzzObject() BuzzObject

type ModuleReport added in v0.4.0

type ModuleReport struct {
	Definition string        `json:"definition" yaml:"definition"`
	Count      int           `json:"count"      yaml:"count"`
	Modules    []ModuleEntry `json:"modules"    yaml:"modules"`
}

ModuleReport is the "describe module[s]" envelope.

type NearCycle

type NearCycle struct {
	From, To string
	BackPath []string
}

NearCycle describes a pair where adding From→To would close a cycle.

type Node

type Node struct {
	Path        string   `json:"path" yaml:"path"`
	Name        string   `json:"name" yaml:"name"`
	SpellName   string   `json:"spell_name,omitempty" yaml:"spell_name,omitempty"`
	Children    []string `json:"children" yaml:"children"`
	Dir         string   `json:"dir,omitempty" yaml:"dir,omitempty"`
	Exclusive   bool     `json:"exclusive,omitempty" yaml:"exclusive,omitempty"`
	BlastRadius int      `json:"blast_radius,omitempty" yaml:"blast_radius,omitempty"`
	DurationMs  int64    `json:"duration_ms,omitempty" yaml:"duration_ms,omitempty"`
	// Churn, Authors, and LastCommit are populated by the churn heatmap and omitted
	// by the plain dependency graph: how many recent commits touched the project,
	// how many distinct authors made them, and when the most recent one landed.
	Churn      int        `json:"churn,omitempty" yaml:"churn,omitempty"`
	Authors    int        `json:"authors,omitempty" yaml:"authors,omitempty"`
	LastCommit *time.Time `json:"last_commit,omitempty" yaml:"last_commit,omitempty"`
}

Node is a single project node in a structured graph output.

func (Node) BuzzObject added in v0.4.0

func (v Node) BuzzObject() BuzzObject

func (Node) Label added in v0.4.0

func (n Node) Label() string

Label is the human-facing project name. Path remains the stable machine key.

type NoopObserver

type NoopObserver struct{}

NoopObserver discards every event.

func (NoopObserver) OnBuild

func (NoopObserver) OnBuild(BuildStats)

func (NoopObserver) OnError

func (NoopObserver) OnError(error)

func (NoopObserver) OnQuery

func (NoopObserver) OnQuery(QueryEvent)

type Observer

type Observer interface {
	OnBuild(BuildStats)
	OnQuery(QueryEvent)
	OnError(error)
}

Observer receives structured events from a Graph. Implementations must be safe for concurrent calls.

func FanOut

func FanOut(obs ...Observer) Observer

FanOut returns an Observer forwarding to every non-nil observer.

func GraphObserverFromContext

func GraphObserverFromContext(ctx context.Context) Observer

GraphObserverFromContext returns the request-scoped observer, or nil.

type OutputRef added in v0.4.0

type OutputRef struct {
	Project string `json:"project,omitempty" yaml:"project,omitempty"`
	Glob    string `json:"glob" yaml:"glob"`
}

OutputRef names one file output a target declares via ctx.writesFiles, in the same shape as InputRef: Project is the OWNING project (the tree written into) and Glob is relative to that root. For a same-project output (ctx.writesFiles("glob")) Project is empty at extraction and filled to the project's own path when resolved.

A separate type from InputRef despite the identical shape, because the dependency edge each implies runs the OTHER WAY. A cross-project INPUT means "I read you, so I run after you". A cross-project OUTPUT means "I write your tree, so YOU run after ME" - the owner gains the edge, not the declarer. Sharing one type would let a caller pass an input where an output belongs and silently invert a build order.

func (OutputRef) BuzzObject added in v0.4.0

func (v OutputRef) BuzzObject() BuzzObject

type OwnershipEntry added in v0.4.0

type OwnershipEntry struct {
	Path         string    `json:"path"                   yaml:"path"`
	Name         string    `json:"name"                   yaml:"name"`
	Commits      int       `json:"commits"                yaml:"commits"`
	Authors      int       `json:"authors"                yaml:"authors"`
	Primary      string    `json:"primary"                yaml:"primary"`
	PrimaryShare int       `json:"primary_share"          yaml:"primary_share"` // percent
	BusFactor1   bool      `json:"bus_factor_1,omitempty" yaml:"bus_factor_1,omitempty"`
	Stale        bool      `json:"stale,omitempty"        yaml:"stale,omitempty"`
	LastCommit   time.Time `json:"last_commit,omitempty"  yaml:"last_commit,omitempty"`
}

OwnershipEntry is one project's authorship: how many distinct authors touched it, who touched it most (and their share), whether it is bus-factor-1 (a single author), and whether it has gone quiet in the recent half of the window (abandonment risk).

func (OwnershipEntry) BuzzObject added in v0.4.0

func (v OwnershipEntry) BuzzObject() BuzzObject

func (OwnershipEntry) Label added in v0.4.0

func (o OwnershipEntry) Label() string

type OwnershipOutput

type OwnershipOutput struct {
	Definition string           `json:"definition" yaml:"definition"`
	Commits    int              `json:"commits"    yaml:"commits"`
	Since      string           `json:"since,omitempty" yaml:"since,omitempty"`
	Projects   []OwnershipEntry `json:"projects"   yaml:"projects"`
}

OwnershipOutput reports author concentration per project — the knowledge-risk view.

func (OwnershipOutput) BuzzObject added in v0.4.0

func (v OwnershipOutput) BuzzObject() BuzzObject

type Path added in v0.4.0

type Path struct {
	Value string `json:"value"`
	Base  string `json:"base,omitempty"`
	IsDir bool   `json:"is_dir,omitempty" buzz:"isDir"`
}

Path is magus's lexical filesystem reference: a value plus the directory it is measured from, so a path is self-describing rather than only meaningful to whoever produced it.

Base is what makes this worth having over a bare string. Every path magus hands out is relative to SOMETHING - a VCS status path to the repository root, a glob result to the pattern's root, a footprint entry to the project directory - and a target's cwd is its project directory, not the workspace root. A []string forces every consumer to know, out of band, which of those it was handed, and to be right; the failure is silent and looks like a missing file. Carrying the base makes Resolve total: it needs no argument, so there is no argument to get wrong.

Base is empty when Value is already absolute, or when the producer genuinely does not know - which is itself information a caller can test for, where a bare string offered no way to ask.

IsDir describes the intended filesystem kind when it is known. A directory is still a filesystem entry, so FileInfo remains the observed stat result rather than a competing path abstraction.

func (Path) BuzzObject added in v0.4.0

func (p Path) BuzzObject() BuzzObject

BuzzObject is the Buzz boundary map a Path crosses as: {value, base, isDir}.

Base rides along deliberately. A Buzz caller that receives a path from vcs.status or a glob needs to know what it is measured from to open it, and the object mirror carries no methods - so the field is the only place that fact can live.

func (Path) Rebase added in v0.4.0

func (p Path) Rebase(base string) Path

Rebase returns p measured from base instead of its current one. This is the deliberate way to reinterpret a path, as opposed to Resolve's no-argument form: a caller that genuinely means "treat this as relative to somewhere else" says so.

func (Path) RelativeTo added in v0.4.0

func (p Path) RelativeTo(base string) (Path, error)

RelativeTo returns p expressed from base, carrying base as its new Base so the result is still self-describing. Resolving first is what makes this correct for a path whose current Base differs from the requested one.

func (Path) Resolve added in v0.4.0

func (p Path) Resolve() Path

Resolve returns p as an absolute path, retaining its kind. It takes no base: a Path carries its own. An absolute Value is cleaned and returned unchanged; an empty Value remains empty so optional paths do not accidentally become their base directory. The result has an empty Base, since an absolute path is measured from nothing.

type PatternType

type PatternType string

PatternType identifies the matching strategy for an IgnorePattern. Glob is the default; regex and literal are escape hatches for cases globs cannot express.

const (
	PatternGlob    PatternType = "glob"    // doublestar `**`-aware glob; default for bare CLI values
	PatternRegex   PatternType = "regex"   // Go regexp; for rules globs cannot express
	PatternLiteral PatternType = "literal" // matches any path segment at any depth (like .gitignore bare entry)
)

func (PatternType) String added in v0.4.0

func (v PatternType) String() string

String renders v for an error message: the value, or "unset" when empty.

func (PatternType) Valid added in v0.4.0

func (v PatternType) Valid() bool

Valid reports whether v is a declared PatternType. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (PatternType) Values added in v0.4.0

func (v PatternType) Values() []string

Values lists the PatternType values a caller may choose, excluding the zero value.

type Person

type Person struct {
	Name  string
	Email string
}

Person identifies who authored a revision.

type PlatformStyle added in v0.4.0

type PlatformStyle string

PlatformStyle names the naming convention a platform\arch or platform\os call renders its canonical answer in.

A named type with a declared case list rather than a bare string, for the same reason as SignAlgorithm: renderPlatform rejects an unknown style at RUN time ("unknown style %q (want go|uname)"), which lands halfway through whatever release or container job asked for it. Declared here, a typo is a checker error at load. The registry between here and Buzz is cmd/magus-utils/boundary_types.go; adding a case there and here is what makes the validator, the error message, and the Buzz mirror all follow.

The zero value means "unset" and renders the Go form, which is why the argument stays optional: platform\arch("x86_64") and platform\arch("x86_64", .go) agree.

const (
	// PlatformStyleGo renders canonical Go GOOS/GOARCH spellings (darwin, amd64).
	// It is the default, and what the rest of magus indexes platforms by.
	PlatformStyleGo PlatformStyle = "go"
	// PlatformStyleUname renders the spellings uname -m / uname -s report
	// (x86_64, aarch64, Darwin) - the form a shell script or a download URL
	// built for a release asset usually wants.
	PlatformStyleUname PlatformStyle = "uname"
)

func (PlatformStyle) String added in v0.4.0

func (v PlatformStyle) String() string

String renders v for an error message: the value, or "unset" when empty.

func (PlatformStyle) Valid added in v0.4.0

func (v PlatformStyle) Valid() bool

Valid reports whether v is a declared PlatformStyle. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (PlatformStyle) Values added in v0.4.0

func (v PlatformStyle) Values() []string

Values lists the PlatformStyle values a caller may choose, excluding the zero value.

type Project

type Project struct {
	Path string // repo-relative directory, forward slashes (e.g. "api", ".")
	// Name is the declared human label from magus.project's "name" key, or "" to
	// derive one from the path. It exists for the ROOT project, whose path is "."
	// and whose label would otherwise fall back to the checkout's directory
	// basename - so a worktree, a clone under a different name, or a CI checkout
	// each renamed the root project and rewrote every generated index that names
	// it. Declaring the name makes generated output reproducible anywhere.
	Name string
	// Origin is what put this project in the workspace (see ProjectOrigin). It is
	// PROVENANCE, never identity - nothing dispatches on it - and it exists because a
	// provided project has no file to point at, so "where did this come from" would
	// otherwise be unanswerable for exactly the projects a reader has never seen
	// declared anywhere. Named Origin rather than Source because Sources below is the
	// unrelated glob list, and one letter is not enough distance between "where this
	// project came from" and "the files it is built from".
	Origin    ProjectOrigin
	Dir       string // absolute filesystem path
	Spell     string // primary spell name; use Spells for fan-out dispatch
	Spells    []string
	Bindings  []*Binding // parallel to Spells, in registration order
	Sources   []string   // doublestar globs relative to Dir for the cache key
	Outputs   []string   // doublestar globs snapshotted into and replayed from cache
	DependsOn []string
	Exclusive bool
	// NoLanguage is the reason a project binds no toolchain spell ON PURPOSE, from
	// magus.project's "no_language" key. A spell-less project is legal and common, so
	// doctor's language-coverage check cannot tell an intentional one (a polyglot
	// harness no single pack describes) from a real gap (someone forgot to import the
	// go spell) without being told. Carrying the REASON rather than a bare bool is what
	// keeps the opt-out honest: it has to say what it is instead of silencing a check.
	NoLanguage string
	// ToolBounds is the version window THIS project requires of each binary its spells
	// drive, keyed by bin name, from magus.project's "tools" key. Intersected with what
	// the spell itself declares, narrower bound winning on each side, so neither can
	// loosen the other. The intersection happens once at run start, in checkToolWindows -
	// NOT at op dispatch, so a project whose targets never dispatch a spell op is held to
	// its window all the same.
	//
	// On the project rather than in magus.yaml, and that is not a filing preference.
	// config.Load merges a user-global tier ($XDG_CONFIG_HOME/magus/) beneath the
	// workspace, so a bound living there could be set in one person's private file and
	// silently gate every workspace on their machine. A magusfile is committed, is read
	// by everyone who reads the project, and is per project - which also means `console`
	// and `docs` can hold different policies instead of sharing one workspace-wide map.
	//
	// Sharing is an explicit import of a shared MODULE, never ambient inheritance: see
	// tools/toolchain-policy.buzz, imported the same way tools/audit.buzz already is.
	// There is no special root project and nothing is inherited by position in the tree.
	//
	// Deliberately not `import "project/.." as root`. That handle exposes only a
	// project's `export fun` targets, read statically from the AST so an import can
	// never trigger a VM load and recurse; an exported VALUE there reads as null and
	// this key would be silently skipped. A policy several projects share is a shared
	// module, not a side effect of one project's magusfile.
	ToolBounds map[string]spells.VersionBounds
	// ReviewRequired are the globs where a person actually reading a change matters, from
	// magus.project's "review_required" key. Empty is the default and means magus reports
	// read receipts without singling anything out.
	//
	// It exists so the finding can be QUIET. "Nobody read this" is true of nearly every
	// file in nearly every changeset, and a report that says so everywhere is one people
	// learn to skip - taking the signing code and the cache-key logic with it. Naming the
	// few places where an unread change is a real risk is what makes the report worth
	// reading, and only the workspace knows which those are.
	//
	// Declared, never inferred. magus could guess from churn or from a security-sounding
	// path, and a guess here would be magus asserting whose code is dangerous - which is
	// the judgment this key exists to leave with the people who own it.
	ReviewRequired []string
	WatchIgnores   []IgnorePattern
	TargetPolicies map[string]Target // per-target execution policy; values carry only the policy fields of Target
	// TargetInputs are per-target file inputs declared in a target body via
	// ctx.readsFiles(...), keyed by normalized target name (DefaultTargetNameNormalizer,
	// matching the TargetPolicies key space buildStep looks up). ONE representation
	// covers both a same-project glob and a cross-project file: each InputRef carries its
	// owning project (workspace-relative once resolved) and the glob/file relative to
	// that project. When present they DEFINE the target's file footprint: buildStep
	// retains the owning magusfiles and target-specific spell sources, then folds these
	// refs to workspace-relative globs via path.Join(Project, Rel). A cross-project input's owning project
	// is also unioned into DependsOn so a change to it marks this project affected; a
	// same-project input needs no such edge (it seeds by directory containment).
	// Populated statically at load from describe.Extract.
	TargetInputs map[string][]InputRef
	// TargetOutputs are per-target ctx.writesFiles refs. When a target has any, they
	// replace the broad project/spell output baseline for that target's replay set.
	TargetOutputs map[string][]OutputRef
	// TargetUpdates are per-target ctx.modifiesExistingFiles refs: existing files the
	// target changes in place.
	// Deliberately absent from AllOutputs, which is what makes magus clean skip them and
	// the cache neither snapshot nor replay them. See types.UpdateRef.
	TargetUpdates map[string][]UpdateRef
	// TargetExecOverrides are per-target ctx.withEnv / ctx.withCwd overrides, in
	// declaration order, folded into the cache key. See TargetGraphNode.ExecOverrides.
	TargetExecOverrides map[string][]string
	// MagusfileTargets are the target names this project's magusfile exports, normalized.
	// They live here rather than on the magusfile spell because that spell is ONE global
	// instance shared by every project, so it cannot know what any particular magusfile
	// declares - which is why its Targets() is empty and why nothing could previously ask
	// whether a magusfile shadows a spell op of the same name.
	MagusfileTargets []string
	// TargetCrossDeps are the cross-project targets each target depends on, declared
	// via a project import (<alias>.<target>). The descendant-write audit reads them:
	// when a parent target depends on a target INSIDE a descendant project, the writes
	// that descendant makes are its own, not the parent reaching across a boundary.
	TargetCrossDeps map[string][]CrossTargetRef
	// TargetChains are each composed target's ctx.needs steps in INVOCATION ORDER, local
	// and cross-project alike. TargetCrossDeps above answers a different question (which
	// other projects a target reaches into) and drops both the ordering and every local
	// step, so it cannot serve this one. See TargetGraphNode.Chain.
	TargetChains map[string][]ChainStep
	// TargetEnvAllow are per-target ctx.env declarations: env var NAMES whose process
	// values fold into the cache key. See TargetGraphNode.EnvAllow.
	TargetEnvAllow map[string][]string
	// TargetObservations are per-target ctx.observes declarations: external facts the
	// target's answer depends on, as "key=value", folded into the cache key. See
	// TargetGraphNode.Observations.
	TargetObservations map[string][]string
	// InboundOutputs are output globs OTHER projects declare INTO this project's tree
	// via ctx.writesFiles(<alias>.file(...)), keyed by the WRITING project's path. Globs are
	// relative to THIS project's root, so they compose with Outputs directly - which is
	// the whole reason they are filed here rather than left on the writer, whose own
	// globs are relative to a different root. Without this a cross-project output would
	// be invisible to every consumer that asks a project what lands in its tree: clean,
	// watch's rebuild-loop guard, ownership lookup, and the merge driver. The writer key
	// is what lets the merge driver regenerate the file, since only the writer can.
	// Populated at load, after the walk, once every project is known (the owner may not
	// be discovered yet when the writer declares it).
	InboundOutputs map[string][]string
	ResolvedSpells []*spells.Spell // set at the end of magus.Open; immutable thereafter
}

Project is the record magus maintains for every directory with a marker file.

func (*Project) AllOutputs added in v0.2.0

func (p *Project) AllOutputs() []string

AllOutputs is every output glob that lands in this project's tree, deduplicated and PROJECT-ROOT RELATIVE: the project-wide Outputs, every per-target ctx.writesFiles glob this project declares for itself (TargetOutputs), and every glob another project declares into it (InboundOutputs). It is the "what files appear in this tree" view - consumed by `magus clean --outputs`, watch's rebuild-loop guard, output-ownership lookup, and the merge driver - as opposed to the per-target cache view (buildStep's step.Outputs), which stays scoped to the one target being run.

A cross-project ref in TargetOutputs is skipped here and counted on the OWNER instead, through that project's InboundOutputs: its glob is relative to the tree it writes into, so returning it from the writer would have every caller resolve it against the wrong root. The two halves meet on the owner, where the glob is already relative.

The result never aliases p.Outputs. Callers treat it as their own slice (the merge driver builds workspace-relative globs from it, clean ranges it), and p.Outputs carries spare capacity from AttachSpell, so handing the live backing array out of an exported method lets one append reach into the project record.

Contributions are sorted so the result is deterministic despite the maps.

func (*Project) AttachSpell

func (p *Project) AttachSpell(spell *spells.Spell)

AttachSpell associates spell with p without applying registration overrides.

func (*Project) DeclaredGlobs added in v0.4.0

func (p *Project) DeclaredGlobs() []string

DeclaredGlobs is every glob this project declares, rooted at the WORKSPACE rather than at the project: the project-wide Sources and AllOutputs, plus the per-target ctx.readsFiles, ctx.writesFiles, and ctx.modifiesExistingFiles refs, each anchored on the project its glob is relative to. Sorted and deduplicated.

It answers "does this project declare that path", which is the question affected attribution asks before falling back to directory containment, and the one doctor asks about the tree standing still. The rooting goes through RootGlob, which is also what the cache step and `magus describe file` root with, so the three agreeing is a shared function rather than three parallel implementations that happen to match. Measured, they do not: plain concatenation leaves a reaching "../" glob at a path nothing can match, while joining the same glob with filepath.Join resolves it.

Dedup here is string equality on the ROOTED form, so two spellings that resolve to one path collapse to one entry - a project-wide "../proto/**" and a per-target ctx.readsFiles of proto's "**" are the same declaration and count once.

Deliberately NOT the magusfile globs the cache step layers on top. Every project's key carries the ROOT magusfile, so counting those here would make one magusfile edit read as a declaration by every project in the workspace - and attribution would then seed all of them where directory containment seeds exactly one.

type ProjectEntry

type ProjectEntry struct {
	Path string `json:"path"                yaml:"path"`
	// Name is the project's DECLARED name (magus.project's "name" key), empty when
	// it declares none. Carried on the boundary because a consumer rendering a
	// human label has to prefer it over the directory basename - without it,
	// `magus ls` printed the checkout directory ("agent-harness-handoff-92f105" in
	// a worktree) while MAGUS.md, built from the same workspace, printed "magus".
	Name string `json:"name,omitempty"      yaml:"name,omitempty"`
	// Origin is what put this project in the workspace: "magusfile", or
	// "provider:<spell>" for one a workspace provider reported. It is on the boundary
	// because a provided project has no file to open - without it, `magus describe
	// project libs/foo` describes a project whose declaration the reader cannot find.
	//
	// Plain string, not the ProjectOrigin the engine carries: a boundary record is
	// data (Buzz and JSON both see a str either way), and the named type exists to
	// stop a Go caller comparing against a prefix, which no wire consumer can do.
	Origin string   `json:"origin,omitempty"    yaml:"origin,omitempty"`
	Dir    string   `json:"dir"                 yaml:"dir"`
	Spell  string   `json:"spell,omitempty"     yaml:"spell,omitempty"`
	Spells []string `json:"spells,omitempty"    yaml:"spells,omitempty"`
	// Sources and Outputs are the DECLARED globs, project-relative (as written in
	// the magusfile/spell). EvaluatedProject populates these same fields (via its
	// embedded ProjectEntry) with the RESOLVED, workspace-rooted globs instead
	// (joined against the project path, plus the magusfile's own globs folded into
	// Sources) - the same name, a different representation, because the evaluated
	// view answers "what does the cache key actually see" rather than "what was
	// written".
	Sources   []string `json:"sources,omitempty"    yaml:"sources,omitempty"`
	Outputs   []string `json:"outputs,omitempty"    yaml:"outputs,omitempty"`
	DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty" buzz:"dependsOn"`
	Exclusive bool     `json:"exclusive,omitempty"  yaml:"exclusive,omitempty"`
	// Manifests lists this project's spells' version-manifest candidates
	// (spells.Spell.Manifests), filtered to the ones that actually exist in Dir and
	// kept in declared order - so element 0, when present, is "the" manifest under
	// the first-existing-file-wins rule. A project with no manifest-declaring spell,
	// or none of whose candidates exist, has an empty list: it carries no version of
	// its own.
	Manifests []string `json:"manifests,omitempty" yaml:"manifests,omitempty"`
	// Lockfiles lists the lockfile each entry in Manifests actually resolves into
	// (spells.Manifest.LockCandidates), found by walking from Dir up to the workspace
	// root and taking the first candidate that exists. Empty when the ecosystem has no
	// lockfile, when none has been written yet, or when the manifest is one that does
	// not lock (setup.py).
	//
	// These are WORKSPACE-RELATIVE, unlike Manifests, which are bare filenames. That
	// asymmetry is the useful part rather than an inconsistency: a manifest is always
	// in Dir, so its directory says nothing, while a lockfile may be hoisted to a
	// workspace root several levels up to serve many projects - so which directory
	// holds it is the only thing resolving it determines. A pnpm workspace member
	// reports "pnpm-lock.yaml" at the root here while its Manifests says
	// "package.json" beside it.
	Lockfiles []string `json:"lockfiles,omitempty" yaml:"lockfiles,omitempty"`
}

ProjectEntry is the structured view of a single project. Its Buzz mirror is generated alongside Projects; DependsOn is tagged because BuzzObject emits the camelCase `dependsOn` the rest of the Buzz surface uses, not the snake_case JSON name.

func (ProjectEntry) BuzzObject added in v0.4.0

func (v ProjectEntry) BuzzObject() BuzzObject

type ProjectOption added in v0.4.0

type ProjectOption struct {
	Key   string
	Since string
}

ProjectOption is one recognized magus.project({...}) key and the release that first understood it.

Since exists because a magusfile key is a HARD compatibility break in a way a magus.yaml key is not. An unknown yaml key is a warning and the run continues; an unknown magus.project key aborts workspace load, so every magus command fails at once - including the one that would build a binary new enough to read the file. The only thing that turns that into a sentence instead of a puzzle is the workspace declaring a floor that covers the keys it actually uses, which is what doctor's "required version covers schema" check asserts using this field.

Empty Since means the key predates the floor mechanism itself and needs no coverage.

type ProjectOrigin added in v0.4.0

type ProjectOrigin string

ProjectOrigin is what put a project in the workspace. It is a named type rather than a bare string because its two cases are not interchangeable: OriginMagusfile is a whole value you compare, while a provided project's origin CARRIES the provider's name, so it has no single constant to compare against. Constructing and reading it through ProvidedBy and Provider keeps that asymmetry out of every call site - a hand-written `origin == "provider"` would be a condition that never fires.

const OriginMagusfile ProjectOrigin = "magusfile"

OriginMagusfile is the origin of a directory discovery found a magusfile in.

func ProvidedBy added in v0.4.0

func ProvidedBy(spellName string) ProjectOrigin

ProvidedBy returns the origin of a project a workspace provider reported. The spell name is part of the value because a workspace can wire more than one provider, and "which tool says this is a project" is the whole question a reader has about a directory with no magusfile in it.

func (ProjectOrigin) Provider added in v0.4.0

func (o ProjectOrigin) Provider() (string, bool)

Provider returns the spell that reported this project, and whether a provider reported it at all. It is the read half of ProvidedBy: the spell name lives inside the value, so a caller comparing against a bare "provider" would be writing a condition that never fires.

type ProjectRef added in v0.2.0

type ProjectRef struct {
	// Path is the workspace-relative identifier: "." for the root, "pkg/foo"
	// for a nested project. Always forward-slash, never absolute, never escapes.
	Path string `json:"path" yaml:"path"`
	// Name is the human label. It coincides with Path for a nested project and
	// diverges for the root, which reads as the workspace directory's base name
	// rather than a bare ".". Carried on the wire because consumers of
	// SymbolIndexStatus and friends have always been able to read it without
	// re-deriving it from a directory they do not have.
	Name string `json:"name" yaml:"name"`
	// Dir is the project's absolute directory ("" if unknown), used only to
	// derive Name for the root. Never serialized: it is a host-specific
	// absolute path, and every consumer of this type reads workspace-relative
	// data. Putting it on the wire would leak the user's directory layout.
	Dir string `json:"-" yaml:"-"`
}

ProjectRef is the canonical project reference: holds the data once (workspace-relative path plus optional absolute dir) and exposes two render methods. One source of truth means the URI form and the display form cannot drift - they share the same fields and the same ProjectLabel / WorkspaceRef helpers both delegate to a method on this struct.

func NewProjectRef added in v0.2.0

func NewProjectRef(path, dir string) ProjectRef

NewProjectRef builds a ProjectRef from a workspace-relative path and the project's absolute directory. The dir feeds the root display name; pass "" when it is not known.

func (ProjectRef) BuzzObject added in v0.4.0

func (v ProjectRef) BuzzObject() BuzzObject

func (ProjectRef) Display added in v0.2.0

func (r ProjectRef) Display() string

Display renders the project for human consumption: the bare path for nested projects, the dir basename for the root (so a bare "." never appears in logs or Mermaid labels), and "(workspace root)" as the final fallback. This is the canonical rendering: the bare workspace-relative path is also what every project arg takes, so what magus prints pastes back in.

func (ProjectRef) WorkspaceURI deprecated added in v0.4.0

func (r ProjectRef) WorkspaceURI() string

WorkspaceURI renders the project as a "workspace://<path>" reference. An empty path is the workspace root, so a caller never prints a bare "." or "".

Deprecated: the workspace:// spelling is retired; render the bare workspace-relative path instead (Display or ProjectLabel). Magus no longer emits this form itself.

type ProjectsOutput

type ProjectsOutput struct {
	Definition string         `json:"definition" yaml:"definition" buzz:"-"`
	Workspace  string         `json:"workspace"  yaml:"workspace"`
	Count      int            `json:"count"      yaml:"count"`
	Projects   []ProjectEntry `json:"projects"   yaml:"projects"`
}

ProjectsOutput is the top-level result for "describe projects".

The Buzz `object Projects` mirror is generated from this struct by cmd/magus-utils types, so magus.ls's result can be annotated `> Projects` for compile-checked field access. Definition carries `buzz:"-"` to keep the mirror honest: BuzzObject drops it, so a mirrored field would be one the Buzz value never has.

func (ProjectsOutput) BuzzObject added in v0.4.0

func (v ProjectsOutput) BuzzObject() BuzzObject

type QueryEvent

type QueryEvent struct {
	Op          string
	Nodes       int
	Seeds       int
	Strategy    string
	ResultCount int
	Duration    time.Duration
}

QueryEvent is emitted once per top-level query method.

type RangeDiffReporter added in v0.4.0

type RangeDiffReporter interface {
	// RangeDiff returns the unified diff from base to head, both backend-native revision names.
	//
	// Symmetric difference, not a two-dot comparison: the answer is what head added since it
	// diverged, never what base gained meanwhile. A reviewer asking about a branch is asking what
	// its author did, and two-dot would charge them for every commit that landed on the base while
	// they were not looking.
	//
	// Reads what the repository already has and never fetches, matching BranchChanges. An
	// unresolvable revision is an error rather than an empty diff, because the two read identically
	// to a caller and only one of them means "nothing changed".
	//
	// paths, when non-empty, narrows the answer to those repo-relative paths the way the
	// backend's own pathspec does - at the SOURCE, so a caller never has to re-emit a filtered
	// patch and every count downstream is already scoped.
	RangeDiff(ctx context.Context, dir, base, head string, paths []string) (string, error)
}

RangeDiffReporter is an optional capability for VCSDriver implementations that can produce the unified diff of a committed revision range, which is the half of review DirtyDiff cannot reach.

Callers type-assert for it and degrade gracefully. Degrading here means REFUSING, not answering empty: a working tree with no changes is a real clean answer, but a range magus cannot diff is a gap, and rendering a gap as an empty changeset would report a colleague's branch as untouched.

type ReadinessComponent added in v0.2.0

type ReadinessComponent struct {
	Name   ReadinessName   `json:"name"`
	Status ReadinessStatus `json:"status"`
	Detail string          `json:"detail"`
}

ReadinessComponent is one subsystem's readiness within a ReadinessReport. Status is one of: ok, degraded, down, idle, disabled. Detail is a short plain-ASCII human-readable line that stays generic and quantitative (e.g. "2 of 3 up to date") because /readyz is served unguarded: counts inform without identifying, but workspace roots, project or service names, filesystem paths, PIDs, and raw error text must never appear here - that identifying detail lives behind the bearer-guarded StatusService. Never the sole signal - a client should key off Status.

type ReadinessName added in v0.4.0

type ReadinessName string

ReadinessName identifies which subsystem a readiness component reports on, and ReadinessStatus is its verdict. Both were bare strings whose vocabulary lived in a trailing comment - which a compiler cannot check and a reader has to trust. A probe endpoint is read by a kubelet, so a value drifting from what the reader expects is the kind of break nothing surfaces until a rollout stalls.

const (
	ReadinessWorkspaces     ReadinessName = "workspaces"
	ReadinessSymbolIndex    ReadinessName = "symbol_index"
	ReadinessServices       ReadinessName = "services"
	ReadinessKnowledgeGraph ReadinessName = "knowledge_graph"
)

type ReadinessReport added in v0.2.0

type ReadinessReport struct {
	Ready      bool                 `json:"ready"`
	Components []ReadinessComponent `json:"components"`
}

ReadinessReport is the JSON body of GET /readyz: Ready mirrors the pass/fail gate a kubelet's status-code check already enforces (200 iff Ready), and Components adds component-level detail an orchestrator ignores but a browser client (the console PWA) can render as per-subsystem daemon health. Adding this body does not change the gate - it is purely additive alongside the existing 200/503 status code.

type ReadinessStatus added in v0.4.0

type ReadinessStatus string

ReadinessStatus is one component's verdict. Idle and disabled are deliberately distinct from ok: a subsystem nobody asked for is not the same as one that ran.

const (
	ReadinessOK       ReadinessStatus = "ok"
	ReadinessDegraded ReadinessStatus = "degraded"
	ReadinessDown     ReadinessStatus = "down"
	ReadinessIdle     ReadinessStatus = "idle"
	ReadinessDisabled ReadinessStatus = "disabled"
)

type RefMatch added in v0.4.0

type RefMatch struct {
	Project string
	Target  string
	Charms  []string
}

RefMatch names a workspace target whose live cache key predicts a ref.

magus.IdentifyRef returns []RefMatch, so a caller (the CLI's ref-lookup suggestion, the magus_output MCP tool's not-found fallback) reads the candidate target(s) that would mint a given ref as values, without importing the root magus package just for this one return type.

type RefreshHookInstaller added in v0.2.0

type RefreshHookInstaller interface {
	InstallRefreshHook(ctx context.Context, root, command string) ([]string, error)
}

RefreshHookInstaller is an optional capability (sibling of MergeDriverInstaller) for VCSDriver implementations that can install a hook firing on a history-changing event (branch switch, merge, rebase) to run command. It shares the managed-section convention the merge-driver install uses, so magus has one VCS-integration path, not two. Callers type-assert for it and skip gracefully when a backend lacks it (e.g. jj has no native hooks). It returns the labels of the hooks it installed, for a notice.

type RemoteReporter added in v0.2.0

type RemoteReporter interface {
	// RemoteURL returns the default remote URL for the repository containing dir,
	// or "" with ErrVCSUnsupported when there is no remote configured.
	RemoteURL(ctx context.Context, dir string) (string, error)
}

RemoteReporter is an optional capability for VCSDriver implementations that can report the repository's default remote URL (e.g. git's "origin" fetch URL). It lets callers derive a forge browse/blob URL for turning a workspace-relative source path into a link. Like the other optional capabilities, callers type-assert for it and degrade gracefully (no link) when a backend lacks it.

type Returns added in v0.4.0

type Returns map[string]any

Returns holds one target's return value per project path. That is the shape every consumer wants, because a run selects ONE target across a project set; the sink underneath is keyed more finely (see [returnKey]).

type RevTimeReporter added in v0.4.0

type RevTimeReporter interface {
	// RevTime returns the commit date of rev in the repository containing dir.
	//
	// found is false when rev names nothing in this clone, which is an ordinary
	// state rather than an error - a base branch never fetched into a fresh clone
	// resolves to nothing, and a caller reporting staleness has to tell "old" from
	// "not here". err is reserved for a backend that answered something it cannot
	// itself read back.
	RevTime(ctx context.Context, dir, rev string) (t time.Time, found bool, err error)
}

RevTimeReporter is an optional capability (sibling of RemoteReporter) for VCSDriver implementations that can report when a named revision was committed.

Separate from Metadata's CommitDate, which describes the checked-out commit and is opaque display text. This one answers it for an ARBITRARY rev and returns a real time.Time, because the caller does arithmetic on it: how far behind a comparison base has fallen is a number, not a banner string.

type ReviewOrigin added in v0.4.0

type ReviewOrigin struct {
	Branch string `json:"branch,omitempty" yaml:"branch,omitempty"`
	Remote string `json:"remote,omitempty" yaml:"remote,omitempty"`
}

ReviewOrigin is where a working tree's changes would be discussed: the movable name they sit on, and the remote they would be pushed to.

Two VCS facts, resolved by magus and handed to a provider spell rather than discovered by it. A spell rederiving them would be a second opinion about the same working tree, and one that only knows its own host's conventions where magus already speaks four backends.

Either field may be empty, and that is ordinary rather than an error: a detached HEAD has no branch, a tree with no remote has no remote, and a workspace with no VCS at all has neither. A provider answers "no review" for all three, which is what the reader sees anyway when the branch simply has no pull request open.

type ReviewTarget added in v0.4.0

type ReviewTarget struct {
	// ID is the review's identity in the provider's own terms, opaque to magus and passed
	// back to the spell untouched.
	//
	// A STRING, though every provider magus ships speaks in integers. GitHub numbers pull
	// requests and GitLab numbers merge requests, but Gerrit and Phabricator identify a change
	// by a hash, and an int here would have made those providers unwritable for a saving of
	// nothing - magus does no arithmetic on it. The wrong kind of specific is the kind you find
	// out about from the person who could not write the second provider.
	ID string `json:"id" yaml:"id"`
	// Repo is where the review lives, for display: an owner/name on GitHub, a project path on
	// GitLab. Never parsed here.
	Repo   string `json:"repo,omitempty" yaml:"repo,omitempty"`
	Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
	// State is what the host says has become of the review: "open", "merged" or "closed".
	//
	// EMPTY reads as open, so a provider that does not answer this keeps working unchanged and
	// the subset rule holds - a spell declares nothing to opt out.
	//
	// It is asked of the provider rather than worked out from git because a squash merge leaves
	// no trace git can follow: the branch is rewritten into one new commit, so its tip is never
	// an ancestor of the base and it is not patch-equivalent to what landed either. A repository
	// that squash-merges would simply never notice its own merges.
	State string `json:"state,omitempty" yaml:"state,omitempty"`
	// Author is who opened the review and Viewer is who the credential belongs to, both in the
	// provider's own terms. Empty when the provider does not answer, and an empty pair means
	// UNKNOWN - never "yours". Guessing "yours" would refuse a legitimate approval on a
	// colleague's change; guessing "theirs" would let a change approve itself.
	Author string `json:"author,omitempty" yaml:"author,omitempty"`
	Viewer string `json:"viewer,omitempty" yaml:"viewer,omitempty"`
}

ReviewTarget is the review a branch has open, or the reason it has none.

"No provider wired", "no pull request for this branch" and "the host was unreachable" are all an empty ID with a Reason, deliberately. None is a thing the reader did wrong, and a surface that renders them differently would be inventing a distinction its user does not have - what they can do next is identical in all three.

func (ReviewTarget) AllowedVerdicts added in v0.4.0

func (r ReviewTarget) AllowedVerdicts() []ReviewVerdict

AllowedVerdicts lists every verdict this reviewer may publish, remarks first.

DERIVED from PermittedVerdict rather than restating its rule, so a surface offering the choices and the publish path enforcing them cannot drift apart. A client renders exactly this list; it is never handed the author and viewer names to compare for itself, because a permission rule re-implemented in a browser is one that eventually disagrees with the one that matters.

func (ReviewTarget) Merged added in v0.4.0

func (r ReviewTarget) Merged() bool

Merged reports whether the host says this review has landed.

func (ReviewTarget) Open added in v0.4.0

func (r ReviewTarget) Open() bool

Open reports whether there is a review to publish to or read from AT ALL. It is about existence, not about State: a merged review is still one whose conversation can be read, and Open stays true for it.

func (ReviewTarget) OpenedByViewer added in v0.4.0

func (r ReviewTarget) OpenedByViewer() (opened, known bool)

OpenedByViewer reports whether the credential holder opened this review, and whether that is KNOWN at all.

Two returns rather than one, in the (value, ok) shape, because the third state is real and silently collapsing it is the bug this guards: a provider that names neither party leaves the question unanswered, which is not the same as answering "no".

func (ReviewTarget) PermittedVerdict added in v0.4.0

func (r ReviewTarget) PermittedVerdict(want ReviewVerdict) ReviewVerdict

PermittedVerdict returns the verdict this review may actually carry, which is want unless magus will not permit it, and VerdictComment when it will not.

A caller that needs to know whether it was refused compares the result against what it asked for. That is deliberate: a second `downgraded bool` return would read as the (value, ok) idiom with its polarity inverted, and a reader who glanced at it would take true for success.

Two cases are refused, and both come back as remarks rather than as an error - the review still publishes, it just does not assert:

  • The viewer opened the review. A change cannot approve itself. The provider API would take it, which is exactly why the rule lives here rather than in a workspace-authored spell.
  • Authorship is UNKNOWN. A provider that names neither party has not said the review belongs to somebody else, and "magus could not tell" must never resolve to "go ahead" - which is the whole reason OpenedByViewer reports its own certainty.

func (ReviewTarget) VerdictLimit added in v0.4.0

func (r ReviewTarget) VerdictLimit() string

VerdictLimit explains why AllowedVerdicts is only remarks, or "" when it is not limited.

The two reasons are different facts and a surface that renders them alike misleads: "this is your own change" is how review is supposed to work, while "magus could not tell who opened this" is a gap in what the provider answered - the same distinction the branch lookup's unsupported marker exists to preserve.

type ReviewThread added in v0.4.0

type ReviewThread struct {
	ID   string `json:"id" yaml:"id"`
	Path string `json:"path" yaml:"path"`
	// Line is the new-side line the host anchored this remark to.
	Line int `json:"line" yaml:"line"`
	// Hunk is the index WITHIN Path's hunks of the one containing Line, or -1 when no hunk in
	// this changeset does.
	//
	// Resolved by magus rather than by each surface, because the arithmetic is the only hard
	// part of placing a thread and two surfaces doing it independently is how the same remark
	// comes to sit against different code in the terminal and the browser. -1 is ordinary: the
	// working tree moves after a colleague writes, and a review covers commits a working diff
	// does not.
	Hunk   int    `json:"hunk" yaml:"hunk"`
	Author string `json:"author" yaml:"author"`
	Body   string `json:"body" yaml:"body"`
	// New reports that the reader has not had this thread on screen before. magus's own
	// annotation rather than anything the host said - every other field here belongs to the
	// review, and this one belongs to the reader's history with it.
	New bool `json:"new,omitempty" yaml:"new,omitempty"`
}

ReviewThread is one comment already on the review, written by anybody.

Read-only here. A thread belongs to the host, which is the record every participant sees; magus renders it so a reader never leaves to find out what a colleague said, and replies through the provider rather than editing a local copy that would silently diverge.

type ReviewVerdict added in v0.4.0

type ReviewVerdict string

ReviewVerdict is what a published review SAYS about the change: remarks alone, an approval, or a request for changes. It follows the family rule KnowledgeVerdict states - the scalar judgment is a Verdict, and the constants carry that prefix rather than the domain's.

Provider-neutral by design. GitHub calls this an "event" and spells its values in caps; GitLab approves through a different endpoint entirely; Gerrit scores a label. Naming the magus-side concept after any one of them would make the others translate a foreign word, so a spell maps these three onto whatever its host actually wants.

const (
	// VerdictComment publishes the remarks and takes no position. The default everywhere, the
	// only verdict a self-review can carry, and what any unrecognized value becomes.
	VerdictComment ReviewVerdict = "comment"
	// VerdictApprove says the change should land.
	VerdictApprove ReviewVerdict = "approve"
	// VerdictRequestChanges says it should not, yet.
	VerdictRequestChanges ReviewVerdict = "request_changes"
)

func (ReviewVerdict) Asserts added in v0.4.0

func (v ReviewVerdict) Asserts() bool

Asserts reports whether v takes a position on the change rather than only remarking on it.

It is the question the permission rule turns on, and it is written once here so an unrecognized value - a client's typo, a newer magus's vocabulary - is not an assertion by default. Only the two words below can be.

type RevisionExporter added in v0.2.0

type RevisionExporter interface {
	// ExportRevision writes the tree of rev (a backend-native revision expression)
	// into dstDir, re-rooted at dir: only dir's own subtree is exported, with paths
	// relative to it, so dstDir mirrors the workspace as of rev.
	ExportRevision(ctx context.Context, dir, rev, dstDir string) error
}

RevisionExporter is an optional capability for VCSDriver implementations that can materialize a revision's tracked files into a directory (a "checkout to a throwaway tree" without touching the working copy). Callers type-assert for it and degrade gracefully when a backend lacks it - either wrapping ErrVCSUnsupported (like the other capabilities) or, for a user-facing command, surfacing a plain message. It powers `magus graph diff --rev`, which builds a base knowledge graph from the exported tree.

type RevisionFileReader added in v0.4.0

type RevisionFileReader interface {
	// ReadFileAt returns the content of a root-relative slash path at rev, EXACTLY as the
	// revision holds it: no trimming, and a trailing newline is part of the file.
	//
	// rev is a backend-native revision expression. Empty means "the committed revision",
	// which each backend spells its own way - HEAD for git, `.` for hg and Sapling, `@`
	// for jj - so a caller that wants the committed side passes "" rather than picking a
	// spelling that is only correct for one of them.
	//
	// A path absent at that revision is an error, not empty content: the caller cannot
	// tell those apart otherwise.
	ReadFileAt(ctx context.Context, root, rev, path string) (string, error)
}

RevisionFileReader is an optional capability for VCSDriver implementations that can read ONE file's content at a revision, without materializing a tree.

Sibling of RevisionExporter and deliberately narrower: exporting a whole revision to read a single magusfile costs the size of the repository to answer a question about one file. `magus vcs resolve` reads the committed magusfile this way when the merge in progress left conflict markers in the working copy, which is the only version of it guaranteed to parse.

Callers type-assert and degrade when a backend lacks it, like every other capability here - though every backend magus ships does implement it.

type SecretGrant added in v0.4.0

type SecretGrant struct {
	// Ref is the credential reference, resolved through the run's selected provider
	// exactly as magus\secret.read resolves one. magus never parses it: an
	// environment variable name under the built-in provider, an op:// path under a
	// 1Password spell. See docs/concepts/secrets.md for why there is no URI scheme.
	Ref string
	// Host is the only destination this credential may be attached to, matched
	// against a request URL's host. A port is part of the match when present, so
	// "localhost:8080" and "localhost" are different destinations.
	//
	// ASCII-lowercased by Normalize, and non-ASCII is REJECTED rather than folded.
	// Case-insensitive comparison via strings.EqualFold was a real hole: it applies
	// Unicode simple case folding, under which U+017F folds to "s" and U+212A to "k",
	// so "hookſ.slack.com" satisfied a grant for "hooks.slack.com". Punycode an
	// internationalized host yourself; magus will not guess an encoding for the one
	// field that decides where a credential may go.
	//
	// No wildcards, deliberately. A pattern is how the placeholder-matching designs
	// leak: "*.example.com" reads as a convenience until a subdomain someone else
	// controls satisfies it. Declaring a second grant is cheap and says what it means.
	Host string
	// Header names the request header the value is attached to, e.g. "Authorization"
	// or "X-API-Key".
	Header string
	// Prefix is written before the value in the header, e.g. "Bearer " or "token ".
	// Empty sends the value alone, which is what a bare X-API-Key wants.
	//
	// A prefix rather than a format template with a placeholder: Buzz interpolates
	// braces inside a string literal, so a "{}" placeholder would be a lexer hazard
	// for no reach a prefix does not already have. `Authorization: Basic` is NOT
	// expressible here and the docs say so rather than implying coverage.
	Prefix string
}

SecretGrant declares one credential and the single destination it may be sent to. It is what [magus\secret.endpoint] takes: magus binds a loopback URL, a child process is pointed at that instead of the real API, and magus attaches the credential on the way upstream. The child never holds it.

It exists for the case `magus\secret.read` cannot serve. A read hands the value to your magusfile, which is fine when the consumer is your own code - magus knows the value is a credential and masks it out of everything it writes. It is not fine when the consumer is a SUBPROCESS that decides at runtime what to do and can be induced to print its own environment. There is nothing to redact in another process.

magus briefly attached these to its own http\* calls too. That was cut: the plaintext is in magus's memory either way (the resolver memoizes it, and the redaction set must retain it), so withholding it from a Buzz variable in the same process bought close to nothing, while the host matching and per-hop redirect re-checking it needed produced two credential-leak bugs. The host here is ROUTING, not matching - the forwarder has to know where to send.

Named a grant rather than a binding because `binding` already means the Go/Buzz trampoline layer in this repo (internal/interp/bindings), and a second meaning on that word would collide at every call site a contributor reads.

What it removes is the credential's presence in whatever consumes it. What it does NOT remove is that consumer's ability to SPEND the credential within this grant's scope: a child pointed at the loopback endpoint can issue any request the grant permits. It stops exfiltration, not use. Both sentences belong together wherever this is described, per docs/concepts/secrets.md.

Always SecretGrant.Normalize a grant before storing or matching one. The zero comparison rules below assume canonical fields.

func (SecretGrant) Normalize added in v0.4.0

func (g SecretGrant) Normalize() (SecretGrant, error)

Normalize validates g and returns it in canonical form, with the message naming the field at fault.

Every failure carries SecretGrantInvalid. A malformed grant is a magusfile authoring mistake with a documented resolution, and it is the one declaration that decides where a credential may go - so it gets a lookupable code rather than a bare string, and a caller can branch on `e.code == "MGS1027"`. One code for every clause: the resolution is the same in each case (fix the declaration the message names), and the specifics are in the message.

One function rather than a Validate/canonicalize pair because splitting them is what produced a live defect: the old Validate tested strings.TrimSpace(g.Host) and then stored the UNTRIMMED value, so `host = " api.example.com"` passed and then matched nothing forever, sending every request unauthenticated with no error anywhere. A caller that cannot get the checked value back cannot use it.

type SemverNext added in v0.4.0

type SemverNext struct {
	Major string
	Minor string
	Patch string
}

SemverNext mirrors semver.next's {major, minor, patch} object: the three candidate next versions after a parsed version (bump major, minor, or patch), each rendered "vX.Y.Z" to match SemverVersion.String()'s convention.

func (SemverNext) BuzzObject added in v0.4.0

func (v SemverNext) BuzzObject() BuzzObject

type SemverVersion

type SemverVersion struct {
	Major      int
	Minor      int
	Patch      int
	Prerelease string
	Metadata   string
	Original   string
}

SemverVersion mirrors semver.parse's {major, minor, patch, prerelease, metadata, original} object.

func (SemverVersion) BuzzObject added in v0.4.0

func (v SemverVersion) BuzzObject() BuzzObject

func (SemverVersion) String added in v0.4.0

func (v SemverVersion) String() string

String renders the canonical "vMAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]" form, e.g. "v1.2.3-rc1+build5". This is deliberately NOT Original: Original is the raw text as the user wrote the tag/version string, so it round-trips things String() normalizes away (a leading zero like "v1.02.3", a missing "v", metadata the canonical form still carries). The leading "v" matches how this codebase already writes versions everywhere else - git tags ("v0.3.0"), the linker-stamped build version (-X main.version=v0.1.0), and selfupdate's target version handling.

type ServiceState added in v0.4.0

type ServiceState string

ServiceState is where a supervised service sits in its lifecycle. Deliberately NOT TargetRunState: the two share only "running" and "failed", and describe different things - a service is idle when nothing needs it, a target run is cached when its result was replayed. One union type would make half the values invalid for each user, which is the opposite of what naming them buys.

const (
	ServiceStarting ServiceState = "starting"
	ServiceRunning  ServiceState = "running"
	ServiceIdle     ServiceState = "idle"
	ServiceFailed   ServiceState = "failed"
)

func (ServiceState) String added in v0.4.0

func (v ServiceState) String() string

String renders v for an error message: the value, or "unset" when empty.

func (ServiceState) Valid added in v0.4.0

func (v ServiceState) Valid() bool

Valid reports whether v is a declared ServiceState. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (ServiceState) Values added in v0.4.0

func (v ServiceState) Values() []string

Values lists the ServiceState values a caller may choose, excluding the zero value.

type Shard

type Shard struct {
	ID           string   // zero-padded shard index (e.g. "00", "01")
	ProjectPaths []string // workspace-relative project paths assigned to this shard
}

Shard is one runner's worth of work in a CI shard plan.

type ShardPlan

type ShardPlan struct {
	Shards      []Shard
	Source      string // VCS source label (e.g. "git diff vs origin/main")
	MaxParallel int    // recommended concurrency cap; 0 means unlimited
}

ShardPlan is a provider-neutral CI shard plan produced by Magus.Plan.

type ShellCommand added in v0.4.0

type ShellCommand struct {
	Bin  string
	Args []string
}

ShellCommand is the argv that runs a line through the platform shell: what proc.shell returns, and what proc.exec takes. It exists so the shell choice is a VALUE rather than a decision taken inside a call - you can print it, log it, or assert on it before anything runs, which the old proc.shell wrapper made impossible.

The Buzz `object ShellCommand` mirror is generated from this struct by cmd/magus-utils types (go:generate); keep them in lockstep through the generator.

func (ShellCommand) BuzzObject added in v0.4.0

func (v ShellCommand) BuzzObject() BuzzObject

type SignAlgorithm added in v0.4.0

type SignAlgorithm string

SignAlgorithm names the signature scheme a crypto\sign call uses.

A named type with a declared case list rather than a bare string, so a magusfile naming an algorithm magus does not implement is a CHECKER error at load rather than a runtime throw halfway through a release job. The registry between here and Buzz is cmd/magus-utils/boundary_types.go; adding a case there and here is what makes the validator, the error message, and the Buzz mirror all follow.

One case today. The type exists anyway because the alternative - baking the algorithm into the method name, as ed25519Sign / ed25519SignFile / ed25519Verify / ed25519Public - costs four new names per algorithm and leaves callers no way to pass the choice around.

const SignEd25519 SignAlgorithm = "ed25519"

SignEd25519 is the only scheme magus signs and verifies with. It is what every magus signature already uses: SHA256SUMS.sig, the release index, and the registry.

func (SignAlgorithm) String added in v0.4.0

func (v SignAlgorithm) String() string

String renders v for an error message: the value, or "unset" when empty.

func (SignAlgorithm) Valid added in v0.4.0

func (v SignAlgorithm) Valid() bool

Valid reports whether v is a declared SignAlgorithm. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (SignAlgorithm) Values added in v0.4.0

func (v SignAlgorithm) Values() []string

Values lists the SignAlgorithm values a caller may choose, excluding the zero value.

type Spell

type Spell struct {
	Name string `json:"name"              yaml:"name"`
	// BuzzImport is the module path a magusfile writes to bind this spell's handle:
	// "magus/spell/go", for `import "magus/spell/go"`. See spells.ModulePath.
	//
	// Named for Buzz because Language below already means something else on this
	// record - the language the spell ADAPTS (go, typescript). This one is the
	// language you write the import IN. Unqualified "module" left a reader to guess
	// which of the two it meant.
	//
	// A path, deliberately, and not the handle itself. internal/describe reads spell
	// imports STATICALLY to build the target graph, so a spell reached any way other
	// than a literal import would lose its target-uses-spell edge and under-report the
	// graph silently. Carrying the path keeps discovery dynamic and the import static:
	// look the spell up, read what to write, then write it.
	BuzzImport string `json:"buzz_import"       yaml:"buzz_import"`
	// BuiltIn reports whether this spell ships compiled into the binary, reachable
	// from any workspace as `import "magus/spell/<name>"`.
	//
	// False means it is a spell THIS workspace loaded from a path
	// (`import "spells/github/actions" as github`), registered when the magusfile
	// evaluated. Both end up in the same registry, so a listing that did not
	// distinguish them showed `github-actions` beside `go` as though a reader could
	// import it by handle and find it documented - they cannot, on either count.
	BuiltIn bool     `json:"built_in"          yaml:"built_in"`
	Sources []string `json:"sources,omitempty" yaml:"sources,omitempty"`
	Outputs []string `json:"outputs,omitempty" yaml:"outputs,omitempty"`
	Targets []string `json:"targets,omitempty" yaml:"targets,omitempty"`
	Opaque  bool     `json:"opaque,omitempty" yaml:"opaque,omitempty"`
	// Language is the canonical source language the spell adapts (e.g. "go",
	// "typescript"), empty for a spell tied to no single language. It tags the spell
	// node so `magus query language:go` reaches the adapter alongside that language's
	// files and symbols.
	Language string `json:"language,omitempty" yaml:"language,omitempty"`
	// VersionProbe reports whether the spell declares a toolchain-version command
	// (mgs_getVersionProbe). Its OUTPUT is mixed into every cache key for the
	// project (run.go's toolVersionsByProject), making it one of the few cache
	// inputs that is not a file - so "why did this key change" is unanswerable from
	// the spell inventory without it. Reported as a bool rather than the argv
	// because the argv survives only inside the probe closure; the descriptor keeps
	// it, and `magus describe spell <name>` docs render it from there.
	//
	// Its absence is not cosmetic: with every spell reporting identically whether or
	// not it probed, the inventory reads as though none of them do.
	VersionProbe bool `json:"version_probe,omitempty" yaml:"version_probe,omitempty"`
	// Versions are the probes' OBSERVED results, populated only when the caller asks
	// for them (they shell out, so they are never gathered by default).
	//
	// Declared and observed are different questions and only the second debugs
	// anything: VersionProbe above says a probe exists, which cannot tell you that the
	// toolchain on this machine has drifted from what the project pins, even though
	// that value is in every cache key. On the model rather than in a print helper so
	// every output format carries it - an agent reading -o json needs it most.
	Versions []SpellVersion `json:"versions,omitempty" yaml:"versions,omitempty"`
	// TargetDocs maps a target name to its handler's doc comment, where one
	// exists. Populated only for workspace-local Buzz spells (built-in docs are
	// not serialized in bytecode).
	TargetDocs map[string]string `json:"target_docs,omitempty" yaml:"target_docs,omitempty"`
	// OpCommands maps an op (target) name to the base argv it runs, rendered with
	// an empty charm set (element 0 is the tool). Present only for ops that declare
	// a static command; a function-op (whose argv is computed by executing its Buzz
	// body) has no entry. It lets the knowledge graph link an op to the tool it runs
	// without re-rendering, so `explain tool:go` reaches every op that runs go.
	OpCommands map[string][]string `json:"op_commands,omitempty" yaml:"op_commands,omitempty"`
	// Toolchains groups OpCommands by their base executable. It is derived rather
	// than declared, so a spell cannot misreport the commands it implements.
	Toolchains []SpellToolchain `json:"toolchains,omitempty" yaml:"toolchains,omitempty"`
}

Spell is the structured view of a single spell.

type SpellErrors

type SpellErrors struct {
	Project string
	// ProjectLabel is the human name for Project, set from ProjectDisplayName where
	// the whole project is in hand. Project is the workspace-relative path, and for
	// a root project that path is ".", which rendered as "magus lint .:" - a bare
	// dot against a colon, which reads as punctuation rather than as the project it
	// actually names. Empty falls back to Project.
	ProjectLabel string
	Target       string
	Failed       []SpellFailure
}

SpellErrors aggregates failures across multiple spells running the same target.

func (*SpellErrors) Cause added in v0.4.0

func (e *SpellErrors) Cause() string

Cause is the failure WITHOUT the project and target restated: which tool broke, and how.

It exists because those two facts reach a reader twice. The CLI prints the project and target in the heading immediately above the cause line, so a cause that opened by repeating them ("magus lint .: 1 spell(s) failed [magusfile] magusfile: target lint: ...") spent its first two thirds on what the previous line already said, and buried the one thing it alone knew - the failing tool - at the end. Error() keeps the full sentence for an SDK consumer holding nothing but the error; the CLI logs this.

A lone failure needs no spell attribution: the tool name is what a reader acts on, and which spell dispatched it is not. Fan-out across several spells keeps the bracketed list, where the spell is what tells the failures apart.

func (*SpellErrors) Error

func (e *SpellErrors) Error() string

func (*SpellErrors) Unwrap

func (e *SpellErrors) Unwrap() []error

Unwrap satisfies errors.Is/As.

type SpellFailure

type SpellFailure struct {
	Spell string
	Err   error
}

SpellFailure records the error from a single spell during multi-spell fan-out.

type SpellReport added in v0.4.0

type SpellReport struct {
	Definition string  `json:"definition" yaml:"definition"`
	Count      int     `json:"count"      yaml:"count"`
	Spells     []Spell `json:"spells"     yaml:"spells"`
}

SpellReport is the "describe spell[s]" envelope.

type SpellToolchain added in v0.4.0

type SpellToolchain struct {
	Command    string   `json:"command" yaml:"command"`
	Operations []string `json:"operations" yaml:"operations"`
}

SpellToolchain is a derived executable inventory for one spell. It reports the base command Magus has already resolved from static operations and the operations that use it; it adds no spell-authoring contract.

type SpellVersion added in v0.4.0

type SpellVersion struct {
	Tool     string `json:"tool" yaml:"tool"`
	Version  string `json:"version,omitempty" yaml:"version,omitempty"`
	CacheKey string `json:"cache_key,omitempty" yaml:"cache_key,omitempty"`
	Error    string `json:"error,omitempty" yaml:"error,omitempty"`
}

SpellVersion is one probe's result: the tool it names, what it reported, and the cache-key fragment that value produces. Error is set instead of Version when the probe could not run, so a caller can tell "not installed" from "no probe declared".

type StagingPlan added in v0.4.0

type StagingPlan struct {
	// Sources and Outputs are what staging claimed: declared sources, and the declared
	// outputs a source change in this same set accounts for.
	Sources []string `json:"sources,omitempty" yaml:"sources,omitempty"`
	Outputs []string `json:"outputs,omitempty" yaml:"outputs,omitempty"`
	// Unexplained are declared outputs that moved with no dirty declared input behind
	// them. Skipped unless named explicitly; Code says why they are suspect.
	Unexplained []string `json:"unexplained,omitempty" yaml:"unexplained,omitempty"`
	// Undeclared is what no project claims, and Maintained the subset magus's own core
	// writes outside any target's globs (.gitattributes).
	Undeclared []string `json:"undeclared,omitempty" yaml:"undeclared,omitempty"`
	Maintained []string `json:"maintained,omitempty" yaml:"maintained,omitempty"`
	// Staged is what actually reached the index, empty on a dry run. It is the plan's
	// product, so it is what -o jsonl streams.
	Staged []string `json:"staged" yaml:"staged" jsonl:"primary"`
	// Reason is the prose --untracked was given, and is empty on every other run.
	// The flag switches off the undeclared-file report, so the plan is where that
	// decision stays answerable: it reaches the terminal, `-o json`, and the hook
	// reading the verdict, which is every audience the staging has.
	Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
	// Code, Message and URL classify Unexplained via ClassifyDrift, and are empty when
	// nothing is unexplained.
	Code    string `json:"code,omitempty" yaml:"code,omitempty"`
	Message string `json:"message,omitempty" yaml:"message,omitempty"`
	URL     string `json:"url,omitempty" yaml:"url,omitempty"`
}

StagingPlan is what `magus vcs add` decided about a working tree, as a value.

A value rather than a pile of Printlns because the same decision has several audiences: the terminal, `-o json`, and (next) a pre-commit hook that needs the verdict as an exit status rather than as prose. Every one of those used to mean another hand-written rendering of the same four groups, which is how `-o json` came to be accepted and silently answer in text.

type Status added in v0.4.0

type Status struct {
	// Clean reports whether the tree has no uncommitted changes. It is len(Files) == 0,
	// named so a gate reads as a gate.
	Clean bool
	// Files are the changed paths, empty when Clean. Paths only: a per-entry status code
	// is not portable (jj's diff --name-only reports none at all), and Commit's rule
	// applies - a concept one backend lacks is not modeled here. Reach for vcs.exe() when
	// the codes matter.
	Files []Path
}

Status is the working tree's uncommitted state: whether it is clean, and which paths changed. It replaces the pair of vcs.is_dirty / vcs.dirty_files at the Buzz boundary, where the two answered the same question in two shapes - a bool and a list of the backend's own status lines, which a caller had to parse differently per VCS.

Files are Paths, not strings, and each carries the repository root as its base: a VCS reports paths relative to the root, while a target runs with its cwd set to its PROJECT directory. Handing back bare strings made that difference invisible and left every caller to rediscover it.

func (Status) BuzzObject added in v0.4.0

func (s Status) BuzzObject() BuzzObject

BuzzObject is the Buzz boundary map vcs.status returns: {clean, files}.

type StatusBase added in v0.2.0

type StatusBase struct {
	Telemetry TelemetryStatus
	Cache     CacheStatus
	Build     BuildStatus
}

StatusBase holds the static portions of a StatusReport: telemetry, cache, and build-flag fields. It is populated by cmd/magus (which has access to the selfUpdateCompiled build-tag constant) and injected into console.NewService so the bridge can assemble a full StatusReport without importing cmd/magus.

type StatusConfig added in v0.2.0

type StatusConfig struct {
	// DefaultCharms are the execution charms applied to every run (e.g. rw, cd, gha).
	DefaultCharms []string `json:"default_charms,omitempty" yaml:"default_charms,omitempty"`
	// Concurrency is the CONFIGURED cap on concurrent builds; 0 means nothing was
	// configured, not that no build may run.
	Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
	// ConcurrencyEffective is the width a run actually gets - Concurrency resolved through
	// the default and the machine clamp (internal/cache.ResolveConcurrency). It is the
	// number to budget against; Concurrency alone cannot be, because its common value is
	// the one that means "ask someone else".
	ConcurrencyEffective int `json:"concurrency_effective" yaml:"concurrency_effective"`
	// Sandbox reports whether subprocess/spell sandboxing is enabled.
	Sandbox bool `json:"sandbox" yaml:"sandbox"`
}

StatusConfig is the read-only slice of the daemon's resolved config surfaced on the status wire.

type StatusLock added in v0.4.0

type StatusLock struct {
	// Project is the workspace-relative path whose lock is held ("." for the root).
	Project string `json:"project" yaml:"project"`
	// PID, Command and Dir identify the holder. Dir is the one that usually settles
	// it: a holder running in a directory that no longer exists is abandoned.
	PID     int    `json:"pid,omitempty" yaml:"pid,omitempty"`
	Command string `json:"command,omitempty" yaml:"command,omitempty"`
	Dir     string `json:"dir,omitempty" yaml:"dir,omitempty"`
	// StaleAfterSeconds is the threshold at which this holder should be read as
	// possibly abandoned rather than busy. Carried on the wire so every renderer
	// shares one judgment instead of each picking its own.
	StaleAfterSeconds int `json:"stale_after_seconds,omitempty" yaml:"stale_after_seconds,omitempty"`
	// Waiters are the processes blocked on this lock right now. Empty is the common
	// case; a non-empty list is the other half of the picture, because a holder alone
	// says who is working and a waiter list says who is stalled because of it.
	Waiters []StatusLockWaiter `json:"waiters,omitempty" yaml:"waiters,omitempty"`
	// AcquireTime is when the holder took it. Age is the signal a human reads: seconds
	// is a peer, days is something nobody knows is running. Named per AIP-142, which
	// asks for a _time suffix on a timestamp rather than the _at spelling.
	AcquireTime time.Time `json:"acquire_time,omitempty" yaml:"acquire_time,omitempty"`
}

StatusLock is one held per-project workspace lock and the process holding it.

Held is the normal state, not an alarm. What makes it worth reporting is the holder: an OS file lock carries no identity of its own, so without this a blocked run can only say "another magus process" and a lock held by something abandoned is indistinguishable from one held by a peer that is about to finish.

type StatusLockWaiter added in v0.4.0

type StatusLockWaiter struct {
	PID      int       `json:"pid,omitempty" yaml:"pid,omitempty"`
	Command  string    `json:"command,omitempty" yaml:"command,omitempty"`
	Dir      string    `json:"dir,omitempty" yaml:"dir,omitempty"`
	WaitTime time.Time `json:"wait_time,omitempty" yaml:"wait_time,omitempty"`
}

StatusLockWaiter is one process blocked on a lock, with how long it has been blocked. A waiter is transient by nature, so this is only ever a snapshot.

type StatusOutput

type StatusOutput struct {
	ParentPID     int    `json:"parent_pid" yaml:"parent_pid"`
	DaemonVersion string `json:"daemon_version,omitempty" yaml:"daemon_version,omitempty"`
	Mode          string `json:"mode,omitempty" yaml:"mode,omitempty"` // "daemon", "proc", or ""
	// Socket is the proc-server address this snapshot was read from, so a reader running
	// more than one server can tell the entries apart and narrow with --socket.
	Socket   string `json:"socket,omitempty" yaml:"socket,omitempty"`
	Capacity int    `json:"capacity" yaml:"capacity"`
	Running  int    `json:"running" yaml:"running"`
	// Available is the free slots: Capacity minus Running, floored at zero. Carried as a
	// number because the question it answers - how much work can I hand this box right
	// now - should not require the reader to subtract.
	Available      int                   `json:"available" yaml:"available"`
	Queued         int                   `json:"queued" yaml:"queued"`
	RunningTargets []StatusRunningTarget `json:"running_targets,omitempty" yaml:"running_targets,omitempty"`
	Workspaces     []StatusWorkspace     `json:"workspaces,omitempty" yaml:"workspaces,omitempty"`
	Affected       []string              `json:"affected,omitempty" yaml:"affected,omitempty"`
}

StatusOutput is the public shape of the live concurrency pool reported by `magus status`.

type StatusRecord added in v0.4.0

type StatusRecord struct {
	Clean bool
	Files []Path
}

StatusRecord is the boundary mirror of the object vcs.status returns; the Buzz `object Status` mirror is generated from it by cmd/magus-utils types. See CommitRecord for why the mirror exists separately from the type it mirrors.

func (StatusRecord) BuzzObject added in v0.4.0

func (v StatusRecord) BuzzObject() BuzzObject

type StatusReport added in v0.2.0

type StatusReport struct {
	Telemetry TelemetryStatus `json:"telemetry" yaml:"telemetry"`
	Cache     CacheStatus     `json:"cache" yaml:"cache"`
	Build     BuildStatus     `json:"build" yaml:"build"`
	// BuildInfo is the reporting binary's identity (version/commit/date), so a StatusService
	// client knows which magus it is talking to. Distinct from Build above.
	BuildInfo BuildInfo     `json:"build_info" yaml:"build_info"`
	Pool      *StatusOutput `json:"pool,omitempty" yaml:"pool,omitempty"`
	PoolError string        `json:"pool_error,omitempty" yaml:"pool_error,omitempty"` // reason Pool is absent
	// Pools is every live proc server found on this machine, one entry each, and is set
	// only when there is more than one: Pool above is the first of them, so a single
	// server would just be repeated here. Enumerating them is the point - a multi-server
	// box must still report the capacity and in-use numbers, never an error demanding
	// --socket.
	Pools []StatusOutput `json:"pools,omitempty" yaml:"pools,omitempty"`
	// Runs are the invocations the daemon is executing right now (adopted
	// dispatches), each with its per-target execution state. Empty when nothing is
	// running or when reported by a process that is not the daemon.
	Runs []StatusRun `json:"runs,omitempty" yaml:"runs,omitempty"`
	// Services are the long-running shared services the daemon is hosting right now,
	// kept warm across invocations. Empty when none are held or when reported by a
	// process that is not the daemon.
	Services []StatusService `json:"services,omitempty" yaml:"services,omitempty"`
	// ObservingSince is when this daemon began observing (its start). The telemetry and
	// cache counters above are cumulative from this instant and are NOT persisted across
	// restarts, so a dashboard can be transparent that the numbers are "since <this>", not
	// all-time. Zero (omitted) when reported by a non-daemon `magus status`.
	ObservingSince time.Time `json:"observing_since,omitempty" yaml:"observing_since,omitempty"`
	// Config surfaces the daemon's RESOLVED configuration (read-only) so a dashboard can show what
	// the daemon is set to do - the default charms it applies, the concurrency cap - without a
	// round-trip to the terminal. Additive JSON, not on the proto event wire.
	Config StatusConfig `json:"config,omitempty" yaml:"config,omitempty"`
	// SymbolIndexes reports each symbol-capable project's SCIP index freshness (up to
	// date / out of date / not indexed), so `magus status` and the dashboard show at a
	// glance whether code symbols reflect current source. Empty when the workspace is
	// unavailable or no project is symbol-capable.
	SymbolIndexes []SymbolIndexStatus `json:"symbol_indexes,omitempty" yaml:"symbol_indexes,omitempty"`
	// Locks are the per-project workspace locks held right now, with the process
	// holding each. A held lock is NORMAL - every mutating run takes one - so this is
	// reported as state, never as a fault: it must not fail a readiness or liveness
	// probe, because a run blocked on a peer is waiting correctly and restarting it
	// would only send it to the back of the queue.
	//
	// It is here because the failure it makes visible is otherwise invisible. A lock is
	// held for as long as its process lives, so a process nobody remembers starting
	// holds one indefinitely, and every other run simply waits. Surfacing who holds
	// what turns that from a hang into a fact.
	Locks []StatusLock `json:"locks,omitempty" yaml:"locks,omitempty"`
	// Machine is the host-wide admission budget the daemon arbitrates: what every magus
	// on this machine holds and who is queued for it. Nil when no daemon is running, or
	// when the one that answered is a per-process proc server, which arbitrates nothing
	// beyond itself.
	//
	// It belongs beside Locks for the same reason: held is the normal state, and what
	// makes it worth reporting is WHO. A run queued for the machine is otherwise a run
	// with nothing to show for itself in another terminal.
	Machine *MachineSnapshot `json:"machine,omitempty" yaml:"machine,omitempty"`
	// MCPEndpoint reports the health of the MCP HTTP endpoint agent hosts (an editor,
	// IDEs, Desktop) actually connect to: its address and whether it is really serving.
	// It is checked independently of the Pool fields above, which report the proc socket
	// the daemon dispatches jobs on. The two listeners share a process in normal
	// operation but can diverge (the MCP server failing to bind while the proc daemon is
	// fine), so a "daemon is up" reading does not by itself prove the tools are reachable.
	// Nil when reported by a process that does not probe it (e.g. the daemon's own report).
	MCPEndpoint *MCPEndpointStatus `json:"mcp_endpoint,omitempty" yaml:"mcp_endpoint,omitempty"`
}

StatusReport is the canonical JSON/YAML shape returned by `magus status -o json`. The daemon serves the same data to the console over the typed StatusService (its live fields are projected onto magus.status.v1alpha1.Status) so both consumers share one definition. Fields are exported so pkg types can be read from internal packages without importing cmd/magus.

type StatusRun added in v0.2.0

type StatusRun struct {
	Inv       string            `json:"inv" yaml:"inv"`
	Trigger   string            `json:"trigger,omitempty" yaml:"trigger,omitempty"`
	StartedAt time.Time         `json:"started_at,omitempty" yaml:"started_at,omitempty"`
	Targets   []StatusTargetRun `json:"targets,omitempty" yaml:"targets,omitempty"`
}

StatusRun is one in-flight invocation the daemon has adopted, keyed by its invocation id, carrying the per-target execution state a dashboard renders as a live run.

func (StatusRun) BuzzObject added in v0.4.0

func (v StatusRun) BuzzObject() BuzzObject

type StatusRunningTarget added in v0.2.0

type StatusRunningTarget struct {
	Args      []string  `json:"args" yaml:"args"`
	Workspace string    `json:"workspace,omitempty" yaml:"workspace,omitempty"`
	StartedAt time.Time `json:"started_at,omitempty" yaml:"started_at,omitempty"`
	Step      string    `json:"step,omitempty" yaml:"step,omitempty"`
	Inv       string    `json:"inv,omitempty" yaml:"inv,omitempty"` // invocation id; deep-links to this running target's live log
}

StatusRunningTarget describes one running target in the pool.

type StatusService added in v0.2.0

type StatusService struct {
	ID         string       `json:"id" yaml:"id"`
	Label      string       `json:"label,omitempty" yaml:"label,omitempty"`
	Command    string       `json:"command,omitempty" yaml:"command,omitempty"`
	Ports      []string     `json:"ports,omitempty" yaml:"ports,omitempty"`
	State      ServiceState `json:"state,omitempty" yaml:"state,omitempty"`
	Dependents int          `json:"dependents,omitempty" yaml:"dependents,omitempty"`
	StartedAt  time.Time    `json:"started_at,omitempty" yaml:"started_at,omitempty"`
}

StatusService is one long-running shared service the daemon is hosting, surfaced on the status wire so a dashboard can show what is running and how many targets depend on it. It mirrors service.ServiceStatus (the registry's introspection view).

type StatusTargetRun added in v0.2.0

type StatusTargetRun struct {
	Project    string         `json:"project,omitempty" yaml:"project,omitempty"`
	Target     string         `json:"target,omitempty" yaml:"target,omitempty"`
	State      TargetRunState `json:"state" yaml:"state"`
	StartedAt  time.Time      `json:"started_at,omitempty" yaml:"started_at,omitempty"`
	EndedAt    time.Time      `json:"ended_at,omitempty" yaml:"ended_at,omitempty"`
	OutputRef  string         `json:"output_ref,omitempty" yaml:"output_ref,omitempty"`
	DurationMs int64          `json:"duration_ms,omitempty" yaml:"duration_ms,omitempty"`
}

StatusTargetRun is the execution state of one target within a StatusRun.

func (StatusTargetRun) BuzzObject added in v0.4.0

func (v StatusTargetRun) BuzzObject() BuzzObject

type StatusWorkspace

type StatusWorkspace struct {
	Root       string    `json:"root" yaml:"root"`
	LoadedAt   time.Time `json:"loaded_at" yaml:"loaded_at"`
	LastAccess time.Time `json:"last_access" yaml:"last_access"`
	// Live cache activity for this workspace (daemon mode; zero otherwise).
	CacheHit   int   `json:"cache_hit,omitempty" yaml:"cache_hit,omitempty"`
	CacheMiss  int   `json:"cache_miss,omitempty" yaml:"cache_miss,omitempty"`
	CacheError int   `json:"cache_error,omitempty" yaml:"cache_error,omitempty"`
	CacheBytes int64 `json:"cache_bytes,omitempty" yaml:"cache_bytes,omitempty"`
	// Work the hits replayed instead of ran, summed from each entry's recorded run duration.
	CacheSavedMs int64 `json:"cache_saved_ms,omitempty" yaml:"cache_saved_ms,omitempty"`
	// SecretProvider is the provider spell the magusfile selected; empty means the
	// built-in environment provider. The NAME only - never a reference, never a value.
	SecretProvider string `json:"secret_provider,omitempty" yaml:"secret_provider,omitempty"`
}

StatusWorkspace describes one workspace currently loaded by the daemon.

type StreamBody added in v0.4.0

type StreamBody interface {
	// StreamType reports the type stamped on the wire. It is derived from the body
	// rather than stored on the envelope so the two can never disagree.
	StreamType() StreamEventType
}

StreamBody is the per-type payload of a StreamEvent. Implementations are the Stream* body structs in this file; the interface is closed in practice, and DecodeStreamEvent fails on a type it does not know rather than guessing.

Every implementation marshals to a JSON OBJECT. A body that marshals to an array or a scalar cannot be spliced flat into the envelope, and StreamEvent.MarshalJSON reports that as an error rather than emitting a line no subscriber can parse.

type StreamEvent added in v0.4.0

type StreamEvent struct {
	// Ts is the event time in unix milliseconds, matching journal.Event.Ts so a
	// mapped event keeps the timestamp its producer recorded rather than the time
	// the mapping ran.
	Ts int64 `json:"-"`
	// Workspace is the absolute repository root. It is present on every event
	// because a subscriber may watch more than one workspace over a single
	// connection, and an event with no workspace cannot be routed to a buffer.
	Workspace string `json:"-"`
	// Inv groups every event belonging to one magus invocation. Empty for events
	// that belong to no run - a file change, an attention request, a guard verdict.
	Inv string `json:"-"`
	// Body carries the per-type fields and determines [StreamEvent.Type]. It is
	// never nil on a well-formed event; a nil Body marshals as an error rather
	// than as a typeless line.
	Body StreamBody `json:"-"`
}

StreamEvent is one fact about a workspace, addressed to a PROGRAM: the record an editor plugin, a status bar, or a notifier subscribes to. It is the single envelope magus's several internal producers are mapped onto, so an integrator learns one shape rather than five.

It is the machine-facing sibling of Event, and the split is by AUDIENCE, not by content. Event is what magus emits when it needs a PERSON: it carries an urgency tier and a situation class because a human renderer has to decide whether to interrupt someone. A StreamEvent carries neither, because a program subscribing to a build stream decides relevance by StreamEventType. The two do not meet on the wire today: every mapped type is a run or target fact, and an attention request has no adapter onto this stream - see the const block.

Nothing a subscriber does can change a magus verdict. The stream is outbound only, by design and not by omission: docs/scope.md seals the engine, the cache, the graph schema, and the guard's evaluation, and an extension seam "may change what magus does, never what a verdict means". There is deliberately no reply channel here. The inbound counterpart is `magus session hook`, which is request/reply and whose verdict this stream can only report after the fact.

Every field carries `json:"-"`: the wire shape comes from StreamEvent.MarshalJSON and streamHead, not from struct tags here, so a tag on this struct would be a second source of truth that nothing reads.

On the wire it is one JSON object per line. The envelope fields come first and the body's fields are spliced in flat beside them, so a line reads:

{"schema":1,"type":"target.result","ts":1756312800000,"workspace":"/repo",
 "inv":"inv1a2b3c","project":"cmd/magus","target":"build","status":"fail",...}

Flat rather than nested under a "data" key because that is the shape internal/report already ships on `magus run -o jsonl`, and an integrator reading both surfaces should not meet two conventions. It also keeps a jq filter or an Emacs alist lookup one level deep.

func DecodeStreamEvent added in v0.4.0

func DecodeStreamEvent(line []byte) (StreamEvent, error)

DecodeStreamEvent parses one wire line back into a StreamEvent.

It is the counterpart to StreamEvent.MarshalJSON for Go subscribers and for round-trip tests; the reference clients decode in their own language. An unrecognized type is an error here rather than a skip, because a Go caller can check StreamEventType before decoding and a silent zero body would be worse than a refusal. Line-oriented consumers that want the documented skip-what-you-do-not-know behavior should switch on the type first.

A schema newer than StreamSchema is decoded rather than refused: the envelope contract is that additive changes keep old fields meaning what they meant, so a client built against schema 1 reads a schema 2 line correctly for the fields it knows.

func (StreamEvent) MarshalJSON added in v0.4.0

func (e StreamEvent) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event as one flat JSON object: the envelope fields followed by the body's own fields at the same level.

It fails rather than emitting a partial line when Body is nil or marshals to anything but an object. A subscriber cannot recover from a typeless or malformed line, so producing one would push the failure somewhere it cannot be diagnosed.

func (*StreamEvent) UnmarshalJSON added in v0.4.0

func (e *StreamEvent) UnmarshalJSON(line []byte) error

UnmarshalJSON decodes one wire line. Without it every field is tagged "-", so json.Unmarshal would report no error and leave a zero event with a nil Body.

type StreamEventType added in v0.4.0

type StreamEventType string

StreamEventType names the class of fact an event carries. A subscriber switches on it; an unrecognized value must be skipped, not treated as an error.

const (
	// StreamRunStarted opens an invocation. Carries the command lineage, so a
	// subscriber can tell `magus run build` from `magus affected ci`.
	StreamRunStarted StreamEventType = "run.started"
	// StreamRunFinished closes the invocation opened by StreamRunStarted with the
	// same Inv. A subscriber that shows progress clears it here.
	StreamRunFinished StreamEventType = "run.finished"
	// StreamTargetResult reports one target's outcome, cached replays included.
	// It carries the output ref, so a subscriber fetches the captured log on
	// demand instead of buffering every line it was sent.
	StreamTargetResult StreamEventType = "target.result"
	// StreamTargetOutput is one line of a subprocess's stdout or stderr. It is the
	// only high-volume type in the taxonomy and is off unless a subscriber asks
	// for it; see [StreamFilter].
	StreamTargetOutput StreamEventType = "target.output"
)

The taxonomy, all four mapped from the run journal by internal/eventstream.

Diagnostics, file changes, attention requests and guard verdicts have stores but no adapter, and are absent rather than silent: a type a subscriber can name and never receive is indistinguishable from a broken filter. Adding one is additive and does not bump StreamSchema. See docs/guides/integrations/editor/design.md.

func ParseStreamEventType added in v0.4.0

func ParseStreamEventType(s string) (StreamEventType, error)

ParseStreamEventType validates a type name from a flag or a subscribe frame.

It refuses an unknown name instead of ignoring it: a typo in `--type target.reslut` would otherwise present as a stream that is simply quiet, which is the failure a person debugs for an hour.

func StreamEventTypes added in v0.4.0

func StreamEventTypes() []StreamEventType

StreamEventTypes lists the taxonomy in wire order for `magus events --help` and for flag validation. Every entry has a producer; see the const block.

It is a function returning a fresh slice rather than an exported slice var so a caller cannot reorder or truncate the canonical list for everyone else.

type StreamFilter added in v0.4.0

type StreamFilter struct {
	// Types restricts the stream to these types. Empty means the default set
	// described above.
	Types []StreamEventType
}

StreamFilter selects which types a subscriber receives.

The zero value is not "everything": it is everything EXCEPT StreamTargetOutput, because that one type outnumbers all the others together on any real build and a subscriber that gets it by default drowns on its first `affected ci`. Asking for it is one flag; recovering from having been sent it unasked is a rewrite of the client.

func (StreamFilter) Allows added in v0.4.0

func (f StreamFilter) Allows(t StreamEventType) bool

Allows reports whether t passes the filter.

type StreamOutput added in v0.4.0

type StreamOutput struct {
	Project string `json:"project,omitempty"`
	Target  string `json:"target,omitempty"`
	// Stream is "stdout" or "stderr".
	Stream string `json:"stream"`
	// Text is the line, without its trailing newline.
	Text string `json:"text"`
}

StreamOutput is the body of StreamTargetOutput: one line a subprocess wrote.

This is the only type that scales with build size rather than with project count, and a busy `affected ci` emits tens of thousands. It is off unless a subscriber names it in a StreamFilter; an editor that wants a full log should fetch it by ref from a StreamTarget instead of subscribing here.

func (StreamOutput) StreamType added in v0.4.0

func (StreamOutput) StreamType() StreamEventType

StreamType implements StreamBody.

type StreamRun added in v0.4.0

type StreamRun struct {
	// Phase is "started" or "finished".
	Phase string `json:"phase"`
	// Command is the full argument vector, subcommand included, as invoked.
	// Set on the started phase only.
	Command []string `json:"command,omitempty"`
	// Trigger is how the run was spawned - one of journal's Trigger constants
	// (run, affected, ci, x, watch, direct). Set on the started phase only.
	Trigger string `json:"trigger,omitempty"`
	// MagusVersion is the binary that produced the run. Set on the started phase.
	MagusVersion string `json:"magus_version,omitempty"`
	// Status is the overall outcome, "pass" or "fail". Set on the finished phase.
	// No duration: the journal's finished event carries none, so a subscriber
	// subtracts the started event's Ts.
	Status string `json:"status,omitempty"`
}

StreamRun is the body of StreamRunStarted and StreamRunFinished.

Started and finished share one body because a subscriber correlates them by Inv and reads whichever fields the phase populates: Command and MagusVersion on the started event, Status and DurationMs on the finished one. Splitting them would double the wire types to save two optional fields.

func (StreamRun) StreamType added in v0.4.0

func (b StreamRun) StreamType() StreamEventType

StreamType reports StreamRunStarted or StreamRunFinished from Phase. An unset or unrecognized Phase reports StreamRunFinished, so a malformed body still closes a run a subscriber has open rather than leaving a spinner forever.

type StreamTarget added in v0.4.0

type StreamTarget struct {
	// Project is the workspace-relative project path.
	Project string `json:"project"`
	// Target is the target name as the CLI spells it, charms included.
	Target string `json:"target"`
	// Status is "ok" or "failed". It does NOT distinguish a replay from a run;
	// CacheHit does.
	Status string `json:"status"`
	// CacheHit reports that the result was replayed from cache rather than
	// executed.
	CacheHit bool `json:"cache_hit"`
	// Ref addresses this execution's captured output. A subscriber fetches the
	// full log with `magus query output <ref>` rather than being streamed every
	// line, which is why StreamTargetOutput can stay opt-in.
	Ref string `json:"ref,omitempty"`
	// DurationMs is the target's wall-clock span in milliseconds.
	DurationMs int64 `json:"duration_ms,omitzero"`
	// Error is the failure message when Status is "failed".
	Error string `json:"error,omitempty"`
}

StreamTarget is the body of StreamTargetResult: one target's outcome.

It is emitted for cached targets too, which is what makes CacheHit meaningful: Status reports whether the target SUCCEEDED, CacheHit whether it actually ran. A subscriber that renders a replay as a fresh build is misreading the pair.

func (StreamTarget) StreamType added in v0.4.0

func (StreamTarget) StreamType() StreamEventType

StreamType implements StreamBody.

type SymbolIndexFreshness added in v0.2.0

type SymbolIndexFreshness string

SymbolIndexFreshness is the state of a project's cached SCIP index relative to its current sources, reported by `magus status` and the dashboard.

const (
	SymbolIndexFresh    SymbolIndexFreshness = "up-to-date"  // index reflects current sources
	SymbolIndexStale    SymbolIndexFreshness = "out-of-date" // sources changed since the index was built
	SymbolIndexNotBuilt SymbolIndexFreshness = "not-indexed" // no index has been produced yet
)

func (SymbolIndexFreshness) String added in v0.4.0

func (v SymbolIndexFreshness) String() string

String renders v for an error message: the value, or "unset" when empty.

func (SymbolIndexFreshness) Valid added in v0.4.0

func (v SymbolIndexFreshness) Valid() bool

Valid reports whether v is a declared SymbolIndexFreshness. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (SymbolIndexFreshness) Values added in v0.4.0

func (v SymbolIndexFreshness) Values() []string

Values lists the SymbolIndexFreshness values a caller may choose, excluding the zero value.

type SymbolIndexStatus added in v0.2.0

type SymbolIndexStatus struct {
	Project   ProjectRef           `json:"project"`
	Language  string               `json:"language,omitempty"`
	Freshness SymbolIndexFreshness `json:"freshness"`
}

SymbolIndexStatus is one symbol-capable project's index freshness, for status output. Project carries both the machine path and the human name so the workspace-root project renders as its repo name, not the bare ".".

type SymbolOccurrence added in v0.4.0

type SymbolOccurrence struct {
	Line      int `json:"line"       yaml:"line"`
	Column    int `json:"column"     yaml:"column"`
	EndLine   int `json:"end_line"   yaml:"end_line"`
	EndColumn int `json:"end_column" yaml:"end_column"`
	// Definition marks the site that declares the symbol rather than referencing it. A
	// rename treats the two identically, but a caller that only wants call sites (or only
	// the declaration) needs them told apart, and the index already knows.
	Definition bool `json:"definition,omitempty" yaml:"definition,omitempty"`
	// Text is what magus actually read at this range. It is the evidence behind Status,
	// not decoration: on a mismatch it shows what is really there, which is what tells a
	// reader whether the index is stale or the encoding assumption was wrong.
	Text   string                 `json:"text,omitempty" yaml:"text,omitempty"`
	Status SymbolOccurrenceStatus `json:"status"         yaml:"status"`
}

SymbolOccurrence is one exact source range where a symbol appears, precise enough to drive a mechanical edit. This is deliberately NOT KnowledgeRefSite: that type carries a per-file count and a list of lines capped at MaxRefLines, which is right for describing fan-in and wrong for rewriting, because a hot symbol's list is silently truncated and a line number alone does not say WHICH occurrence on the line to replace. Occurrences are complete and column-precise.

Line and Column are 1-based, matching the file:line:col convention magus already prints and every editor understands. The end is EXCLUSIVE: the text to replace is the half-open span [Column, EndColumn), so a three-character name at column 6 has EndColumn 9.

type SymbolOccurrenceFile added in v0.4.0

type SymbolOccurrenceFile struct {
	File        string             `json:"file"        yaml:"file"`
	Occurrences []SymbolOccurrence `json:"occurrences" yaml:"occurrences"`
	// Stale means at least one of this file's occurrences did not verify, which proves
	// the file changed after it was indexed. That matters beyond the individual bad site:
	// an index whose view of a file is out of date may also be MISSING occurrences added
	// since, and no per-site check can see a site that is not in the list. So this marks
	// the file as one where a rewrite cannot be assumed complete, even for the sites that
	// did verify. Re-index before trusting it.
	//
	// The converse does not hold, and the gap is worth stating plainly: an edit that
	// disturbed no existing range - appending a new use at the end of the file - leaves
	// every occurrence verifying while still adding a site the index never saw. Nothing
	// magus can compute from the index alone detects that. Completeness rests on the
	// index being current; `magus status` reports which indexes are.
	Stale bool `json:"stale,omitempty" yaml:"stale,omitempty"`
}

SymbolOccurrenceFile groups one file's occurrences. Occurrences are sorted by position, which is also the order a caller must NOT apply them in - see KnowledgeOccurrencesOutput.

type SymbolOccurrenceStatus added in v0.4.0

type SymbolOccurrenceStatus string

SymbolOccurrenceStatus says whether one occurrence's recorded range still describes the file on disk. It exists because a SCIP range is only meaningful against the exact bytes that were indexed: an index built before an edit points at text that has since moved, and applying a rewrite to it would corrupt the file at a plausible-looking offset. Every occurrence carries one, and only SymbolOccurrenceVerified is safe to edit.

const (
	// SymbolOccurrenceVerified means magus read the range out of the current file and
	// found exactly the symbol's name there. This is the only status a mechanical
	// rewrite may act on.
	SymbolOccurrenceVerified SymbolOccurrenceStatus = "verified"
	// SymbolOccurrenceMismatch means the range resolved but holds something other than
	// the symbol's name - a stale index, or a position encoding that is not the byte
	// offsets magus assumed. Either way the range is not editable, and saying so is the
	// whole point: the alternative is a confident wrong edit.
	SymbolOccurrenceMismatch SymbolOccurrenceStatus = "mismatch"
	// SymbolOccurrenceUnreadable means the file could not be read, or the range falls
	// outside it. Distinct from mismatch because the fix differs: a deleted or moved
	// file needs a re-index, not a closer look at the line.
	SymbolOccurrenceUnreadable SymbolOccurrenceStatus = "unreadable"
)

type Target

type Target struct {
	// The identity fields carry omitempty because Target is also serialized as a
	// per-target POLICY (describe's target_policies map, keyed by target name),
	// where path/name/charms/files are all empty and would be pure noise.
	Path   string   `json:"path,omitempty"   buzz:"projectPath"` // workspace-relative project path; empty = all projects
	Name   string   `json:"name,omitempty"`                      // e.g. "build", "test"
	Charms []string `json:"charms,omitempty"`                    // execution charms parsed from the "target:charm,..." suffix
	Files  []string `json:"files,omitempty"`                     // changed files within project; populated by affected expansion

	// Declared and DeclaredCharms are the raw spellings ParseTarget rewrote, empty
	// when the caller already wrote canonical form. Provenance, not identity: Name
	// and Charms above are what magus resolves against, and these only record how it
	// was typed. Same convention as TargetGraphNode.Declared.
	//
	// They exist so a caller can react to a non-canonical spelling - the CLI hints
	// the canonical form - WITHOUT the parser reaching out to print anything. A parse
	// function that writes to stderr cannot be used by the daemon, the MCP handler,
	// or a test without dragging that output along; returning the fact instead lets
	// each caller decide, which is why this is on the value rather than in a wrapper.
	Declared       string   `json:"declared,omitempty"`
	DeclaredCharms []string `json:"declared_charms,omitempty"`

	// Per-target execution policy. SkipCache, Exclusive, Slots, and Drift are
	// author-facing, serialized into the Buzz object Target. RetryOnVolatile is a CI-only
	// hook set via the Go registration API, excluded from the Buzz object (buzz:"-").
	SkipCache bool `json:"skip_cache,omitempty" buzz:"skip_cache"` // opt out of the cache: always run, never replay/snapshot
	// SkipCacheReason is the prose the magusfile gave for SkipCache. It is required
	// (a bare `true` is a load error) because opting out is a claim that REPLAYING
	// THIS TARGET WOULD BE WRONG - it signs a fresh artifact, records a screen
	// capture, mutates go.mod - and not a preference for a fresh run, which is what
	// --no-cache is for. Demanding prose is what keeps the two apart: six of these
	// were once workarounds for a snapshot error that no longer exists, and a bare
	// bool gave no way to tell them from the real ones.
	SkipCacheReason string `json:"skip_cache_reason,omitempty" buzz:"skip_cache_reason"`
	Exclusive       bool   `json:"exclusive,omitempty"` // run alone: no other target runs concurrently
	Slots           int    `json:"slots,omitempty"`     // concurrency slots to hold while running (0 or 1 = one slot); throttles parallel work around a resource-heavy target. Clamped to the run's total slot budget.
	// MemoryMB is the peak resident memory this target needs, in megabytes; 0 means
	// undeclared. It is a portable way to spell Slots: an author knows a race-enabled
	// test suite wants 8GB, but nobody can say how many slots that is on a machine
	// they have never seen, and the answer differs between a 16GB CI runner and a
	// 64GB workstation. magus converts it against the host's memory-per-slot share
	// and holds that many slots, so there is ONE admission path rather than two
	// budgets that can disagree.
	//
	// Undeclared (0) and an unmeasurable host both mean "take one slot", which is
	// exactly the behavior that existed before this field.
	//
	// A COMPOSED target inherits the largest declaration in its chain; see
	// ChainMemoryMB, which is the figure both halves of admission actually read.
	MemoryMB int `json:"memory_mb,omitempty" buzz:"memory_mb"`
	// Drift is what happens when this target's declared outputs move under a read-only
	// run. Empty is the DEFAULT, which gates any target that declares outputs - see
	// DriftPolicy for why that is on rather than off.
	Drift DriftPolicy `json:"drift,omitempty" buzz:"drift"`
	// DriftReason is the prose the magusfile gave for turning the gate off, and it is
	// required for exactly the reason SkipCacheReason is: switching off a check that
	// protects everyone downstream is a claim, not a preference, and a bare "off" leaves
	// the next reader no way to tell a considered exemption from a workaround somebody
	// never came back to.
	DriftReason     string `json:"drift_reason,omitempty" buzz:"drift_reason"`
	RetryOnVolatile bool   `json:"retryOnVolatile,omitempty" buzz:"-"` // route through volatility detection + auto-retry
	// IncludeOS and IncludeArch override cache.include.*.enabled for this target.
	// nil inherits the workspace answer, which is what an undeclared target gets.
	//
	// Authored NESTED, to mirror magus.yaml rather than invent a second shape for the
	// same decision:
	//
	//	"image": { "cache": { "include": { "arch": { "enabled": false } } } }
	//
	// Stored flat because the nesting exists for the author's benefit - a Go caller
	// reading two optional bools should not walk three structs to find them.
	IncludeOS   *bool `json:"includeOS,omitempty" buzz:"-"`
	IncludeArch *bool `json:"includeArch,omitempty" buzz:"-"`
}

Target identifies one unit of work (project x target name). An empty Path means all projects.

Target plays a dual role, and the meaningful subset of fields differs by use:

  • As a work-unit it carries identity: Path/Name/Charms/Files describe which project x target to run and against which changed files.
  • As a policy bag it carries per-target execution policy (SkipCache/Exclusive/Drift/RetryOnVolatile). When used purely as policy (Project.TargetPolicies values, EvaluatedTargetEntry.Policy) only the policy fields are meaningful; the identity fields are unset/ignored.

func ParseTarget

func ParseTarget(s string) (Target, error)

ParseTarget parses a target reference of the form "target[:charm[,charm...]]". The project is supplied separately (positional), not embedded in the reference; ':' introduces a comma-separated list of execution charms. Both the target and each charm are constrained to the target-name charset.

func (Target) Key added in v0.4.0

func (t Target) Key() []string

Key returns the lines identifying this target's work for the cache: its name, because the body is Buzz rather than a command and has no argv to serialize - the body's text already reaches the key through the magusfile's own Sources entry, leaving the entry point as what separates build from go-build within one project.

Deliberately no interface here: nothing consumes this yet, and Go declares an interface where it is USED. When the hasher takes either a target or an op, it declares the one-method interface it needs, named for what it wants from them.

func (Target) String

func (t Target) String() string

String returns the canonical "path:target" form.

type TargetEntry

type TargetEntry struct {
	Name     string   `json:"name"               yaml:"name"`
	Kind     string   `json:"kind"               yaml:"kind"`
	Spells   []string `json:"spells,omitempty"   yaml:"spells,omitempty"`
	Projects []string `json:"projects,omitempty" yaml:"projects,omitempty"`
}

TargetEntry describes a single target available in the workspace.

type TargetExpander

type TargetExpander interface {
	ExpandPath(t Target) ([]Target, error)
	// ExpandCwd resolves t against the project containing the current working
	// directory. found reports whether cwd is inside any project; when false,
	// targets is empty and the caller typically falls back to ExpandPath or
	// reports "not inside a project". found is a deliberate signal distinct from
	// len(targets) — callers (e.g. magus tail) key their error message on it.
	ExpandCwd(t Target) (targets []Target, found bool, err error)
	ExpandAffected(ctx context.Context, target, baseRef string) (targets []Target, source string, fellBack bool, err error)
}

TargetExpander resolves a Target into concrete per-project targets.

type TargetGraphNode

type TargetGraphNode struct {
	Name string `json:"name" yaml:"name"`
	// Declared is the target's raw, as-written name when it differs from the normalized
	// Name (Name "go-build" declared as "goBuild" or "go_build"); empty when they match.
	// Name is the identity every edge and lookup keys on - the normalizer maps any
	// spelling to it - so Declared is provenance only: it conveys how the author wrote
	// the target, surfaced as the knowledge graph's declared_as attr.
	Declared     string   `json:"declared,omitempty"     yaml:"declared,omitempty"`
	Doc          string   `json:"doc,omitempty"          yaml:"doc,omitempty"`
	Dependencies []string `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
	Charms       []string `json:"charms,omitempty"       yaml:"charms,omitempty"`
	// Spells are the spell ops the target's body invokes, captured statically from
	// the bracket (`go["go-test"]()`) and dotted (`md.markdownlint()`) call forms,
	// grouped by spell in first-appearance order. It shows which toolchain a
	// composite target drives - the part `deps` (sibling targets) omits.
	Spells []TargetSpellUse `json:"spells,omitempty" yaml:"spells,omitempty"`
	// CrossDependencies are dependencies on specific targets in *other* projects,
	// declared via a project import (<alias>.<target>). Unlike Dependencies (same-project
	// target names), each carries the other project's path, so the graph can draw a
	// target -> target edge across project boundaries instead of a coarse project -> project one.
	CrossDependencies []CrossTargetRef `json:"cross_dependencies,omitempty" yaml:"cross_dependencies,omitempty"`
	// Chain is the target's composition IN INVOCATION ORDER: the DISTINCT steps the
	// body names, in the order it first names them, across every ctx.needs call the
	// target makes. A step named twice appears once, at its first invocation - the
	// second mention adds no ordering the first has not already fixed.
	// Dependencies and CrossDependencies answer "what does this compose"
	// as two sets keyed by locality; Chain answers "in what order", which is the one
	// fact the source carries and neither set preserves once the graph merges them.
	// Empty for a target that composes nothing.
	//
	// DIRECT steps only - one level, never a recursive flattening. A step that itself
	// chains is described by ITS own record; expanding it here would print a plan the
	// magusfile never writes, and the pool (not this list) decides how a transitive
	// dependency actually schedules.
	Chain []ChainStep `json:"chain,omitempty" yaml:"chain,omitempty"`
	// ReadsFiles are the per-target file inputs the body declares via ctx.readsFiles(...),
	// captured statically in ONE representation where each entry carries its owning
	// project (InputRef). A bare-literal glob (ctx.readsFiles("glob")) is a same-project
	// input whose owning project is the target's own project; a ctx.readsFiles(<alias>.
	// file("lit")) entry is a cross-project input whose owning project is the imported
	// one. When present, inputs define the target's file cache footprint rather than
	// extending the project-wide baseline; the target's magusfiles and target-specific
	// spell sources remain included. The
	// single shape feeds the cache footprint, the affected-tracking depends_on edge (a
	// cross-project input only; a same-project one seeds by directory containment), and
	// the consumes edge to the file node in the owning project.
	ReadsFiles []InputRef `json:"reads_files,omitempty" yaml:"reads_files,omitempty"`
	// ReadsSecrets records that the target body reaches for a credential - magus\secret's
	// read, grant or endpoint. A resolved credential contributes NOTHING to the cache key
	// - deliberately, since hashing one would write it into cache metadata - so rotating
	// or revoking it invalidates nothing. A cacheable target that uses one therefore
	// becomes a replay that reports success without ever contacting the provider, which is
	// worst for exactly the authentication targets the `-login` convention encourages,
	// whose sources rarely change. MGS1026 reports the combination; skip_cache with a
	// reason is the fix.
	//
	// A GRANT carries the same hazard in a sharper form, which is why it counts here: the
	// magusfile never holds the value, so changing a grant's ref from staging to
	// production alters nothing the cache can see, and the target replays its old output
	// against a different credential. The name stays ReadsSecrets because it is a
	// Buzz-visible describe field (readsSecrets); the concept it records is "uses".
	ReadsSecrets bool `json:"reads_secrets,omitempty" yaml:"reads_secrets,omitempty"`
	// SecretRefs are the credential REFERENCES this target names, sorted and deduped -
	// never values, which magus does not have at describe time and would not print if it
	// did. It answers "which credentials does this target touch" without running it,
	// which is the question an operator reviewing a magusfile actually has.
	//
	// Only literal references appear. magus\secret.read takes a string literal, so its
	// reference is here; magus\secret.endpoint takes an object usually declared as a
	// `final` elsewhere, so its reference is not at the call site and only ReadsSecrets
	// records the use. Under-reporting is deliberate: resolving that identifier would
	// mean evaluating the magusfile, which a static read refuses to do.
	SecretRefs []string `json:"secret_refs,omitempty" yaml:"secret_refs,omitempty"`
	// WritesFiles are the per-target ctx.writesFiles(...) refs, each carrying its owning project
	// (empty means this target's own). When present, they define the target's
	// snapshot/replay set instead of inheriting project-wide and spell outputs.
	WritesFiles []OutputRef `json:"writes_files,omitempty" yaml:"writes_files,omitempty"`
	// ModifiesExistingFiles are the per-target ctx.modifiesExistingFiles(...) refs: existing files the target edits in place
	// rather than produces. Deliberately NOT unioned into the snapshot/replay set - see
	// UpdateRef for why magus must neither delete nor restore one.
	//
	ModifiesExistingFiles []UpdateRef `json:"modifies_existing_files,omitempty" yaml:"modifies_existing_files,omitempty"`
	// ExecOverrides are the canonical per-op execution overrides this target declares
	// via ctx.withEnv / ctx.withCwd, as "env:K=V" / "cwd:V" strings in declaration
	// order (hash.go sorts a copy at hash time; nothing sorts the stored value). They fold into the target's
	// CACHE KEY: a derived env changes what the tool does, so two runs differing only by
	// it must not share an entry. Read statically for the same reason inputs are - the
	// key is computed before the body runs, so a purely runtime derivation could never
	// reach it. A non-literal derive sets DynamicIO and is rejected at load.
	ExecOverrides []string `json:"exec_overrides,omitempty" yaml:"exec_overrides,omitempty"`
	// EnvAllow names environment variables this target declares via ctx.env, whose
	// PROCESS values fold into the cache key. The complement of ExecOverrides: an
	// override's value is written in the magusfile and hashed directly, while these
	// values are only knowable at run time, so the NAME is what is declared statically
	// and the value is read when the key is computed. That is what lets a target whose
	// env is genuinely derived from the environment stay cacheable instead of having to
	// opt out of the cache entirely.
	EnvAllow []string `json:"env_allow,omitempty" yaml:"env_allow,omitempty"`
	// Observations are the external facts this target declares via ctx.observes, as
	// canonical "key=value" strings in declaration order (hash.go sorts a copy at hash
	// time; nothing sorts the stored value). An observation is a fact the answer depends
	// on that the tree does not contain - a vulnerability feed's id, a remote schema's
	// revision - so the target can key on it instead of opting out of the cache with
	// skip_cache. Both halves are literals, hashed directly as ExecOverrides are, which
	// makes this ExecOverrides' mechanical twin; what differs is that an observation
	// changes nothing about HOW the target runs, only what its answer is a function of.
	// magus never interprets the value: it stores it, and a change is a miss.
	Observations []string `json:"observations,omitempty" yaml:"observations,omitempty"`
	// DynamicIO is set when a ctx.readsFiles/writesFiles/modifiesExistingFiles/envInputs/observes call carries a
	// non-literal argument. A computed glob is invisible to this static read, so the load
	// path rejects it loudly rather than silently caching an under-declared footprint.
	// Not serialized: it is a load-time validation signal, not part of the graph.
	DynamicIO bool `json:"-" yaml:"-" buzz:"-"`
	// DynamicExec is the execution-side counterpart: a ctx.withEnv / ctx.withCwd whose
	// argument is not a literal. It is NOT a load error - the override still takes effect
	// at run time, and a genuinely derived environment cannot be written literally - so it
	// only records that ExecOverrides is an incomplete view of what the target will run
	// with. Not serialized, same as DynamicIO.
	DynamicExec bool `json:"-" yaml:"-" buzz:"-"`
}

TargetGraphNode is one target in the graph: its run name, doc comment, the targets it depends on, and the charm names its body branches on. The static extractor (internal/describe) populates it directly and `magus describe graph` serializes it. Wire keys are snake_case field names (dependencies, not the abbreviated deps), matching the project-level depends_on and the rest of this file.

func (TargetGraphNode) BuzzObject added in v0.4.0

func (v TargetGraphNode) BuzzObject() BuzzObject

type TargetGraphOutput

type TargetGraphOutput struct {
	Definition string               `json:"definition" yaml:"definition" buzz:"-"`
	Projects   []TargetGraphProject `json:"projects"   yaml:"projects"`
}

TargetGraphOutput is the top-level result for "describe graph".

The Buzz `object TargetGraph` mirror is generated from this struct by cmd/magus-utils types, so magus.targets's result can be annotated `> TargetGraph` for compile-checked field access. Definition carries `buzz:"-"` for the same reason ProjectsOutput's does: BuzzObject drops it, so a mirrored field would be one the Buzz value never has.

func (TargetGraphOutput) BuzzObject added in v0.4.0

func (v TargetGraphOutput) BuzzObject() BuzzObject

type TargetGraphProject

type TargetGraphProject struct {
	Path   string            `json:"path"             yaml:"path"`
	Name   string            `json:"name"             yaml:"name"`
	Engine string            `json:"engine,omitempty" yaml:"engine,omitempty"`
	Nodes  []TargetGraphNode `json:"nodes,omitempty"  yaml:"nodes,omitempty"`
	Cycle  []string          `json:"cycle,omitempty"  yaml:"cycle,omitempty"`
	// DependsOn are the workspace-relative paths of the projects this project
	// depends on (its project-level deps, declared in magus.project).
	// They draw the project -> project arrows in the combined workspace graph;
	// intra-project target edges live on each node's Dependencies.
	DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
	// RelPath is Path expressed relative to the VCS (repo) root, used only for an
	// unambiguous MAGUS.md heading when a project sits at the workspace root (Path
	// is "."). Display-only and repo-derived, so it is not serialized; the run path
	// still addresses the project by Path. Empty outside a repo.
	RelPath string `json:"-" yaml:"-" buzz:"-"`
}

TargetGraphProject is one project's target graph, plus a detected cycle (a path of node names that begins and ends at the same node) when the DAG is not acyclic.

func (TargetGraphProject) BuzzObject added in v0.4.0

func (v TargetGraphProject) BuzzObject() BuzzObject

func (TargetGraphProject) Label added in v0.2.0

func (p TargetGraphProject) Label() string

Label is the human display name for this project, the single source every render site uses so none prints a bare ".": the pre-collapsed RelPath (which reads as the repo name for the workspace root), falling back to the shared never-'.' rule on Path.

type TargetReport added in v0.4.0

type TargetReport struct {
	Definition string        `json:"definition" yaml:"definition"`
	Count      int           `json:"count"      yaml:"count"`
	Targets    []TargetEntry `json:"targets"    yaml:"targets"`
}

TargetReport is the "describe target[s]" envelope.

type TargetRunState added in v0.2.0

type TargetRunState string

TargetRunState is where a target sits in its lifecycle within a run. Values match the magus.status.v1alpha1.TargetRun.State enum names (lowercased) so the JSON and the wire agree.

const (
	TargetRunQueued  TargetRunState = "queued"
	TargetRunRunning TargetRunState = "running"
	TargetRunPassed  TargetRunState = "passed"
	TargetRunFailed  TargetRunState = "failed"
	TargetRunCached  TargetRunState = "cached"
)

func (TargetRunState) String added in v0.4.0

func (v TargetRunState) String() string

String renders v for an error message: the value, or "unset" when empty.

func (TargetRunState) Valid added in v0.4.0

func (v TargetRunState) Valid() bool

Valid reports whether v is a declared TargetRunState. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (TargetRunState) Values added in v0.4.0

func (v TargetRunState) Values() []string

Values lists the TargetRunState values a caller may choose, excluding the zero value.

type TargetSpellUse

type TargetSpellUse struct {
	Spell string   `json:"spell"         yaml:"spell"`
	Ops   []string `json:"ops,omitempty" yaml:"ops,omitempty"`
}

TargetSpellUse is one spell a target invokes and the ops it calls on it.

func (TargetSpellUse) BuzzObject added in v0.4.0

func (v TargetSpellUse) BuzzObject() BuzzObject

type TelemetryStatus added in v0.2.0

type TelemetryStatus struct {
	Enabled     bool    `json:"enabled" yaml:"enabled"`
	Endpoint    string  `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
	Protocol    string  `json:"protocol,omitempty" yaml:"protocol,omitempty"`
	Insecure    bool    `json:"insecure,omitempty" yaml:"insecure,omitempty"`
	ServiceName string  `json:"service_name,omitempty" yaml:"service_name,omitempty"`
	SampleRatio float64 `json:"sample_ratio,omitempty" yaml:"sample_ratio,omitempty"`
	Note        string  `json:"note,omitempty" yaml:"note,omitempty"`
}

TelemetryStatus reports the current telemetry configuration.

type TermSize added in v0.4.0

type TermSize struct {
	Width  int
	Height int
}

TermSize is a terminal's dimensions in character cells, as term\size reports them.

An object rather than two returns because a pair of bare ints at a call site is exactly the shape that gets swapped: os\platform's three-string return has the same problem and cannot be fixed without breaking callers. Both fields are 0 when the size cannot be determined - piped output, no controlling terminal - so a caller checks one field rather than interpreting an error.

func (TermSize) BuzzObject added in v0.4.0

func (v TermSize) BuzzObject() BuzzObject

type TermStyle added in v0.4.0

type TermStyle string

TermStyle names a terminal text style a term\colorize call applies.

A named type with a declared case list rather than a raw SGR string, for the reason SignAlgorithm and PlatformStyle give: the underlying form is "\x1b[2;32m", which nobody should be asked to type or proofread, and a wrong code is not an error - it is output that renders as garbage on someone else's terminal. The cases are exactly the SGR codes internal/interactive/tty already defines, so this invents no palette; it names the one magus renders with.

The zero value is "no styling", which Colorize already treats as pass-through. That makes a conditionally-computed style safe to pass without branching.

const (
	// TermBold is emphasis without color, readable on any background.
	TermBold TermStyle = "1"
	// TermDim lowers a line's signal without hiding it.
	TermDim TermStyle = "2"
	// TermRed marks a failure.
	TermRed TermStyle = "31"
	// TermGreen marks a success.
	TermGreen TermStyle = "32"
	// TermYellow marks a warning.
	TermYellow TermStyle = "33"
	// TermDimGreen is the low-signal success magus renders a cache hit with:
	// it happened, and it is not what you are reading the output for.
	TermDimGreen TermStyle = "2;32"
	// TermDimGrey is for text that is present for reference rather than to be read.
	TermDimGrey TermStyle = "2;37"
	// TermBrightGreen is the high-signal success, for the result of a whole run.
	TermBrightGreen TermStyle = "1;32"
)

func (TermStyle) String added in v0.4.0

func (v TermStyle) String() string

String renders v for an error message: the value, or "unset" when empty.

func (TermStyle) Valid added in v0.4.0

func (v TermStyle) Valid() bool

Valid reports whether v is a declared TermStyle. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (TermStyle) Values added in v0.4.0

func (v TermStyle) Values() []string

Values lists the TermStyle values a caller may choose, excluding the zero value.

type TimeLayout added in v0.4.0

type TimeLayout string

TimeLayout names a timestamp format a time\format or time\parse call uses.

Go's reference-layout scheme - spelling a format by writing out the reference instant, "2006-01-02T15:04:05Z07:00" - is unguessable for anyone who has not written Go, and mistyping one digit yields a format that parses and renders the wrong thing. These cases name the layouts Go's own time package defines, so a magusfile writes TimeLayout.rfc3339 instead.

IT DOES NOT CLOSE THE PARAMETER. A Buzz enum<str> accepts a plain string where it is declared, so a custom layout still works: the enum adds names for the common formats without taking away the escape hatch, which matters because there is no finite set of timestamp formats a build might have to read.

const (
	// TimeRFC3339 is the interchange default: 2006-01-02T15:04:05Z07:00. Use it
	// for anything another program will read.
	TimeRFC3339 TimeLayout = "2006-01-02T15:04:05Z07:00"
	// TimeRFC3339Nano is RFC 3339 with nanoseconds, for ordering events that can
	// occur within the same second.
	TimeRFC3339Nano TimeLayout = "2006-01-02T15:04:05.999999999Z07:00"
	// TimeDateOnly is 2006-01-02, for a changelog heading or a directory name.
	TimeDateOnly TimeLayout = "2006-01-02"
	// TimeTimeOnly is 15:04:05.
	TimeTimeOnly TimeLayout = "15:04:05"
	// TimeDateTime is 2006-01-02 15:04:05, the human-readable pairing.
	TimeDateTime TimeLayout = "2006-01-02 15:04:05"
	// TimeRFC1123 is the HTTP date format, for a Last-Modified or Expires header.
	TimeRFC1123 TimeLayout = "Mon, 02 Jan 2006 15:04:05 MST"
	// TimeKitchen is 3:04PM, for output a person reads at a glance.
	TimeKitchen TimeLayout = "3:04PM"
)

func (TimeLayout) String added in v0.4.0

func (v TimeLayout) String() string

String renders v for an error message: the value, or "unset" when empty.

func (TimeLayout) Valid added in v0.4.0

func (v TimeLayout) Valid() bool

Valid reports whether v is a declared TimeLayout. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (TimeLayout) Values added in v0.4.0

func (v TimeLayout) Values() []string

Values lists the TimeLayout values a caller may choose, excluding the zero value.

type TrackedFileReporter added in v0.4.0

type TrackedFileReporter interface {
	// TrackedFiles returns the subset of paths that the VCS tracks, as given.
	// Paths are interpreted relative to dir, matching the backend CLI's own pathspec
	// handling. An empty paths slice returns no results rather than every tracked
	// file in the repository.
	TrackedFiles(ctx context.Context, dir string, paths []string) ([]string, error)
}

TrackedFileReporter is an optional capability (sibling of RemoteReporter) for VCSDriver implementations that can report which paths the VCS actually tracks.

"Tracked" is not answerable from Dirty or DirtyFiles, which is why this exists separately: an ignored file and a clean tracked file both report nothing dirty, so a caller that needs to tell a committed artifact from a build product cannot infer it from cleanliness. Callers type-assert for it and skip the question when a backend lacks it, rather than guessing - a wrong guess here misclassifies generated output as committed, or the reverse.

type TrendEntry added in v0.4.0

type TrendEntry struct {
	Path    string `json:"path"    yaml:"path"`
	Name    string `json:"name"    yaml:"name"`
	Recent  int    `json:"recent"  yaml:"recent"`
	Earlier int    `json:"earlier" yaml:"earlier"`
	Delta   int    `json:"delta"   yaml:"delta"`
}

TrendEntry is one project's churn split across the window's two halves; Delta>0 is rising.

func (TrendEntry) BuzzObject added in v0.4.0

func (v TrendEntry) BuzzObject() BuzzObject

func (TrendEntry) Label added in v0.4.0

func (t TrendEntry) Label() string

type TrendOutput

type TrendOutput struct {
	Definition string       `json:"definition" yaml:"definition"`
	Commits    int          `json:"commits"    yaml:"commits"`
	Since      string       `json:"since,omitempty" yaml:"since,omitempty"`
	Projects   []TrendEntry `json:"projects"   yaml:"projects"`
}

TrendOutput ranks projects by whether their activity is rising or cooling — the window is split at its midpoint and the two halves compared.

func (TrendOutput) BuzzObject added in v0.4.0

func (v TrendOutput) BuzzObject() BuzzObject

type URL

type URL struct {
	Scheme   string
	Host     string
	Port     string
	Path     string
	Query    string
	Fragment string
}

URL mirrors encoding.parse_url's {scheme, host, port, path, query, fragment} object.

func (URL) BuzzObject added in v0.4.0

func (v URL) BuzzObject() BuzzObject

type UncompressResult added in v0.4.0

type UncompressResult struct {
	Files []Path
	Bytes int
}

UncompressResult mirrors archive.uncompress's {files, bytes} object: the paths written (sorted) and their total uncompressed size.

Files are Paths based at the destination directory, which is where uncompress actually wrote them - so a caller can open one without first remembering which of the two directories it passed in the entries were measured from.

func (UncompressResult) BuzzObject added in v0.4.0

func (v UncompressResult) BuzzObject() BuzzObject

type UnreferencedEntry added in v0.4.0

type UnreferencedEntry struct {
	ID       string `json:"id"                 yaml:"id"`
	Label    string `json:"label"              yaml:"label"`
	Source   string `json:"source,omitempty"   yaml:"source,omitempty"`
	Kind     string `json:"kind,omitempty"     yaml:"kind,omitempty"`
	Language string `json:"language,omitempty" yaml:"language,omitempty"`
}

UnreferencedEntry is one symbol nothing names, with where it is defined so the reader can go look at it. Kind is the SCIP classifier (Function, Struct, ...), which is what makes the list triageable: an unreferenced exported Function reads very differently from an unreferenced Field.

func (UnreferencedEntry) BuzzObject added in v0.4.0

func (v UnreferencedEntry) BuzzObject() BuzzObject

type UnreferencedOutput added in v0.4.0

type UnreferencedOutput struct {
	Definition string              `json:"definition" yaml:"definition"`
	Symbols    []UnreferencedEntry `json:"symbols"    yaml:"symbols"`
	Answer     KnowledgeAnswer     `json:"answer"     yaml:"answer"`
}

UnreferencedOutput lists the symbols nothing in the workspace names.

Answer is what keeps the list honest. A project whose symbol index was never built contributes no symbols at all, so its dead code would silently render as a clean report; the verdict says when the list is a fact and when it is only what magus could see. An empty Symbols list with an unknown verdict means "nothing found and I could not look everywhere", which is not the same as "nothing to find".

func (UnreferencedOutput) BuzzObject added in v0.4.0

func (v UnreferencedOutput) BuzzObject() BuzzObject

type UnregisteredDep

type UnregisteredDep struct {
	Consumer   string // project path that declared the dep
	Dep        string // dep path that did not resolve
	DidYouMean string // nearest registered path, or ""
}

UnregisteredDep is one missing-dep observation found while building the graph.

type UnregisteredDepError

type UnregisteredDepError struct {
	Missing []UnregisteredDep
}

UnregisteredDepError aggregates every UnregisteredDep found during a Graph call.

func (*UnregisteredDepError) Error

func (e *UnregisteredDepError) Error() string

Error returns an end-user-readable description of every missing dependency.

func (*UnregisteredDepError) Is

func (*UnregisteredDepError) Is(target error) bool

Is returns true for ErrUnregisteredDep.

type UpdateRef added in v0.4.0

type UpdateRef struct {
	Project string `json:"project,omitempty" yaml:"project,omitempty"`
	Glob    string `json:"glob" yaml:"glob"`
}

UpdateRef names one EXISTING file a target edits in place rather than produces, declared via ctx.modifiesExistingFiles. Same shape as OutputRef, and a third type for the same reason OutputRef is not InputRef: what magus is allowed to DO with the file differs, and sharing a type would let a caller pass one where the other belongs.

An output is a file magus owns end to end, so magus may delete it (magus clean) and restore it wholesale from a cache snapshot. An update is a file magus does NOT own - a hand-written page with a generated region between markers, a lockfile a tool rewrites in place - where only part of the content is the target's to produce. Deleting one destroys authored content that regeneration cannot bring back, and replaying one from a snapshot silently reverts edits made since. So an update is never deleted and never replayed.

It is a plain source for cache purposes: because it is NOT in the output set, it is not excluded from the source hash, so editing the authored prose around the generated region correctly invalidates the target that maintains it.

func (UpdateRef) BuzzObject added in v0.4.0

func (v UpdateRef) BuzzObject() BuzzObject

type VCSCheckpoint added in v0.4.0

type VCSCheckpoint struct {
	// Revision is the resolved head revision id (VCSMeta.ID): git SHA, hg node, jj
	// commit_id. Full, never abbreviated - it is meant to be fed back to a VCS.
	Revision string `json:"revision" yaml:"revision"`
	// Branch is the movable name pointing at Revision, or empty where the backend has
	// none. VCSMeta.Ref's value under the name a caller recording a handoff writes;
	// empty is ordinary (a detached HEAD, or jj's usual anonymous change), not an error.
	Branch string `json:"branch,omitempty" yaml:"branch,omitempty"`
	// Dirty reports uncommitted changes, on the vcs package's existing definition
	// (VCSMeta.IsDirty, which for git is a non-empty `status --porcelain` and so counts
	// untracked files too).
	Dirty bool `json:"dirty" yaml:"dirty"`
	// PatchDigest fingerprints the uncommitted patch, empty when the tree is clean.
	//
	// It covers exactly what the backend's DirtyDiff covers, which is TRACKED content
	// against the checked-out revision. Untracked files make Dirty true and leave no
	// mark here, so two trees differing only in untracked files share a digest. Stated
	// rather than papered over: a digest that silently changed with build residue would
	// answer "did we see the same tree" wrong in the commoner direction.
	PatchDigest string `json:"patch_digest,omitempty" yaml:"patch_digest,omitempty"`
	// VCS is the resolved backend name (git, hg, jj), so a reader knows whose revision
	// syntax Revision is written in.
	VCS string `json:"vcs,omitempty" yaml:"vcs,omitempty"`
}

VCSCheckpoint is the identity of the working state a piece of work was handed: what was true in this tree, at this moment, in a form a ledger can record and a later reader can act on.

It RESOLVES AND RECORDS; it never MINTS. No tag, no stash, no ref, no file - a checkpoint is a pure read, so nothing about it can be lost by not keeping it and nothing about the tree changes by taking it. That is what makes it safe to take one per lease, and it is why this is a plain value with no id of its own: an identity magus invented would be a fact only magus could confirm.

Two things a reader can do with it. Revision feeds anything that takes a rev (`magus graph diff --rev <rev>`). PatchDigest compares against another worker's: equal digests mean the two saw the same uncommitted patch, which no revision alone can tell you, because a dirty tree's revision is the same one everybody else has.

func (VCSCheckpoint) BuzzObject added in v0.4.0

func (v VCSCheckpoint) BuzzObject() BuzzObject

type VCSDriver

type VCSDriver interface {
	Name() string
	Claims() []string
	// IsSecondaryCheckout reports whether dir is a second checkout of the same
	// repository under this VCS (a git linked worktree, an `hg share`, a jj
	// secondary workspace) rather than the primary. Discovery skips such dirs so a
	// repo's projects and spells are not indexed twice. Matched structurally
	// against the backend's on-disk signature; no process is spawned.
	IsSecondaryCheckout(dir string) bool
	Base() string
	// ParentRef names the parent of the checked-out commit in this backend's own
	// revision syntax (git HEAD^, hg p1(.), jj @-). It is the fallback base for a
	// ref that builds itself, where Base - that ref's own tip - would compare a
	// commit against itself and report nothing affected.
	ParentRef() string
	// Root, ChangedFiles, and Metadata operate on the repository containing dir. An empty
	// dir uses the process working directory. Passing an explicit dir is required
	// for correctness when work runs concurrently, since the process cwd is global.
	Root(ctx context.Context, dir string) (string, error)
	ChangedFiles(ctx context.Context, dir, base string) ([]string, error)
	Bisect(ctx context.Context, dir string, opts BisectOptions) (Culprit, error)
	DiffCommands(ctx context.Context, dir, base string) (DiffCommandHints, error)
	Metadata(ctx context.Context, dir string) (VCSMeta, error)
	// Dirty reports whether the working tree has uncommitted changes. When paths
	// is non-empty the probe is scoped to those pathspecs (interpreted relative to
	// dir, the same as the VCS's own CLI); empty checks the whole repository. It is
	// the path-scoped counterpart to Metadata's repo-wide IsDirty.
	Dirty(ctx context.Context, dir string, paths []string) (bool, error)
	// DirtyFiles is Dirty with the detail: the changed PATHS, relative to the
	// repository root and using forward slashes, nil when clean. Dirty is defined in
	// terms of this; callers that report *what* changed use these paths.
	//
	// Paths, not the backend's status lines, and the difference is the whole contract.
	// Each backend prints a different shape - git porcelain's two status columns, hg
	// and sl's one, jj's bare `diff --name-only` output, plus git's " -> " for a rename
	// and its C-quoting for unusual bytes - and only the driver knows which it emits.
	// Returning lines pushed that knowledge outward, where it grew THREE parsers that
	// disagreed: one keyed on the backend name, one that guessed the prefix from the
	// line's own bytes, and a Buzz-boundary wrapper delegating to the first. The
	// guessing one read a jj file named "A note.txt" as status "A " plus path
	// "note.txt", so a legitimately-named file silently matched no glob.
	//
	// A per-entry status CODE is deliberately not modeled. It is not portable - jj
	// reports none at all - which is the same reason types.Status carries paths only.
	// Reach for vcs.cmd when the codes matter.
	DirtyFiles(ctx context.Context, dir string, paths []string) ([]string, error)
	// DirtyDiff is DirtyFiles with the CONTENT: the working tree's uncommitted changes
	// to those paths, as the backend's own unified diff, empty when nothing changed.
	// Callers that must show WHY a file changed use this; the ones that only need the
	// names use DirtyFiles.
	//
	// Parity here means every backend answers the question, not that the bytes match:
	// git, hg, sl, and jj each emit their native diff header, and no wrapper can reconcile
	// those without lying about what ran. Context width follows the backend's own flag
	// where it has one.
	DirtyDiff(ctx context.Context, dir string, paths []string) (string, error)
	// FindCommit looks up a revision (a VCS-native rev expression; empty means
	// the current revision) and returns its normalized Commit.
	FindCommit(ctx context.Context, dir, rev string) (Commit, error)
	// History returns up to limit recent commits, newest first.
	History(ctx context.Context, dir string, limit int) ([]Commit, error)
	// Describe returns a human-readable version string derived from the nearest
	// tag (git's `describe --tags --always --dirty`: tag, else short id, with a
	// -dirty suffix for a modified tree). Tags are a git-shaped concept; a backend
	// without an equivalent returns "" rather than faking one. Callers treat "" as
	// "no describe available" and fall back (e.g. to a short hash); a magus author
	// needing backend-specific behavior reaches for vcs.exe().
	Describe(ctx context.Context, dir string) (string, error)
	// Tags lists the repository's tags, newest first. Tags are shared prior art,
	// not a git import: Mercurial versions them in .hgtags, and Fossil, Bazaar,
	// and Darcs each have the same concept under the same name. A backend that
	// genuinely lacks one returns none rather than faking it.
	//
	// pattern is a path.Match glob over the tag name ("v*"); "" lists every tag.
	// Wildcards stop at "/", so "v*" skips a namespaced tag like backup/x.
	//
	// An empty result means "no tags visible here", which is NOT "never
	// released": a shallow or single-branch clone commonly fetches none, so a
	// caller deciding what shipped must treat empty as unknown.
	Tags(ctx context.Context, dir, pattern string) ([]VCSTag, error)
}

VCSDriver describes a version control system.

type VCSMeta

type VCSMeta struct {
	// ID and Short are the revision identifier and its abbreviation, named to match
	// Commit.ID above rather than for git's word: "hash" is git and hg, while jj has
	// a commit_id and a change_id and no hash in its vocabulary at all. One concept
	// had two names in this file, and Commit.ID is the one that already said so.
	Short string
	ID    string
	// Ref is the movable name pointing at this revision - a git branch, a
	// Mercurial named branch, a Jujutsu bookmark - or "" when there is none
	// (jj's working copy is usually an anonymous change, so empty is ordinary
	// there). Named for the concept rather than for git's word for it, matching
	// the vcs.ref() a magusfile already calls; "branch" is what two of the three
	// backends happen to call theirs, which is not the same as it being the
	// portable name.
	Ref string
	// CommitDate stays a string, deliberately not time.Time: each backend
	// formats it with its own native command (git's `log --format=%ci`, hg's
	// `{date|isodate}` template, jj's custom "%Y-%m-%d %H:%M:%S %z") and the
	// formats do not even agree with each other (hg's isodate filter omits
	// seconds; git and jj include them). It is opaque, backend-provided
	// display text meant for a build banner, not a value any caller parses
	// back into a time - forcing one shared layout here would mean discarding
	// or reformatting what the VCS itself chose to report.
	CommitDate string
	IsDirty    bool
}

VCSMeta holds per-revision metadata for embedding in build artifacts.

type VCSOptions

type VCSOptions struct {
	Enabled *bool  // nil = check MAGUS_VCS_ENABLED
	Name    string // overrides MAGUS_VCS_NAME
	BaseRef string // overrides MAGUS_VCS_BASE_REF
}

VCSOptions holds explicit VCS configuration; non-zero fields override MAGUS_VCS_* env vars.

type VCSResolution

type VCSResolution struct {
	Name   string // active VCS name, empty when disabled
	Source VCSSource
	Base   string
	VCS    VCSDriver // nil when disabled
}

VCSResolution is the outcome of resolving the active VCS for a workspace.

type VCSSource

type VCSSource string

VCSSource indicates how the active VCS was chosen.

const (
	VCSSourceExplicit VCSSource = "explicit"
	VCSSourceAuto     VCSSource = "auto"
	VCSSourceDefault  VCSSource = "default"
	VCSSourceDisabled VCSSource = "disabled"
)

func (VCSSource) String added in v0.4.0

func (v VCSSource) String() string

String renders v for an error message: the value, or "unset" when empty.

func (VCSSource) Valid added in v0.4.0

func (v VCSSource) Valid() bool

Valid reports whether v is a declared VCSSource. The zero value is valid: it means the field was not set, which callers distinguish from a wrong value.

A switch rather than a scan over Values: Values allocates a fresh slice per call so its result can never be mutated by a caller, which is the right trade for a helper that builds an error message and the wrong one for a predicate.

func (VCSSource) Values added in v0.4.0

func (v VCSSource) Values() []string

Values lists the VCSSource values a caller may choose, excluding the zero value.

type VCSTag added in v0.4.0

type VCSTag struct {
	// Name is the tag as a user writes it ("v0.3.0", or "libs/gopherbuzz/v0.1.0"
	// for a nested-module tag), without a refs/tags/ prefix.
	Name string
	// Prefix is everything through the final "/" of a nested-module tag
	// ("libs/gopherbuzz/" for "libs/gopherbuzz/v0.1.0"); "" for a root tag with
	// no "/" in its name.
	Prefix string
	// Version is Name's version portion (Name with Prefix stripped) parsed as
	// semver. It is the zero value - test Version.Original == "" - when Name
	// (or its portion after Prefix) is not a semver-shaped tag at all, or when
	// parsing it failed: an annotated tag like "checkpoint" or "release-2026"
	// is a legitimate, non-error case, not a reason to carry a separate
	// IsSemver bool that could disagree with the zero value it's mirroring.
	Version SemverVersion
	// Date is when an annotated tag was created, else when its revision was
	// recorded. Zero if the VCS reported no timestamp.
	Date time.Time
	// ID is the revision identifier the tag resolves to.
	ID string `buzz:"id"`
}

VCSTag is a VCS-agnostic release marker: a name pinned to a revision. Only the facts every tagging backend agrees on are modeled - an annotated tag's tagger and message are not, since a lightweight tag has neither. Reach for vcs.exe() for backend-specific tag work.

func (VCSTag) BuzzObject added in v0.4.0

func (v VCSTag) BuzzObject() BuzzObject

type VolatilityReport added in v0.2.0

type VolatilityReport struct {
	Threshold float64            `json:"threshold" yaml:"threshold"`
	Targets   []VolatilityTarget `json:"targets"   yaml:"targets"`
}

VolatilityReport is the per-(project, target) volatility lens: the run-outcome axis of insight. It is computed from the shared runtime-history file - a pure file read plus the Wilson-score compute, no shell-out and no workspace graph - and folded into InsightView (the console serves it under the "volatility" key of GET /api/v1/insight) and into the volatility lens. Threshold is the configured Wilson lower-bound above which a target is treated as volatile (Volatility.Threshold); a target's Volatile field is Score >= Threshold.

func (VolatilityReport) BuzzObject added in v0.4.0

func (v VolatilityReport) BuzzObject() BuzzObject

type VolatilityTarget added in v0.2.0

type VolatilityTarget struct {
	Project       string    `json:"project"             yaml:"project"`
	Target        string    `json:"target"              yaml:"target"`
	Score         float64   `json:"score"               yaml:"score"`
	Volatile      bool      `json:"volatile,omitempty"  yaml:"volatile,omitempty"`
	Pass          int       `json:"pass"                yaml:"pass"`
	Fail          int       `json:"fail"                yaml:"fail"`
	VolatileCount int       `json:"volatile_count"      yaml:"volatile_count"`
	Samples       int       `json:"samples"             yaml:"samples"`
	LastPass      time.Time `json:"last_pass,omitempty" yaml:"last_pass,omitempty"`
}

VolatilityTarget is one (project, target) pair's recorded volatility: the Wilson lower-bound Score against the threshold, the recent-outcome tallies, how many outcomes are retained, and the most recent passing (or volatile) run. Pass/Fail/VolatileCount count the retained window.

func (VolatilityTarget) BuzzObject added in v0.4.0

func (v VolatilityTarget) BuzzObject() BuzzObject

type Workspace

type Workspace struct {
	// Root is the workspace root, always absolute and symlink-free.
	Root string

	// Projects maps project path to *Project.
	Projects map[string]*Project

	// VCSOptions holds explicit VCS configuration injected at construction time.
	VCSOptions VCSOptions
	// contains filtered or unexported fields
}

Workspace is the discovered set of projects under a root directory.

func (*Workspace) All

func (w *Workspace) All() []*Project

func (*Workspace) Get

func (w *Workspace) Get(path string) *Project

Get returns the project with the given path, or nil.

func (*Workspace) GraphObserver

func (w *Workspace) GraphObserver() Observer

GraphObserver returns the default graph observer, or nil.

func (*Workspace) SetGraphObserver

func (w *Workspace) SetGraphObserver(o Observer)

SetGraphObserver installs a default graph observer. Pass nil to clear. For concurrent callers (daemon) use ContextWithGraphObserver instead.

func (*Workspace) UnderPath

func (w *Workspace) UnderPath(prefix string) []*Project

UnderPath returns every project whose Path has prefix as a path prefix.

type WorkspaceConfig

type WorkspaceConfig struct {
	CacheDir    string
	Concurrency int
}

WorkspaceConfig carries infrastructure details for Inspector.Workspace that are not part of the WorkspaceRepository interface (cache path, concurrency).

type WorkspaceEntry

type WorkspaceEntry struct {
	Root         string `json:"root"                    yaml:"root"`
	VCSBaseRef   string `json:"vcs_base_ref,omitempty"  yaml:"vcs_base_ref,omitempty"`
	CacheDir     string `json:"cache_dir,omitempty"     yaml:"cache_dir,omitempty"`
	Concurrency  int    `json:"concurrency,omitempty"   yaml:"concurrency,omitempty"`
	ProjectCount int    `json:"project_count"           yaml:"project_count"`
}

WorkspaceEntry holds details about the active workspace.

type WorkspaceReader

type WorkspaceReader interface {
	Root() string
	All() []*Project
	Get(path string) *Project
	// Graph returns the PROJECT dependency graph (project -> project, from
	// depends_on). See Inspector.TargetGraph for the target-level graph.
	Graph() (*Graph, error)
	VCSOptions() VCSOptions
	Where(dir string) (*Project, bool)
}

WorkspaceReader is the read-only in-memory view of a discovered workspace.

type WorkspaceReport added in v0.4.0

type WorkspaceReport struct {
	Definition string           `json:"definition" yaml:"definition"`
	Count      int              `json:"count"      yaml:"count"`
	Workspaces []WorkspaceEntry `json:"workspaces" yaml:"workspaces"`
}

WorkspaceReport is the "describe workspace[s]" envelope.

type WorkspaceRepository

type WorkspaceRepository interface {
	WorkspaceReader
	TargetExpander
	AffectedComputer
	Inspector
}

WorkspaceRepository is the full domain interface for a discovered workspace. Prefer the narrowest embedded role a consumer actually uses.

func WorkspaceFromContext

func WorkspaceFromContext(ctx context.Context) WorkspaceRepository

WorkspaceFromContext returns the WorkspaceRepository from ctx, or nil when no workspace was installed (e.g. a direct SpellDriver call outside a run).

Directories

Path Synopsis
gen

Jump to

Keyboard shortcuts

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