corpus

package
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package corpus loads a topic's documents (sources and wiki articles) and provides everything decisions need to reason about them without a model: body hashes, value hashes, BM25 retrieval, the alias dictionary and mention scan, section-aware excerpts, provenance state, the `.decisions/state.jsonl` store and the byte-preserving owned-key writer.

Index

Constants

View Source
const (
	StatusWritten        = "written"
	StatusUnchanged      = "unchanged"
	StatusSkippedLocked  = "skipped:locked"
	StatusSkippedChanged = "skipped:changed"
	// StatusSkippedUserKey is the run-summary label for WriteResult.SkippedUserKeys.
	StatusSkippedUserKey = "skipped:user-key"
)

Write statuses reported in WriteResult.Status.

View Source
const (

	// HeadChars is the number of body characters indexed by DefaultFields.
	HeadChars = 2000
)

BM25 parameters.

View Source
const StateFile = resolve.DecisionsDir + "/state.jsonl"

StateFile is the topic-relative path of the document state store.

Variables

This section is empty.

Functions

func BodyHash

func BodyHash(body string) string

BodyHash is the sha256 hex digest of body after normalizing `\r\n` to `\n`.

func DefaultFields

func DefaultFields(d *Document) map[string]string

DefaultFields extracts the standard retrieval fields of a document: title, aliases, criterion, summary, entities, questions and head (the first HeadChars characters of the body with wikilinks flattened).

func DefaultWeights

func DefaultWeights() map[string]float64

DefaultWeights are the field weights used with DefaultFields.

func DocumentState

func DocumentState(d *Document, maxTokens int, terms []string) map[string]any

DocumentState is the single state builder every decision purpose uses for a document (spec §4.4): `{"title", "excerpt", "provenance"}`, with the excerpt bounded by maxTokens and biased toward sections matching terms (see Excerpt).

func EstimateTokens

func EstimateTokens(text string) int

EstimateTokens estimates the token count of text as runes/4, rounded up. It is a deliberately cheap, provider-independent approximation used only to bound state size.

func Excerpt

func Excerpt(body string, maxTokens int, terms []string) string

Excerpt returns a section-aware excerpt of body within about maxTokens tokens (EstimateTokens). A body that fits is returned unchanged. Otherwise the body is split into sections at ATX headings (outside fenced code) and kept in document order by priority: the head (the text before the first heading plus the first section), then every section containing one of terms (case-insensitive), then the remaining sections, while the budget allows. Each run of omitted sections is replaced by `[... N sections omitted ...]`; a head section larger than the whole budget is cut and ends with `[... truncated ...]`. The tail is never dropped without a marker.

func Head(body string, maxChars int) string

Head flattens wikilinks (`[[a|b]]` → `b`, `[[a]]` → `a`) and returns at most maxChars runes of the trimmed result.

func IsMergedListKey

func IsMergedListKey(key string) bool

IsMergedListKey reports whether kb merges new items into a user-owned value of key instead of leaving it alone (`aliases` and the relation lists).

func ListPaths

func ListPaths(paths []string, limit int) string

ListPaths renders document paths for a run summary: at most limit of them, comma-separated, then "(+N more)" when some were left out.

func MatchGlob

func MatchGlob(pattern, p string) bool

MatchGlob reports whether the slash path p matches pattern. Matching is segment-aware: `*`, `?` and `[...]` match within one path segment (as in path.Match) and a `**` segment matches zero or more whole segments, so `raw/audit-*/**` matches every file below any `raw/audit-...` folder. Leading `./` and surrounding slashes are ignored on both sides; a malformed pattern matches nothing.

func Tokenize

func Tokenize(text string) []string

Tokenize lowercases text, splits it on every rune that is not a Unicode letter or digit, drops stopwords and keeps tokens of at least 2 runes.

func Unclassified

func Unclassified(doc *Document, row *StateRow, contract string, banks map[string]string) bool

Unclassified reports whether doc needs a (re)classification: it has no state row, or any bank in banks was recorded with another version, judged on another body, or judged under another contract hash (when contract is non-empty). Writes by other commands never refresh these per-bank records, so a body edit followed by `kb link` still leaves doc unclassified.

func ValidGlob

func ValidGlob(pattern string) bool

ValidGlob reports whether every segment of pattern is well formed.

func ValueHash

func ValueHash(v any) string

ValueHash is the sha256 hex digest of the canonical JSON of the normalized value: strings are trimmed, lists keep their order ([]any of strings hashes like []string), numbers encode as JSON numbers (int 2 and float 2.0 hash alike), maps encode with sorted keys and times as their frontmatter text (`2006-01-02` at midnight, RFC 3339 otherwise). A value written by kb and the same value parsed back from YAML therefore hash identically.

Types

type BM25

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

BM25 is an in-memory BM25F index over documents: each named field has its own length normalization and a weight applied to its term frequency.

func NewBM25

func NewBM25(docs []*Document, fields func(*Document) map[string]string, weights map[string]float64) *BM25

NewBM25 indexes docs. fields extracts the text of every named field of a document; weights gives each field's weight (fields missing from weights weigh 1). Parameters: k1 = 1.2, b = 0.75.

func (*BM25) Search

func (index *BM25) Search(query string, k int) []Hit

Search returns the k best-scoring documents for query (all matches when k <= 0), highest score first, ties by path. Documents that match no query term are not returned.

type Corpus

type Corpus struct {
	// Root is the topic root directory.
	Root string
	// Skipped lists documents skipped because they could not be read or
	// their frontmatter did not parse.
	Skipped []Skip
	// contains filtered or unexported fields
}

Corpus is the loaded set of topic documents.

func Load

func Load(topicRoot string, opts LoadOptions) (*Corpus, error)

Load reads the topic at topicRoot. Sources are `raw/**/*.md` except `raw/_quarantine/**`, `raw/codebase/**` and documents whose `source_kind` starts with `codebase-`; articles are `wiki/concepts/**/*.md`. Hub files (`log.md`, `CLAUDE.md`, `AGENTS.md`, `wiki/index/**`, `outputs/**`, `.decisions/**`, `bases/**`) fall outside both trees and dot directories are never walked, so neither is ever loaded. A document whose frontmatter does not parse is skipped and recorded in Corpus.Skipped; it never fails the load.

func (*Corpus) Articles

func (c *Corpus) Articles() []*Document

Articles returns the article documents sorted by path.

func (*Corpus) ByBodyHash

func (c *Corpus) ByBodyHash(h string) []*Document

ByBodyHash returns the documents whose body hash is h, sorted by path.

func (*Corpus) ByPath

func (c *Corpus) ByPath(p string) *Document

ByPath returns the document at the topic-relative path p (with or without `.md`), or nil.

func (*Corpus) Documents

func (c *Corpus) Documents() []*Document

Documents returns every loaded document sorted by path.

func (*Corpus) Sources

func (c *Corpus) Sources() []*Document

Sources returns the source documents sorted by path.

type Dictionary

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

Dictionary is the alias dictionary of a topic's articles: each article's title and `aliases`.

func NewDictionary

func NewDictionary(articles []*Document) *Dictionary

NewDictionary builds the dictionary over articles. Terms shorter than 3 runes are skipped unless they are all-caps acronyms of at least 2 runes; such short acronyms match case-sensitively, every other term case-insensitively.

func (*Dictionary) Mentions

func (d *Dictionary) Mentions(text string) []Mention

Mentions scans text for dictionary terms at word boundaries, skipping leading frontmatter, fenced and inline code, heading lines, URLs, and existing wikilinks and markdown links. At each position the longest term wins; overlapping shorter terms are not reported. Mentions are returned in text order, one per article per occurrence.

type Document

type Document struct {
	// Path is the topic-relative slash path (with `.md`).
	Path string
	// AbsPath is the absolute filesystem path.
	AbsPath string
	// Title is the frontmatter title, else the first `# ` heading, else the
	// file stem.
	Title string
	Kind  Kind
	// Frontmatter holds the parsed frontmatter values.
	Frontmatter map[string]any
	// Raw is the full file text as read.
	Raw string
	// Body is the text after the frontmatter.
	Body string
	// BodyHash is BodyHash(Body).
	BodyHash string
	// Aliases are the Obsidian `aliases` values.
	Aliases []string
	ModTime time.Time
}

Document is one loaded topic document.

func ReadDocument

func ReadDocument(topicRoot, relative string, kind Kind) (*Document, error)

ReadDocument loads one topic document from disk. relative is the topic-relative path.

func (*Document) Concepts

func (d *Document) Concepts() []string

Concepts returns the normalized wikilink targets of `concepts`.

func (*Document) Criterion

func (d *Document) Criterion() string

Criterion returns the `criterion` key.

func (*Document) Depth

func (d *Document) Depth() (float64, bool)

Depth returns the numeric `depth` key.

func (*Document) Entities

func (d *Document) Entities() []string

Entities returns the `entities` list.

func (*Document) Genre

func (d *Document) Genre() string

Genre returns the `genre` key.

func (*Document) Locked

func (d *Document) Locked() bool

Locked reports whether the user set `locked: true`.

func (*Document) Provenance

func (d *Document) Provenance() map[string]any

Provenance returns the document's provenance state (spec §4.4): `path` (topic-relative), `source_kind`, `source_host` (host of `source_url`, lowercased, `www.` stripped), `ingest_batch` and `ingest_query`. Empty fields are omitted, never guessed.

func (*Document) Questions

func (d *Document) Questions() []string

Questions returns the `questions` list.

func (*Document) Relevance

func (d *Document) Relevance() string

Relevance returns the `relevance` key.

func (*Document) SourceKind

func (d *Document) SourceKind() string

SourceKind returns the `source_kind` key.

func (*Document) SourceURL

func (d *Document) SourceURL() string

SourceURL returns the `source_url` key.

func (*Document) Summary

func (d *Document) Summary() string

Summary returns the `summary` key.

func (*Document) Triage

func (d *Document) Triage() string

Triage returns the `triage` key.

type Hit

type Hit struct {
	Doc   *Document
	Score float64
}

Hit is one BM25 search result.

type Kind

type Kind string

Kind classifies a corpus document.

const (
	KindSource  Kind = "source"
	KindArticle Kind = "article"
)

Document kinds.

type LoadOptions

type LoadOptions struct {
	// Exclude lists topic-relative globs (see MatchGlob) of documents to skip.
	Exclude []string
}

LoadOptions configures Load.

type Mention

type Mention struct {
	Article *Document
	// Term is the dictionary term (title or alias) that matched.
	Term string
	// Start and End are byte offsets of the matched text in the scanned text.
	Start int
	End   int
	// Sentence is the sentence around the match, trimmed and bounded to about
	// 300 characters.
	Sentence string
}

Mention is one occurrence of an article title or alias in a body.

type Skip

type Skip struct {
	Path   string
	Reason string
}

Skip records a document that could not be loaded.

type StateMeta

type StateMeta struct {
	// Contract is the selection contract hash ("" keeps the row's value).
	Contract string
	// Banks maps question bank ids to versions; merged into the row. Each
	// listed bank is also recorded as judged on the document's body (the one
	// the write started from) under the contract.
	Banks map[string]string
	// Facts are per-document outcomes merged into the row; an empty value
	// deletes the fact.
	Facts map[string]string
}

StateMeta is the classification context recorded in the state row.

type StateRow

type StateRow struct {
	Path         string            `json:"path"`
	BodyHash     string            `json:"body_hash"`
	Contract     string            `json:"contract"`
	Banks        map[string]string `json:"banks"`
	Written      map[string]string `json:"written"`
	BankBody     map[string]string `json:"bank_body,omitempty"`
	BankContract map[string]string `json:"bank_contract,omitempty"`
	WrittenBody  map[string]string `json:"written_body,omitempty"`
	Facts        map[string]string `json:"facts,omitempty"`
	Updated      string            `json:"updated"`
}

StateRow is one row of `<topic>/.decisions/state.jsonl` (spec §6): the bookkeeping kb keeps per document instead of writing it to frontmatter.

BodyHash and Contract describe the document at kb's last write (rename detection follows BodyHash). Freshness is tracked per judgment instead: a write by one command (link, review, ingest) must not make another command's answers look current. BankBody and BankContract record, per question bank, the body and contract its answers were computed on; WrittenBody records, per owned key, the body its value was derived from; Facts holds small per-document outcomes other runs aggregate (for example the primary concept). Rows written before these maps existed read as if every bank and key was judged on BodyHash under Contract.

func (*StateRow) JudgedBody

func (row *StateRow) JudgedBody(bank string) string

JudgedBody returns the body hash the answers of bank were computed on (BodyHash for rows that predate per-bank tracking).

func (*StateRow) JudgedContract

func (row *StateRow) JudgedContract(bank string) string

JudgedContract returns the contract hash bank was judged under (Contract for rows that predate per-bank tracking).

func (*StateRow) KeyBody

func (row *StateRow) KeyBody(key string) string

KeyBody returns the body hash the value of the owned key was derived from (BodyHash for rows that predate per-key tracking).

type StateStore

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

StateStore is the append-only document state store. The last row per path wins. It is safe for concurrent use.

func OpenState

func OpenState(topicRoot string) (*StateStore, error)

OpenState reads `<topicRoot>/.decisions/state.jsonl`. A missing file is an empty store. A torn last line (a crash mid-append) is ignored; any other malformed line is an error.

func (*StateStore) Compact

func (s *StateStore) Compact() error

Compact atomically rewrites the state file with only the latest row per path, sorted by path.

func (*StateStore) FindByBodyHash

func (s *StateStore) FindByBodyHash(h string) []StateRow

FindByBodyHash returns the latest rows whose body hash is h, sorted by path.

func (*StateStore) Get

func (s *StateStore) Get(path string) (*StateRow, bool)

Get returns the latest row for the topic-relative path.

func (*StateStore) Lookup

func (s *StateStore) Lookup(path, bodyHash string, exists func(path string) bool) (*StateRow, bool)

Lookup returns the row of path. When path has no row it performs rename detection: the first row with the same body hash whose own path no longer exists (exists reports false) follows the file and is returned with Path set to path. The store itself is not modified.

func (*StateStore) Path

func (s *StateStore) Path() string

Path returns the absolute path of the state file.

func (*StateStore) Put

func (s *StateStore) Put(row StateRow) error

Put appends row as one whole JSON line and makes it the latest row for its path.

func (*StateStore) Rows

func (s *StateStore) Rows() []StateRow

Rows returns the latest row of every path, sorted by path.

type WriteResult

type WriteResult struct {
	// Written lists the keys whose value changed on disk, sorted.
	Written []string
	// SkippedUserKeys lists keys left alone because they belong to the user
	// (present and not last written by kb), sorted.
	SkippedUserKeys []string
	// BodyWritten reports whether ApplyBody replaced the body.
	BodyWritten bool
	// Status is StatusWritten, StatusUnchanged, StatusSkippedLocked or
	// StatusSkippedChanged.
	Status string
}

WriteResult reports what Apply or ApplyBody did.

type Writer

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

Writer applies kb-owned frontmatter keys (spec §6) to topic documents, byte-preserving everything else, and records each write in the state store. It is safe for concurrent use; writes to one document are serialized.

func NewWriter

func NewWriter(topicRoot string, store *StateStore, now func() time.Time) *Writer

NewWriter returns a writer for the topic at topicRoot. now defaults to time.Now.

func (*Writer) Apply

func (w *Writer) Apply(doc *Document, updates map[string]any, meta StateMeta) (WriteResult, error)

Apply writes updates (owned key → value; a nil value deletes the key) to doc. Rules, in order:

  • any non-owned key is refused with an error;
  • a document with `locked: true` is left alone (skipped:locked);
  • the file is re-read: if its body changed since doc was loaded nothing is written (skipped:changed); other non-owned edits are fine because only owned key blocks are replaced in the fresh content;
  • a key is written only if it is absent or its current value hash equals the state row's written hash (kb wrote it and nobody edited it since); otherwise it belongs to the user and is reported in SkippedUserKeys. `aliases` is merged instead (case-insensitive dedupe, never removes), even when user-owned, and so is a user-owned relation list (`related`, `extends`, `prerequisite`, `example_of`, `contradicts`, `affects`, `supersedes`): new targets are appended, deduplicated by normalized link target, and existing items are never removed or reordered;
  • unchanged values are not rewritten;
  • the file is replaced atomically (temp file + rename, mode kept), and only then the state row is appended with the new body hash, contract, merged banks and merged written hashes. The banks of meta, and every key that now holds the requested value, are recorded as derived from the body the write started from; judgments recorded earlier keep their own body hash, so a write never makes another command's answers look current. A body replacement that only adds or changes wikilink markup (link insertion) carries every judgment made on the old body over to the new one. If the file changed between the read and the rename, the whole step is retried once, then reported as skipped:changed.

On success doc is refreshed in place with the written content.

func (*Writer) ApplyBody

func (w *Writer) ApplyBody(doc *Document, newBody string, updates map[string]any, meta StateMeta) (WriteResult, error)

ApplyBody is Apply plus a body replacement (link insertion, recapture). The body is replaced only while the file's body hash still equals doc.BodyHash; the same key rules apply to updates.

Jump to

Keyboard shortcuts

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