knowledge

package
v0.9.3 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package knowledge lifts the reusable part of one repository's spec/item corpus into a portable artifact, and condenses several such artifacts into one. It does not paraphrase or generalize prose: an EARS sentence, an ADR's fields, or an intent paragraph travel byte-for-byte as the repository wrote them. Genericity is measured, not inferred — see Merge's recurrence rank.

This package only produces and reads the artifact format. Wiring it to a CLI subcommand or MCP tool, and an "apply to a target repo" step, are deliberately out of scope (a later task), the same way internal/mcpclient landed before its subcommand.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ADRKey added in v0.8.0

func ADRKey(question string) string

contentKey is a knowledge entry's identity, and the single definition of it: the normalized hash of the one field that says what the entry IS — a rule's text, a decision's question, an intent's prose. Everything that compares entries across repositories (FoldInto's dedup, Merge's bucketing, apply's duplicate-decision suppression) compares this, which is what lets the same sentence arriving from two sources, or an LLM-authored generalization and an extracted rule that agree, land in one bucket. It returns "" when the identifying field is empty, i.e. when the entry carries nothing to be identified by. ADRKey is contentKey for a decision, exported for callers that hold a question but no Entry — chiefly a caller asking "has this workspace already decided this?" about a record that has left work.md and survives only as a journal tombstone, which Extract cannot see. It exists so such a caller never re-derives the hash itself: two identity rules for one format is the bug KN-001 records.

func Marshal

func Marshal(a Artifact) ([]byte, error)

Marshal renders an artifact as front-matter-fenced markdown. Output is canonicalized (entries sorted, provenance sorted, sources deduped and sorted) so that Marshal is a pure function of an artifact's content: two calls on equivalent input, however ordered, produce byte-identical output.

func Merge

func Merge(as ...Artifact) (Artifact, []Conflict, error)

Merge condenses N artifacts into one: entries are grouped by identity (Kind, Key), their provenance is unioned, and the count of distinct sources becomes the recurrence rank — the signal this package uses instead of ever rewriting a sentence to look "more generic". The result is sorted by that rank (see sortEntries) so a human curates a ranked list top-down.

Merge is associative in effect and idempotent: Merge(a) reproduces a modulo provenance normalization (sorted, deduped), and Merge(a, a) unions a's own provenance with itself rather than double-counting it as two sources — an artifact that is itself the product of an earlier Merge (and so already carries multi-source entries) folds in correctly, which is what lets Merge be called pairwise or all-at-once interchangeably.

Types

type Artifact

type Artifact struct {
	// Sources lists every repository label (as passed to Extract) reflected
	// somewhere in Entries, sorted. One element for a freshly Extracted
	// artifact, the union of inputs after Merge.
	Sources []string
	Entries []Entry

	// Resolutions records how a Merge Conflict was settled: which identity
	// was in conflict, which value won, and which values lost — see
	// Resolution's doc. Losing values are not deleted, only demoted out of
	// Entries; they remain fully present here. Empty for an artifact that
	// has never had a conflict resolved.
	Resolutions []Resolution
}

Artifact is a portable snapshot of one repository's reusable knowledge (as produced by Extract), or the condensate Merge produces from several. It shares spec.md's format family — YAML front matter plus `##`-headed markdown sections — because a condensate exists to be read and curated by a human before it is applied to other repositories; an opaque encoding would defeat that.

func Apply

func Apply(a Artifact, res Resolution) Artifact

Apply folds a Resolution into an artifact: the winner becomes an ordinary entry (it sorts and marshals exactly like any other Entry — see sortEntries), and the Resolution itself is appended to a.Resolutions so Marshal/Parse carry the losing alternatives alongside it. Apply trusts res was produced by Resolve against a real Conflict; it does not re-derive or re-check that here — Resolve is the validating step, Apply is just bookkeeping. a is not mutated; a new Artifact is returned.

func Extract

func Extract(c *spec.Cascade, items []item.Item, source string) (Artifact, error)

Extract lifts one workspace's reusable knowledge into a portable artifact: every EARS rule and every whitelisted prose section (ears.IsProseSection — intent, notes, design, context; not only `## intent`) in the cascade, and every ADR among items (proposals/tasks/bugs/research describe transient, repository-local state and are never included — including them would drown the condensate in noise no other repository can use).

source is the repository label recorded as provenance on every entry (the caller passes e.g. the module path); it is also the artifact's sole top-level Sources entry.

Extraction does not paraphrase: rule sentences and prose sections travel verbatim. What is stripped is exactly what cannot mean anything outside this repository — rule-ID prefix/number, `applies` node-ID anchors, lifecycle state, file paths — by simply never copying those fields into Entry in the first place.

func FoldInto

func FoldInto(current, incoming Artifact) Artifact

FoldInto computes the additive delta of folding an incoming artifact into a workspace: every entry of incoming whose (Kind, Key) identity is not already present in current — current being the workspace's own artifact, exactly as Extract would produce it right now. This is pure data plumbing over two in-memory Artifacts; no workspace write happens here. The actual write step (internal/mcpserver's knowledge tool, op=apply) calls Extract on the live workspace to build current, calls FoldInto to learn what is missing, and then routes each missing entry through the existing rule/decide composers — never a new write path.

Named FoldInto, not Apply: this package already exports an Apply (merge.go) that folds one conflict Resolution into an Artifact's own Entries/Resolutions — bookkeeping over a single artifact's conflict history, entirely in memory. "Fold an artifact into a live repository's spec/work files" is a different operation in every respect (what it reads, what it's for, who calls it) from "fold a resolved conflict back into an artifact's own entry list" — reusing the Apply name across two such different operations on the same package's public surface would make every call site ambiguous without a qualifying comment. FoldInto keeps the two apart by name alone.

FoldInto is idempotent by construction: given the same current, a second call with the same incoming returns an empty Artifact once the first call's entries have actually been folded into the workspace (so a subsequent Extract of that workspace becomes the new current) — every key incoming carries is by then already present. Duplicate identities within incoming itself (e.g. a hand-assembled or concatenated artifact) also collapse to their first occurrence, so the result never carries the same (Kind, Key) twice even if the input did.

func Parse

func Parse(data []byte) (Artifact, error)

Parse reads an artifact rendered by Marshal. The schema stamp is checked exactly like spec.Load/item.LoadWork: a mismatch is a hard error, there is no migration.

type Conflict

type Conflict struct {
	Kind    EntryKind
	Key     string
	Entries []Entry
}

Conflict is surfaced instead of silently merged, when two or more entries share an identity (same Kind and content Key) but disagree in substance. Today this only arises for ADRs, since a rule's or intent section's Key is derived from its entire payload — identical Key already implies identical content, so there is nothing left to disagree about. An ADR's Key is derived from its Question alone, so two repositories answering the same question differently produce a Conflict here rather than a coin-flip pick.

Entries holds one Entry per distinct answer, each already scoped to only the sources that assert it — a human reads both (or all) sides intact, nothing is synthesized.

type Divergence added in v0.8.0

type Divergence struct {
	Kind EntryKind
	Key  string
	Ours Entry
	Then Entry // the incoming value, not adopted
}

Divergence is one incoming entry whose identity this workspace already carries but whose substance differs — the import disagrees with a position already held.

func Diverging added in v0.8.0

func Diverging(current, incoming Artifact) []Divergence

Diverging reports the incoming entries FoldInto skips for a reason other than agreement. FoldInto is identity-based on purpose (local wins, apply stays additive and idempotent), but that makes "we already say this" and "an import contradicted us and was dropped" the same silent outcome. The distinction is exactly the one Merge draws between agreement and Conflict across artifacts; this draws it between an artifact and the workspace.

It reports; it never resolves. Precedence is unchanged — the caller's job is to make the disagreement visible, not to adopt it.

type Entry

type Entry struct {
	// Kind and Key are carried by the section heading on the wire (see
	// Marshal/Parse), not by YAML body fields — they identify the entry, the
	// payload below is its content.
	Kind EntryKind `yaml:"-"`
	Key  string    `yaml:"-"`

	// Rule payload (Kind == KindRule).
	Text      string `yaml:"text,omitempty"`
	Rationale string `yaml:"rationale,omitempty"`

	// ADR payload (Kind == KindADR). Question is the item's Title. There is
	// no separate "options" field on internal/item.Item — options live as
	// `option:`/`options:` body lines (see item.ParseOptions) — but they are
	// the rejected alternatives an ADR exists to preserve, so they travel
	// here once parsed out of the item's Body.
	Question     string   `yaml:"question,omitempty"`
	Context      string   `yaml:"context,omitempty"`
	Decision     string   `yaml:"decision,omitempty"`
	Consequences string   `yaml:"consequences,omitempty"`
	Status       string   `yaml:"status,omitempty"`
	Options      []string `yaml:"options,omitempty"`

	// Intent payload (Kind == KindIntent): one repository's prose section —
	// any of ears.IsProseSection's whitelist (intent, notes, design,
	// context), not only `## intent` — verbatim.
	Prose string `yaml:"prose,omitempty"`

	// Count is the recurrence rank: the number of distinct repositories
	// backing this entry, pooling Sources and DerivedFrom — both are
	// evidence of how widely a convention holds, whether a repository
	// asserts the entry itself or was one of the inputs an LLM generalized
	// it from. 1 for a freshly Extracted entry; recomputed by Merge (and by
	// NewEntry) as entries combine.
	Count int `yaml:"count"`

	// Sources is the union of provenance rows that literally assert this
	// entry (asserted-by): that repository's own spec/item files contain
	// this text. Sorted by (Source, Dir) for determinism. May be empty for
	// a purely LLM-authored generalization — see DerivedFrom.
	Sources []Provenance `yaml:"sources,omitempty"`

	// DerivedFrom is the union of provenance rows an LLM drew on to
	// generalize this entry (derived-from): those repositories fed the
	// generalization, but none of them necessarily contains this exact
	// text. Disjoint in meaning from Sources — an asserted fact and a
	// generalization drawn from facts are not the same kind of evidence,
	// even though both count toward Count — and kept as a separate field
	// (never merged into Sources) so the distinction survives the wire.
	// Sorted by (Source, Dir) for determinism.
	DerivedFrom []Provenance `yaml:"derived_from,omitempty"`
}

Entry is one portable unit of knowledge: an EARS rule sentence, an ADR, or a prose section from ears.IsProseSection's whitelist — stripped of everything that cannot mean anything outside the repository that minted it: rule-ID prefix/number, `applies` anchors, lifecycle state, file paths. Sentences and prose keep their own words verbatim; this package never paraphrases. An Entry may instead be LLM-authored — a generalization over several repositories rather than a copy of any one of them — but it still never rewrites a sentence itself; see NewEntry.

func NewEntry

func NewEntry(kind EntryKind, payload Entry, assertedBy, derivedFrom []Provenance) (Entry, error)

NewEntry builds and validates an entry the package itself did not lift out of a repository's own files — chiefly an LLM-authored generalization, but the same entry point works for any caller-supplied entry. It gives that entry the same treatment Extract gives an extracted one:

  • the content key is computed here, from payload, the same way Extract computes it (drift.NormHash of Text/Question/Prose depending on kind) — this is what makes an LLM-authored entry merge with an Extracted one that says the same thing. There is no Key parameter: a caller cannot supply one even by mistake, because a caller-chosen key would break dedup.
  • kind is validated against the three known EntryKinds.
  • the payload fields required for that kind must be non-empty after trimming: Text for a rule, Question and Decision for an ADR, Prose for intent. A malformed entry is rejected outright, not silently coerced into something plausible — this package still never paraphrases or invents prose.

Only the fields relevant to kind are read from payload (its Kind, Key, Count, Sources and DerivedFrom are ignored; pass assertedBy/derivedFrom instead). assertedBy is Sources (this repository's files literally contain this text); derivedFrom is DerivedFrom (repositories an LLM drew on to generalize this entry). At least one of the two must be non-empty: an entry backed by no repository at all is not knowledge, it is a hallucination.

type EntryKind

type EntryKind string

EntryKind identifies which of the three portable shapes an Entry carries. Only one of Entry's payload groups is populated, matching Kind.

const (
	KindRule   EntryKind = "rule"
	KindADR    EntryKind = "adr"
	KindIntent EntryKind = "intent"
)

type Provenance

type Provenance struct {
	Source string `yaml:"source"`
	Dir    string `yaml:"dir"`
}

Provenance names one repository (Source) and, within it, the context dir (Dir; "" = root) that stands behind an Entry — either because that repository's own spec/item files literally assert it (Entry.Sources), or because an LLM drew on it while generalizing an entry that has no single asserting repository (Entry.DerivedFrom). Dir travels as a portability hint for the human curating a condensate — it is never part of merge identity.

type Resolution

type Resolution struct {
	Kind   EntryKind
	Key    string
	Winner Entry
	Losers []Entry
}

Resolution records how a Conflict was settled: Winner is the entry the LLM (or a human) chose, and Losers is every other entry the Conflict held — sources and payload intact. Losing values are not deleted: the rejected alternatives are exactly the knowledge this system exists to preserve, so a Resolution keeps them, it does not replace them with a single pick the way a naive "merge conflict" resolution would.

func Resolve

func Resolve(conflict Conflict, winner Entry) (Resolution, error)

Resolve builds a Resolution from a Conflict and the entry chosen as winner. winner must be one of conflict.Entries by value — this checks the caller's decision against the actual conflict rather than trusting it blindly, and is what lets Apply below assume Losers is exactly "the conflict minus the winner". Every other entry in the conflict becomes a Loser, complete and unmodified.

Jump to

Keyboard shortcuts

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