Documentation
¶
Overview ¶
Package embed wires the engram embedder, sidecar format, and staleness detection for the embed-on-write semantic-search pipeline.
Index ¶
- Constants
- Variables
- func BodyText(raw []byte) []byte
- func ContentHash(raw []byte) string
- func Cosine(a, b []float32) float32
- func ExtractBody(raw []byte) []byte
- func MarshalSidecar(s Sidecar) []byte
- func SidecarPath(notePath string) string
- func SituationText(raw []byte) []byte
- type Embedder
- type FS
- type HugotEmbedder
- func NewBundledHugotEmbedder(ctx context.Context, cacheDir string) (*HugotEmbedder, error)
- func NewHugotEmbedderFromDir(ctx context.Context, modelDir, modelID string) (*HugotEmbedder, error)
- func NewHugotEmbedderFromFS(ctx context.Context, modelFS stdembed.FS, modelDir, modelID, cacheDir string) (*HugotEmbedder, error)
- type LazyEmbedder
- type Sidecar
- type State
Constants ¶
const ( // AnsweredByBodyMarker prefixes the machine-written `Answered by: [[…]]` body // line on QA question notes. Same exclusion rationale as VocabBodyMarker. AnsweredByBodyMarker = "Answered by:" // AnswersBodyMarker prefixes the machine-written `Answers: [[…]]` body line on // QA answer notes. Same exclusion rationale as VocabBodyMarker. AnswersBodyMarker = "Answers:" // ContributorsBodyMarker prefixes the machine-written `Contributors: [[…]], …` // body line on QA answer notes. Excluded from BodyText/ContentHash so a // contributors-only write leaves the body vector and hash unchanged. ContributorsBodyMarker = "Contributors:" // RelatedSectionMarker is retained for backward compatibility: unmigrated // vault bodies still carry "Related to:" sections (the ritual was removed // 2026-07-02; bodies are stripped by the vocab migration). Hash exclusion // must keep working for them until that migration lands, then this can go. RelatedSectionMarker = "Related to:" // SupersedesBodyMarker prefixes the machine-written `Supersedes: [[…]] — // type: claim` body lines (replace-whole channel, written by learn/amend). // Excluded from BodyText/ContentHash for the same reason as VocabBodyMarker. // The cli writer's line matching aliases this constant — keep them in sync. SupersedesBodyMarker = "Supersedes:" // VocabBodyMarker prefixes the machine-written `Vocab: [[vocab.…]]` body // line (replace-whole channel, written by WriteVocabAssignment AFTER a // note is embedded). Excluding it from BodyText/ContentHash keeps a // vocab-assigning write from staling the sidecar and keeps [[vocab.…]] // wikilink noise out of body vectors on re-embed. The cli writer's line // matching aliases this constant — keep them in sync. VocabBodyMarker = "Vocab:" )
Exported constants.
const (
BundledModelID = "minilm-l6-v2@384"
)
Exported constants.
const (
SidecarSchemaVersion = 1
)
Exported constants.
Variables ¶
var ( ErrDimsMismatch = errors.New("sidecar dims mismatch len(vector)") ErrSchemaVersion = errors.New("sidecar schema version unsupported") ErrSidecarMalformed = errors.New("sidecar malformed") )
Exported variables.
var ( "bundled model missing or empty — rebuild the binary with the model in place, " + "or set ENGRAM_MODEL_PATH to a directory containing model.onnx", ) ErrHugotEmbedEmpty = errors.New("hugot embed: empty result") ErrHugotProbeEmpty = errors.New("hugot probe returned no embedding") )
Exported variables.
Functions ¶
func BodyText ¶
BodyText returns the note body (frontmatter stripped) with all machine-written channel content removed: `Vocab:` and `Supersedes:` body lines (replace-whole channels) and any trailing "Related to:" section. It is the body-vector source for every note type. Dropping channel content means a channel-only edit (vocab assignment, supersession write, link edit) leaves the body vector and ContentHash unchanged (D3).
Machine lines are stripped BEFORE the Related-to pass: the writers append their lines after an unmigrated note's trailing "Related to:" block, and a non-bullet line after the block would otherwise disqualify it.
Trailing blank lines are normalized to a single newline LAST: the learn renderers end bodies with "\n\n" while the channel writers trim trailing blanks before appending their line — the original count is unrecoverable after a write, so the hash must be insensitive to it on both sides.
func ContentHash ¶
ContentHash returns a sha256: prefixed hex digest covering BOTH embed sources — the situation: field and the body — joined by a 0x00 separator. Hashing both means staleness detection tracks either source: editing a note's situation OR its body changes the hash, marking the stored dual vectors stale.
func Cosine ¶
Cosine returns the cosine similarity of a and b. Returns 0 when either vector has zero magnitude or when lengths differ — callers should treat that as "no signal" rather than a strong match.
func ExtractBody ¶
ExtractBody returns the markdown body of a note with the leading YAML frontmatter block stripped. If the note has no leading frontmatter, it is returned unchanged.
Frontmatter format: a leading "---\n" line, arbitrary lines (which may themselves be blank), and a closing "---\n" line. Anything after the closing delimiter is the body. A single leading blank line after the closing delimiter is also stripped so notes whose frontmatter blocks differ but whose bodies match produce identical hashes.
func MarshalSidecar ¶
MarshalSidecar encodes s as compact JSON. Vectors are large; pretty- printing them wastes disk and noises downstream diffs.
json.Marshal of a Sidecar (a struct of typed-string / int / []float32 / string fields, none of which implement MarshalJSON) cannot fail — the encoder only errors on cyclic data or custom marshaler failures. We swallow the error pointer to avoid the unreachable branch confusing coverage tools.
func SidecarPath ¶
SidecarPath returns the .vec.json path sibling to a note's .md path. Non-.md inputs get .vec.json appended unchanged (defensive).
func SituationText ¶
SituationText returns the `situation:` frontmatter field for any note type ("" when absent or unparseable). It is the situation-vector source.
Types ¶
type Embedder ¶
type Embedder interface {
Embed(ctx context.Context, text string) ([]float32, error)
ModelID() string
Dims() int
}
Embedder produces fixed-dimension dense vectors from text. Implementations are expected to be safe for concurrent use unless documented otherwise.
type FS ¶
FS is the read-only filesystem surface used by ComputeState. The production reader returns *os.PathError which satisfies errors.Is for fs.ErrNotExist; test fakes can hand any error implementing IsNotExist() bool — the interface fallback covers them.
type HugotEmbedder ¶
type HugotEmbedder struct {
// contains filtered or unexported fields
}
HugotEmbedder wraps a Hugot pipeline. Safe for concurrent use — Hugot's pipeline runs the model under its own lock.
func NewBundledHugotEmbedder ¶
func NewBundledHugotEmbedder(ctx context.Context, cacheDir string) (*HugotEmbedder, error)
NewBundledHugotEmbedder is the production constructor: bundled assets FS, fixed model directory, fixed model ID, and a caller-supplied cache dir. The cache dir is the XDG-keyed path where the model is extracted once and reused across all subsequent invocations.
func NewHugotEmbedderFromDir ¶
func NewHugotEmbedderFromDir( ctx context.Context, modelDir, modelID string, ) (*HugotEmbedder, error)
NewHugotEmbedderFromDir constructs an embedder reading the model from a directory on disk. Thin wrapper over buildEmbedder with the production Hugot backend; tests use buildEmbedder directly with a fake backend to exercise every error branch.
func NewHugotEmbedderFromFS ¶
func NewHugotEmbedderFromFS( ctx context.Context, modelFS stdembed.FS, modelDir, modelID, cacheDir string, ) (*HugotEmbedder, error)
NewHugotEmbedderFromFS constructs an embedder from any stdembed.FS rooted at modelDir. cacheDir is the stable directory where the model is extracted once and reused across invocations (XDG-keyed). Tests pass an empty FS to verify UAT 10's clear-error path; production wraps bundled assets.
func (*HugotEmbedder) Close ¶
func (h *HugotEmbedder) Close() error
Close releases the Hugot session. Safe to call multiple times. The model cache dir is NOT removed — it is a shared, persistent cache reused across all engram invocations.
func (*HugotEmbedder) Dims ¶
func (h *HugotEmbedder) Dims() int
Dims reports the embedding dimensionality.
func (*HugotEmbedder) Embed ¶
Embed runs the pipeline on text (truncated to fit the model's context window) and returns the resulting vector.
The char guard assumes prose density; code-dense text can still exceed the model's 512-token positional limit within the char limit (observed: 1500 chars of transcript tokenizing to 538 tokens, panicking graph compilation). On failure the input is halved and retried until it succeeds or bottoms out, so a single dense chunk degrades to a shorter prefix instead of failing the whole ingest.
func (*HugotEmbedder) ModelID ¶
func (h *HugotEmbedder) ModelID() string
ModelID reports the configured model identifier.
type LazyEmbedder ¶
type LazyEmbedder struct {
// contains filtered or unexported fields
}
LazyEmbedder defers construction of an embedder until first use so commands that don't need it (help, update, transcript) don't pay the model-unpack cost or die if model loading fails. The construction is factory-injected so tests can drive both the success and failure init paths without running real Hugot.
func NewLazyEmbedder ¶
func NewLazyEmbedder(cacheDir string) *LazyEmbedder
NewLazyEmbedder returns a wrapper around NewBundledHugotEmbedder that extracts the bundled model to cacheDir at most once (on first Embed / ModelID / Dims call). cacheDir should be the XDG-keyed stable cache path for the model, e.g. $XDG_CACHE_HOME/engram/models/<model_id>/.
func (*LazyEmbedder) Dims ¶
func (l *LazyEmbedder) Dims() int
Dims lazily constructs the embedder, then delegates. Returns 0 when construction failed; callers should detect via an Embed error.
func (*LazyEmbedder) ModelID ¶
func (l *LazyEmbedder) ModelID() string
ModelID lazily constructs the embedder, then delegates. Returns the bundled model id when construction has not been attempted yet so status-style callers can avoid paying the unpack cost.
type Sidecar ¶
type Sidecar struct {
SchemaVersion int `json:"schema_version"`
EmbeddingModelID string `json:"embedding_model_id"`
Dims int `json:"dims"`
SituationVector []float32 `json:"situation_vector"`
BodyVector []float32 `json:"body_vector"`
ContentHash string `json:"content_hash"`
// LastUsed is the date (YYYY-MM-DD) this note last surfaced as a useful
// (above-cutoff) recall hit. Additive metadata: omitempty, EXCLUDED from
// ContentHash (hash.go hashes situation+body of the raw note, not this), and
// it does NOT bump SidecarSchemaVersion — old sidecars decode LastUsed=""
// ("never used"). Never feed LastUsed into any hash: bumping it must not
// mark a note stale.
LastUsed string `json:"last_used,omitempty"` //nolint:tagliatelle // sidecar JSON keys are spec contract
}
Sidecar is the on-disk shape of a per-note .vec.json file. Field order here is the JSON key order. Snake-case keys match the spike spec's sidecar contract verbatim and are part of the on-disk file format: MiniLM-L6-v2@384 is the shipped bundled model, and the 2026-05-24 query spike froze the snake_case sidecar keys as a file format. Each note carries two vectors — one for its situation: frontmatter field and one for its body — so retrieval can match by max(situation, body).
func BuildSidecar ¶
BuildSidecar embeds a note's situation and body and returns a fully stamped dual-vector sidecar. When the note has no situation field, the body text stands in for the situation embedding so every note still carries a meaningful situation vector. Either embed failure is returned to the caller, which applies its own warn-or-fail policy.
func UnmarshalSidecar ¶
UnmarshalSidecar decodes a sidecar from JSON, returning ErrSidecarMalformed on parse failure, ErrSchemaVersion when the on-disk schema is not the current one (e.g. an old single-vector sidecar), or ErrDimsMismatch when either vector's length disagrees with Dims. The schema check precedes the vector-length check so an old sidecar (whose new vector fields decode empty) classifies as a schema mismatch rather than a dims mismatch.
type State ¶
type State int
State is the relationship between a note and its sidecar relative to the current binary's embedder.
func ComputeState ¶
ComputeState reads notePath and the sibling .vec.json and returns the note's State relative to currentModelID. Stale-vs-incompatible precedence: model_id mismatch first (a re-embed under the new model also picks up any body change), then content_hash mismatch.
ComputeState never returns an error: every failure mode classifies as a State (Missing for absent sidecar; Broken for unreadable note, unreadable sidecar, malformed JSON, or dims mismatch). The State IS the report so vault-wide passes can iterate without short-circuiting.