notes

package
v0.4.3 Latest Latest
Warning

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

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

Documentation

Overview

Package notes is the workspace's human-authored knowledge store: discrete entries (one markdown file per note, YAML frontmatter carrying the structured fields) that attach to graph entities without being derived from any of them.

It is the counterpart to internal/memory, and the two differ on every axis that matters:

  • WHO WRITES. A note is written by a person in their editor. Nothing here is an agent-facing write path, and there is deliberately no Put: `magus notes edit` opens $EDITOR and gets out of the way.
  • WHERE IT LIVES. In the CHECKOUT, at a path the workspace declares - not in XDG state like memory, whose package doc explains that "a developer's working memory does not belong in a shared checkout". A note inverts exactly that clause: a team's shared understanding does belong there. Being in the checkout is also what buys per-author attribution for free, since the @vcs shard already mints author nodes for files it can see, and nothing outside the checkout can ever have that.
  • WHAT IT MAY SAY. memory REQUIRES a ref, because an agent-written claim has to be anchored to something checkable. A note is the class that cannot be checked that way: its only provenance is a person. So anchors are required for FINDABILITY, but prose is the payload rather than a caption.

Everything in the graph other than a note is DERIVED from workspace content - docs from markdown, rationale from comments, symbols from an index, authors from git. Delete the graph and rebuild and you get all of it back. A note is the one node class that is injected rather than extracted, and no rebuild recovers it.

Index

Constants

View Source
const FrontmatterKey = "magus"

FrontmatterKey is the single frontmatter key magus owns. Everything magus stores lives under it, and everything outside it belongs to someone else.

One namespaced key rather than bare `id:`/`anchors:` at the top level, because a notes store may be an Obsidian vault whose frontmatter is already busy: `tags`, `aliases`, `cssclass`, and whatever the plugin ecosystem invented this month. `id` in particular is common enough that squatting on it would silently capture other tools' notes as magus ones. Claiming exactly one key makes "is this addressed to magus" a precise question rather than a guess about a generic name.

View Source
const MaxNotes = 5000

MaxNotes bounds one scan. A store may be a vault of any size, and a graph build must not become unbounded work because someone pointed magus at their whole writing life. The cap is REPORTED rather than silently applied, since quietly loading half a store is the kind of partial truth this whole feature exists to avoid.

Variables

View Source
var ErrDisabled = errors.New("notes: this workspace declares no notes path (set knowledge.notes.shared or knowledge.notes.private in magus.yaml)")

ErrDisabled reports that this workspace declares no notes path, which is the default and is not a failure. Every entry point returns it rather than inventing a location: the guard rule in particular must fire only on a DECLARED path, because a deny fired on a guessed one blocks real work.

Functions

func DeclarationHeld

func DeclarationHeld(ctx context.Context, res Resolver, a Anchor) bool

DeclarationHeld reports whether the anchored subject's DECLARATION is the one the note was last reviewed against. Call it only once Digest has already found a change: it is what separates a body edit, which rarely invalidates prose, from drift worth re-reading for.

Exported so the CLI and the daemon grade with one rule rather than two. They present the answer differently - an Issue with a hint, an AnchorStatus with a detail - but a surface whose verdict disagreed with `magus notes verify` would be a second opinion rather than a second view of one answer.

False is the answer for every uncertainty: no recorded declaration digest, no computable one, or an error. The finding then degrades to ungraded drift instead of to silence, which is the safe direction - an ungraded finding overstates a change, while grading on absent data would understate one.

func Digest

func Digest(lines []string) string

Digest fingerprints the anchored source so a CHANGE is detectable, not merely a deletion.

The existence check answers the easy question - a rename or a delete, which the anchor itself already reports. This answers the harder and more common one: the code is still there and quietly stopped meaning what the note says. That case is the reason the whole store needs a gate, because it is invisible to a reader who trusts the note.

NORMALIZATION IS THE WHOLE DESIGN, and it is tuned in one direction on purpose. A fingerprint that fires on reformatting produces false drift; the flags get ignored; and an ignored gate is worse than no gate at all, because the store then looks checked. So:

  • leading and trailing whitespace per line is dropped (re-indentation is free)
  • runs of internal whitespace collapse to one space (alignment is free - gofmt realigning struct tags or trailing comments must not read as an edit)
  • blank lines are dropped entirely (spacing is free)

What it deliberately does NOT do is normalize tokens. Renaming a variable, changing a literal, or reordering statements all change the digest, because all of them can change what the code means, and a note about it deserves a second look.

The result is a whitespace-insensitive, token-sensitive fingerprint - the same trade Fiberplane's Drift makes with a tree-sitter AST hash, reached without a parser per language. magus indexes any language SCIP can, so a parser-based fingerprint would work for a few and silently do nothing for the rest.

func DigestDecl

func DigestDecl(src string, start, end int) string

DigestDecl fingerprints just the DECLARATION at the head of [start, end] - the first non-blank line of the range - so a change to what the anchored thing IS can be told apart from a change to how it does it.

This exists because the body fingerprint alone reports far more than it should. Measured across 1,496 repositories, only about one code change in thirty-eight to a documented subject actually invalidates the prose about it; a hash over the whole body fires on all thirty-eight. The same study measured WHICH changes matter, and the answer is structural: a signature or constructor change is 39-105x more likely to be accompanied by a prose update than a rename, a literal, or an expression edit. So the declaration line carries almost all of the signal, and the body carries almost all of the noise.

One line, and no parser, deliberately. magus fingerprints whatever SCIP can index, and a per-language parser would grade a few languages precisely and silently do nothing for the rest - which is the failure mode the body digest already avoided by normalizing whitespace instead of parsing. A first non-blank line is a heuristic for a signature, not a parse: it is right for C-family, Go, Java, TypeScript, Python and Rust declarations, and wrong for a signature wrapped across lines or preceded by an attribute or decorator. Being wrong here costs a grade, never a verdict - the body digest still detects the change either way.

func DigestRange

func DigestRange(src string, start, end int) string

DigestRange fingerprints lines [start, end] of src, both 1-based and inclusive.

Out-of-range bounds yield "" rather than an error or a panic: the caller's line numbers come from a symbol index that can lag the file on disk, and a stale index must degrade to "cannot fingerprint this" instead of taking a verify run down or, worse, fingerprinting the wrong lines and reporting confident nonsense.

func Dir

func Dir(root string, scope Scope, declared string) (string, error)

Dir resolves a scope's notes directory from its declared path, returning ErrDisabled when nothing is declared.

One entry point rather than a resolver per scope, because every caller has the scope in hand and had to pair it with the matching function by eye - a pairing nothing checked, and getting it backwards would silently grant a shared store the private store's freedom to live outside the checkout.

The path is DECLARED rather than discovered by convention for the same reason the declared-output guard rule is definitive while the filename heuristics are not: an advisory fired on a guess trains the reader to ignore it, and a deny fired on a guess blocks work that was never opted in.

func HunkLocator

func HunkLocator(hunk int) string

HunkLocator renders a hunk index the way a capture heading wants it. Here rather than at the call site so every source that has a notion of "the Nth chunk of a file" spells it the same way in the store.

func Inspect

func Inspect(dir string) ([]Note, []Issue, error)

Inspect returns every magus note in the store, plus any issue found reading one.

A FILE IN THE STORE IS NOT AUTOMATICALLY A NOTE. The store may be a directory magus owns, or it may be someone's Obsidian vault with thousands of files that have nothing to do with this workspace - and pointing at a vault is a supported thing to do. So the discriminator is explicit: a file is a magus note when its frontmatter declares `anchors:`, and anything else is somebody's writing that magus reads past in silence.

Getting this wrong is not a small matter of tone. Treating every .md as a malformed note turned a 1,900-file vault into 1,500 error-severity issues and half a megabyte of output from `magus notes ls`, which then exited non-zero - the feature reporting the user's own notes as damage.

The walk is RECURSIVE because vaults are foldered; a flat read silently ignored 400 notes in a subdirectory, which is worse than failing. Dot-directories are skipped (.obsidian, .git, .trash all hold machinery rather than prose), and so is node_modules.

func LineLocator

func LineLocator(line int) string

LineLocator renders a line number for a source that anchors by line, as a forge's inline comment does.

A SEPARATE spelling from HunkLocator, not a shared "position": a hunk index is a coordinate in one patch and a line number is a coordinate in a file, and a heading that blurred them would leave a later reader unable to tell which they were looking at - by which time the patch is long gone and only one of the two still means anything.

func Path

func Path(dir, name string) (string, error)

Path returns where a note of this name WOULD live, without reading anything. `magus notes edit` needs it to hand $EDITOR a path for a note that does not exist yet.

It is not where an EXISTING note lives, and the difference is not academic: a note that declares an id is identified by that id rather than by its filename, so for one that has since been renamed this names a file that is not it. Read the note and use Note.Path for that; calling this instead is what had `notes edit` open a blank scaffold beside the note it was asked to edit.

func RecordDigests

func RecordDigests(ctx context.Context, dir, name, rev string, res Resolver) (int, error)

RecordDigests stamps each anchor with the anchored content's CURRENT fingerprint and returns how many changed, so the caller can say what it did.

This is a re-attestation, not a repair, and the distinction is the whole reason it is separate from verify. Verify never writes; a person running this is stating "I have read this note against the code as it is now". Because notes live in the checkout, that statement lands as a reviewed commit under their name - the one genuinely good idea in Google's freshness stamps, which are otherwise a nag with no gate behind them.

It must therefore never run implicitly on an agent's behalf. Recording a digest silently is how a real drift flag gets cleared without anyone reading the prose, which is the failure every anchoring product in this space eventually shipped.

rev is what makes a later drift report actionable: the digest says the anchored code changed, and the commit says what to diff it against. Empty just omits the provenance.

func Save

func Save(dir string, n Note) error

Save writes a note atomically after validating it. It is NOT a general write API for callers to build on: it exists for `magus notes edit` to lay down a scaffold and for verify to record a re-anchoring. There is no Put, and no agent-facing write path.

func Validate

func Validate(n Note) error

Validate enforces the note schema on the way IN and on the way OUT, so no caller can hold a shape the model does not expect.

Types

type Anchor

type Anchor struct {
	Kind   AnchorKind `json:"kind" yaml:"kind"`
	Target string     `json:"target" yaml:"target"`
	// Digest fingerprints the anchored content as of the last review, so a CHANGE is
	// detectable and not merely a deletion. Existence checks catch a rename or a delete;
	// this catches the far more common case where the code still exists and quietly
	// stopped meaning what the note says. Empty when the kind has no content to hash,
	// and empty until the note has been verified once.
	Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
	// DeclDigest fingerprints only the anchored subject's DECLARATION - see DigestDecl. It
	// grades what Digest detects: when both moved, what the subject IS changed, and the note
	// is very likely wrong; when only Digest moved, the body was edited under an unchanged
	// signature, which is the case that almost never invalidates prose.
	//
	// Empty for a kind with no declaration to speak of (a whole file, a project, a target),
	// and empty on a note stamped before grading existed. Both are ungraded, never a verdict.
	DeclDigest string `json:"decl_digest,omitempty" yaml:"decl_digest,omitempty"`
	// Commit is the revision this anchor was last reviewed against. PROVENANCE ONLY -
	// resolution always runs against the working tree. A pinned revision never breaks,
	// which is precisely why it must never be the anchor: it would go on pointing at
	// correct-looking frozen content long after the thing it described was deleted.
	Commit string `json:"commit,omitempty" yaml:"commit,omitempty"`
}

Anchor is one typed attachment from a note to a graph entity.

func ParseAnchor

func ParseAnchor(s string) (Anchor, error)

ParseAnchor parses one "kind:target" anchor, the form a flag or a config file carries.

Split on the FIRST colon only: a SCIP symbol key is full of them ("m internal/cache/Store#Put()."), so splitting on the last would silently truncate exactly the anchors this store cares most about.

type AnchorHit

type AnchorHit struct {
	Note  string `json:"note" yaml:"note"`
	Title string `json:"title,omitempty" yaml:"title,omitempty"`
	// Pos is the anchor's position in its note. Exported because it is half the documented
	// sort key, so a caller can check the ordering rather than trust it.
	Pos    int        `json:"pos" yaml:"pos"`
	Kind   AnchorKind `json:"kind" yaml:"kind"`
	Target string     `json:"target" yaml:"target"`
	// Matched is the changed thing that produced this hit: a symbol NODE ID for MatchSymbol, a
	// file path for MatchFile and MatchNeighbor. It answers the reader's next question - which of
	// the diff's many paths pulled this note in - which neither the anchor nor the strength
	// can answer alone.
	Matched string        `json:"matched" yaml:"matched"`
	Match   MatchStrength `json:"match" yaml:"match"`
	// Status is the anchor's drift finding carried through untouched: "" when it is clean and
	// StatusUngraded when nothing graded it. This join reports what a diff TOUCHES and never
	// grades, so it can neither invent a verdict nor upgrade an ungraded anchor to clean.
	Status IssueCode `json:"status,omitempty" yaml:"status,omitempty"`
}

AnchorHit is one note anchor that a diff touched.

func AnchorHits

func AnchorHits(res []ResolvedAnchor, files, symbols []string) []AnchorHit

AnchorHits reports every note anchor that a diff's changed files and symbols touch.

It is the join nothing else performs. The store knows what its notes are ABOUT and a diff knows what moved; until the two meet, finding the note that explains the code you are editing requires already suspecting the note exists. That is the case the store was least able to serve, and the one it exists for.

files are the diff's changed paths, matched against a file anchor's target. symbols are its changed symbols' GRAPH NODE IDS, matched against ResolvedAnchor.NodeID - never against the anchor's bare target, which is spelled in a different vocabulary and matched nothing. Both are compared by EXACT equality. Nothing here guesses - no prefix match, no basename fallback, no fuzzy symbol lookup. AnchorIssues' doc records the measurement behind that refusal, and it binds harder here: an unresolved anchor at least admits it failed, while a note surfaced against code it is not about spends the reader's trust in every later hit.

Only file and symbol anchors are joined. project, target and note anchors are SKIPPED because a diff carries paths and symbol ids and no target identity at all, so there is nothing to match them against - which is different from their never matching, and must not render as an absence of relevant notes. Revisit when a diff carries its affected targets.

Duplicates collapse per ANCHOR: one anchor yields at most one hit, the strongest it has, so a symbol anchor whose id changed reports MatchSymbol instead of also reporting the MatchNeighbor hit underneath it. Two DISTINCT anchors of one note stay two hits - including the common case of a note anchoring both a file and a symbol inside it, which is two claims about two subjects. A renderer wanting one row per note groups them.

The result is ordered by note name then anchor position, and is pure: no I/O, no clock, and no dependence on the order of res.

type AnchorKind

type AnchorKind string

AnchorKind is the closed set of things a note may attach to.

Deliberately absent: any kind carrying a POSITION. A node ID is checkable, so its breakage is reportable; a line number is not, so its breakage is invisible - it changes on the next edit above it with nothing to detect. That single distinction is what separates this from every line-anchored review comment, code tour, and web annotation, all of which rot and then paper over it with fuzzy re-matching.

const (
	AnchorSymbol  AnchorKind = "symbol"
	AnchorFile    AnchorKind = "file"
	AnchorProject AnchorKind = "project"
	AnchorTarget  AnchorKind = "target"
	AnchorNote    AnchorKind = "note"
)

type Capture

type Capture struct {
	// Title is what a reader scans for months later. A capture with a generated title is
	// findable only by the code it touched, which is why the caller is asked for one.
	Title  string
	Source Source
	Tags   []string
	// Entries are in the order they should be read, which is the caller's judgment rather
	// than this package's - a review thread reads by file, a chat log by time.
	Entries []CaptureEntry
}

Capture turns a conversation into a note without the store learning where conversations come from.

A review thread lives in internal/diff and dies with the session that held it: the store keeps which hunks were read and nothing else, so the comments are gone when the process is. That is the whole motivation, and it is also why this type is not types.DiffSession. A capture is prose plus provenance; the caller that HAS a session is the one that knows how to describe it, and keeping that mapping outside this package leaves room for a second source (a forge's review thread, say) without the notes store gaining a second opinion about what a conversation is.

func (Capture) Note

func (c Capture) Note(name string) (Note, error)

Note renders the capture as a note under name.

The body is markdown assembled here rather than by the caller, so every capture in a store reads the same way and a later reader can tell one at a glance. Anchors are derived: one file anchor per distinct subject, which is what makes a captured thread turn up when someone asks what is known about a file.

Fails when there is nothing to capture. An empty transcript would be a note asserting that a conversation happened and declining to say what was said, and it would fail Validate for want of an anchor anyway - better to say which of the two went wrong.

type CaptureEntry

type CaptureEntry struct {
	// Subject is what the message was about, in the source's own terms - a file path for a
	// review comment. It becomes a file anchor, so a capture is findable from the code.
	Subject string
	// Locator narrows Subject within itself, rendered beside it and never parsed. A hunk
	// index for a review comment; empty when the source has no such notion.
	Locator string
	Author  string
	Body    string
	// Resolved marks a thread the participants closed. Kept because "we discussed this and
	// settled it" and "we discussed this and stopped" are different things to find later.
	Resolved bool
}

CaptureEntry is one message. Author is a display name and nothing branches on it: a transcript records who said something, and a store that tried to VERIFY that would be making the authorship claim the capture exists to avoid making.

type Issue

type Issue struct {
	Severity Severity  `json:"severity" yaml:"severity"`
	Code     IssueCode `json:"code" yaml:"code"`
	Path     string    `json:"path,omitempty" yaml:"path,omitempty"`
	Note     string    `json:"note,omitempty" yaml:"note,omitempty"`
	Message  string    `json:"message" yaml:"message"`
	Hint     string    `json:"hint,omitempty" yaml:"hint,omitempty"`
}

Issue is one problem found while scanning, in the same shape memory reports, so a frontend can render both stores' findings the same way.

func AnchorIssues

func AnchorIssues(ctx context.Context, dir string, res Resolver) ([]Issue, error)

AnchorIssues reports every anchor that no longer resolves, with the coarser anchor it degrades to. It is the Issue projection of ResolveAnchors' pass - the same grader, filtered to the anchors something is wrong with - and never a second opinion about one.

The empirical case for doing this at all is blunt: humans do not maintain references in prose. Under 9% of links in source comments are ever revised after the commit that added them, and an outdated code reference in documentation survives 4.7 years on average. A store that relies on its authors noticing decays exactly like every corpus that came before it, so the anchor has to be machine-checked and the staleness surfaced.

The degradation ladder is Gerrit's, which is the only system in this space whose comments never silently lose their anchor: a range comment becomes a file comment, a file comment becomes a patchset comment, and the demotion is VISIBLE rather than guessed. Here a symbol degrades to its file and a file to its project, and the note is reported as degraded rather than quietly re-pointed.

What this deliberately does NOT do is guess. Nothing here searches for a renamed symbol or a moved file: a low-confidence match is worse than an admitted failure, measured - when users were shown anchors placed on a single weak match, the median rating was "terrible". Every system that guessed instead (a lost tour step parked at line 2000, a highlight that silently fails to render, a quote anchored within 50% edit distance) produced confident, wrong placement. An unresolved anchor is reported as unresolved.

type IssueCode

type IssueCode string

IssueCode names what verify found, so a caller branches on the code rather than matching the message - the messages are written for a person and change freely.

const (
	// CodeInvalidEntry: a file under the store declares a magus block that does not parse
	// or does not validate. The note is unreadable, not merely stale.
	CodeInvalidEntry IssueCode = "invalid-entry"
	// CodeStoreTruncated: the walk stopped at MaxNotes, so the report describes a prefix
	// of the store rather than the store.
	CodeStoreTruncated IssueCode = "store-truncated"
	// CodeMissingNote: a note anchors to another note that is not in the store.
	CodeMissingNote IssueCode = "missing-note"
	// CodeDanglingAnchor: an anchor names an entity the graph no longer has.
	CodeDanglingAnchor IssueCode = "dangling-anchor"
	// CodeDriftedAnchor: the anchored entity still exists, and what it IS changed since a
	// person last reviewed the note against it - its declaration moved, or the note predates
	// grading and only the whole-content fingerprint is known.
	CodeDriftedAnchor IssueCode = "drifted-anchor"
	// CodeAnchorBodyChanged: the anchored entity's content changed UNDER AN UNCHANGED
	// declaration. Reported separately from drift because it is a different bet: measured
	// across 1,496 repositories, an edit that leaves the signature alone is 39-105x less
	// likely to be accompanied by a prose update than one that changes it. Worth surfacing,
	// not worth interrupting for, and never worth gating on.
	CodeAnchorBodyChanged IssueCode = "anchor-body-changed"
	// CodeUnverifiableAnchor: the anchored entity exists, and its fingerprint could not be
	// computed - so whether the note still holds is UNKNOWN rather than wrong.
	CodeUnverifiableAnchor IssueCode = "unverifiable-anchor"
)
const StatusUngraded IssueCode = "ungraded-anchor"

StatusUngraded is the Status of an anchor nothing graded. It is distinct from "", which means grading ran and found the anchor clean: a caller reading unmeasured as fresh would report a stale note as current, which is the one failure anchor grading exists to prevent.

Named for the field rather than joining the Code* family in store.go: verify never emits it, so it is not an Issue anyone can be shown. The wire spelling keeps that family's shape.

type MatchStrength

type MatchStrength string

MatchStrength says HOW a hit was found.

It is reported rather than collapsed because the weak case and the strong case ask the reader for different things: a note on the exact symbol you changed is almost certainly about your edit, while a note on some other symbol in the same file may have nothing to do with it. Rendering both as "this note applies" lends the weak case the strong one's authority, and a surface that overstates its relevance is one readers learn to skip.

const (
	// MatchSymbol: the anchor names a symbol the diff changed.
	MatchSymbol MatchStrength = "symbol"
	// MatchFile: the anchor names a file the diff changed.
	MatchFile MatchStrength = "file"
	// MatchNeighbor: the anchor names a symbol the diff did NOT change, in a file it did. The
	// note may be about untouched code that merely shares a file with the edit.
	//
	// Unreachable in the shipped path as of this writing: it needs ResolvedAnchor.File on a
	// SYMBOL anchor, which only a caller holding the graph can supply, and no caller does yet.
	MatchNeighbor MatchStrength = "neighbor"
)

Each value names WHAT matched and nothing more: the subject is the only axis they differ on. Do not qualify them exact/fuzzy - every match here is exact equality, so such a prefix would name an inexact tier that does not exist and, given the refusal to guess, cannot.

type Note

type Note struct {
	// Name is the note's identity in memory: its ID when one is declared, otherwise its
	// path within the store. Never serialized under this key - see ID.
	Name string `json:"name" yaml:"-"`
	// ID is an optional stable identity, and it is what makes a store survive being a
	// vault someone reorganizes.
	//
	// Without it a note is identified by its path, and Obsidian's rename - which helpfully
	// rewrites every [[wikilink]] and knows nothing about magus - silently changes the
	// note's graph ID and dangles every note-kind anchor pointing at it. That is the exact
	// failure this feature argues against everywhere else: an identity that encodes a
	// location. magus stamps one on every note it creates; a hand-written vault note gets
	// path identity until its author adds one.
	ID      string   `json:"id,omitempty" yaml:"id,omitempty"`
	Title   string   `json:"title" yaml:"title"`
	Tags    []string `json:"tags,omitempty" yaml:"tags,omitempty"`
	Anchors []Anchor `json:"anchors" yaml:"anchors"`
	// Source records where the prose came from when a person did not type it here. Nil on a
	// written note, which is the overwhelming majority and the reason it is a pointer: an
	// empty block in every hand-written note's frontmatter would be litter in someone's vault.
	Source *Source `json:"source,omitempty" yaml:"source,omitempty"`
	Body   string  `json:"body,omitempty" yaml:"-"`
	// Modified is the file's modification time, filled in on read. It is observed, not
	// stored, so it cannot disagree with the file it describes.
	Modified time.Time `json:"modified" yaml:"-"`
	// Path is the file this note was read FROM, observed on read for the same reason
	// Modified is: it cannot disagree with the file it describes.
	//
	// It exists because Name cannot answer the question. A note that declares an id is
	// identified by that id, and the id is deliberately independent of where the file
	// sits - that is the whole point of having one. So joining the store dir to Name
	// names a real file only while the two happen to agree, and names a file that does
	// not exist the moment someone renames the note in their vault. Two callers derived
	// it that way and both were wrong: the console showed a reader a path they could not
	// open, and `notes edit` scaffolded a SECOND note beside the one being edited.
	//
	// Empty on a Note that was built rather than read (Scaffold, a new note from stdin),
	// where there is no file yet to have a path.
	Path string `json:"-" yaml:"-"`
}

Note is one human-authored entry. Name is the identity and the on-disk basename.

The frontmatter carries only what cannot be derived. There is no Author field and no Trust field: both would be self-attested and therefore forgeable by whatever wrote the file, when authorship already comes from git via the @vcs shard and trust is structural (it follows from which store an entry is in, not from a value the writer chose).

There are no created/updated fields either, and that is the same argument. These files are edited by hand, so a stored timestamp is wrong the first time someone saves without going through magus - a self-maintained field that rots is exactly the failure this whole store is built to avoid. Modified is derived from the file and never serialized; git carries the real history.

func Get

func Get(dir, name string) (Note, error)

Get returns one note by name, or os.ErrNotExist if it is absent.

func List

func List(dir string) ([]Note, error)

List returns every note in name order. An error-severity issue fails the call rather than silently skipping the note; run Verify for every problem and its repair hint.

func Scaffold

func Scaffold(name string) Note

Scaffold is the stub `magus notes edit` writes for a new note, so the author starts from a valid shape rather than a blank file. It is intentionally the smallest thing that passes Validate once the placeholder anchor is replaced.

It returns a Note rather than the rendered bytes so that CREATING a note goes through Save like every other write here. Handing back bytes invited the caller to os.WriteFile them, which is what it did: the one path that brings a note into existence was the one path with neither validation nor an atomic write.

type ResolvedAnchor

type ResolvedAnchor struct {
	// Note is the declaring note's Name and Title its heading. Both ride along because a hit
	// is rendered with the diff in hand and the store nowhere near it.
	Note  string `json:"note" yaml:"note"`
	Title string `json:"title,omitempty" yaml:"title,omitempty"`
	// Pos is the anchor's 0-based position in the note's anchor list, and the second half of
	// the output's sort key. It is carried rather than inferred from slice order: a join whose
	// result depended on the order its input arrived in would report differently for the same
	// workspace depending on who assembled the slice.
	Pos    int    `json:"pos" yaml:"pos"`
	Anchor Anchor `json:"anchor" yaml:"anchor"`
	// File is where a symbol anchor's subject currently lives, workspace-relative, and "" when
	// unknown. SUPPLIED rather than derived: a SCIP symbol key names a package and a
	// descriptor but never a file, and only the graph knows where a symbol sits - which this
	// package deliberately never learns (see internal/graph/knowledge.NoteResolver, whose doc
	// states the one-way dependency). Empty costs the weaker neighbor match and nothing else.
	File string `json:"file,omitempty" yaml:"file,omitempty"`
	// NodeID is the knowledge graph's id for this anchor's subject, and "" when the caller
	// did not mint one. SUPPLIED for the same reason File is: the id spelling belongs to the
	// graph (knowledge.AnchorNodeID), and this package must not learn it.
	//
	// A symbol anchor without one cannot match at all. A diff names its changed symbols by
	// node id while an anchor carries the bare SCIP key, so comparing the two vocabularies
	// directly never matched anything, and the headline case - the note attached to the
	// symbol you just edited - silently reported nothing.
	NodeID string `json:"node_id,omitempty" yaml:"node_id,omitempty"`
	// Status is the IssueCode resolution reported for this anchor, "" when it is clean, and
	// StatusUngraded when nothing graded it.
	Status IssueCode `json:"status,omitempty" yaml:"status,omitempty"`
}

ResolvedAnchor is one anchor, the note that declares it, and what checking it found. ResolveAnchors builds them - one per anchor, healthy ones included - and AnchorIssues is the same pass projected onto the findings, never a second opinion. []Issue cannot drive this join: it carries anchor identity only inside prose and says nothing about an anchor that is fine, which is most of them and most of what a diff touches.

func ResolveAnchors

func ResolveAnchors(ctx context.Context, dir string, res Resolver) ([]ResolvedAnchor, error)

ResolveAnchors grades EVERY anchor of every note, healthy ones included. It is the resolution pass itself, and AnchorIssues is that pass projected onto the anchors that graded to a finding, so the two views cannot disagree - re-deriving a grade for either would be the second opinion DeclarationHeld exists to prevent.

The per-anchor view is the primitive because an Issue can only describe a problem, while the joins built on top of this store - which note does this diff touch - are mostly about anchors that are perfectly fine. Reconstructing those from the Issue view is impossible in both directions: an anchor's kind and target live only inside a message written for a person, and a clean anchor produces no Issue at all.

res may be NIL, meaning ungraded: every anchor comes back StatusUngraded, and the caller learns WHAT is anchored while claiming nothing about freshness nobody measured. That is the state a knowledge graph which will not load leaves behind, and it is deliberately not an error - a broken index should cost the drift column, never the answer.

Status is the IssueCode this anchor graded to, "" when it is clean, and StatusUngraded when res was nil, so a caller renders drift without asking twice. Pos is the anchor's index in its note, carried because callers sort and key on it rather than on slice order.

File is filled in for file anchors only, graded or not. A symbol's file is deliberately not derivable here: a SCIP symbol key names a package and a descriptor, never a path, and only the graph knows where a symbol currently sits - a dependency this package does not have and must not grow (see internal/graph/knowledge.NoteResolver). A caller holding the graph populates it; one that leaves it empty loses the weaker neighbor match in AnchorHits and nothing else.

NodeID is left empty here for the same reason and is NOT as forgiving: a caller that skips it loses every symbol match AnchorHits could have made, because the diff names its changed symbols in the graph's vocabulary and the anchor carries the bare SCIP key.

Ordering follows the store walk and then anchor position. Every anchor appears exactly once, including anchor kinds no join can match, because absent is indistinguishable from clean.

type Resolver

type Resolver interface {
	// Resolves reports whether the given anchor still names a live entity.
	Resolves(ctx context.Context, a Anchor) bool
	// Digest returns the CURRENT fingerprint of the anchored content.
	//
	// An empty digest with a nil error means the kind has nothing to hash: nothing to say,
	// and nothing wrong. A non-nil error means the fingerprint COULD NOT be computed - an
	// unbuilt symbol index, an indexer that reported no enclosing range, an unreadable
	// file.
	//
	// Neither is drift, and that direction is deliberate: a gate that manufactures a change
	// out of its own blind spot gets ignored, and an ignored gate is worse than none. They
	// are separated because they are differently actionable - nothing to hash is the end of
	// the story, while a failure names something a reader can go fix. Collapsing both into
	// "" hid four distinct causes behind one silence.
	Digest(ctx context.Context, a Anchor) (string, error)
	// DeclDigest returns the CURRENT fingerprint of the anchored subject's declaration, or
	// "" when the kind has no declaration to speak of.
	//
	// Errors are NOT reported separately from Digest's: this only ever grades a change
	// Digest already found, so a declaration that cannot be computed degrades to an ungraded
	// finding rather than to a second complaint about the same anchor.
	DeclDigest(ctx context.Context, a Anchor) (string, error)
}

Resolver answers whether one anchor still names something that exists.

Injected rather than imported: this package must not learn about the knowledge graph, so the composition root - the one place allowed to know both - supplies the lookup.

type Scope

type Scope string

Scope names which of the two stores a path belongs to. The two differ in exactly one way that matters - whether the location can attribute a note to anyone - and every other difference below follows from it.

const (
	// ScopeShared is the team's store: inside the checkout, so a commit attributes each
	// note and review saw it.
	ScopeShared Scope = "shared"
	// ScopePrivate is the reader's own store: anywhere on disk, attributed to nobody.
	ScopePrivate Scope = "private"
)

func (Scope) ConfigKey

func (s Scope) ConfigKey() string

ConfigKey is the magus.yaml key that declares this scope's directory, so a diagnostic names the line the reader has to edit rather than the concept.

type Severity

type Severity string

Severity separates the two things verify reports, and the split is load-bearing rather than cosmetic: an error means the store could not be READ as declared, so a caller acting on the result is acting on an incomplete store; a warning means a note was read fine and what it points AT has moved. Only the first should ever stop a caller.

const (
	SeverityError   Severity = "error"
	SeverityWarning Severity = "warning"
)

type Source

type Source struct {
	Kind SourceKind `json:"kind" yaml:"kind"`
	// Ref identifies the conversation within Kind - a review session id. Opaque here.
	Ref string `json:"ref,omitempty" yaml:"ref,omitempty"`
	// AsOf is the subject's identity at capture time: for a review thread, the digest of the
	// patch the comments were written against. It is what makes a stale capture detectable
	// rather than merely old.
	AsOf string `json:"as_of,omitempty" yaml:"as_of,omitempty"`
	// Captured is when the transcript was taken, which is NOT the note file's mtime: editing
	// a capture's surrounding prose later moves the mtime and must not move this.
	//
	// A stored timestamp, and deliberately not the created/updated fields Note refuses above.
	// Those describe the FILE, which git and the filesystem already describe and which a hand
	// edit immediately falsifies. This describes an event that happened once somewhere else
	// and that nothing in the repository records, so a later edit cannot make it wrong.
	Captured time.Time `json:"captured,omitempty" yaml:"captured,omitempty"`
}

Source is the provenance of prose a note did not originate.

It exists so the store can hold a transcript without the transcript pretending to be authored. A note's whole value is that a person stands behind it; a capture's value is the opposite, that nobody does and the source can be re-read instead. Recording which one a file is makes that a fact a reader and `notes verify` can both check, rather than a convention that holds until someone forgets it.

type SourceKind

type SourceKind string

SourceKind names what a captured note is a transcript OF. It is an open vocabulary rather than an enum: the store's job is to record where prose came from, and a reader who meets a kind this binary predates is better served by seeing it than by having it rejected.

const SourceReviewThread SourceKind = "review-thread"

SourceReviewThread is a conversation from a magus review session - the comments a human and any agents left against one changeset.

type Verification

type Verification struct {
	Notes  int     `json:"notes" yaml:"notes"`
	Issues []Issue `json:"issues" yaml:"issues"`
}

Verification is the summary `magus notes verify` reports.

func Verify

func Verify(dir string) (Verification, error)

Verify scans every entry without hiding malformed files or broken links. A missing directory is valid and reports zero entries: declaring a path before writing the first note is a normal state, not a failure.

Jump to

Keyboard shortcuts

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