okf

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package okf is the core in-memory model for an OKF bundle. It must not import cobra or any CLI package.

Index

Constants

View Source
const DriftIgnoreRevsFile = ".okf-drift-ignore-revs"

DriftIgnoreRevsFile is the bundle-root file naming the mechanical commit SHAs that opt out of git drift. It mirrors the file `git blame --ignore-revs-file` consumes: one SHA per line, blank lines and #-comments ignored. A checked-in list lets a human declare commit INTENT that git itself cannot read — the day-one bulk migration commit that would otherwise collapse a corpus's real authoring history into a single date.

View Source
const SpecVersion = "0.2"

SpecVersion is the OKF spec version this build targets.

View Source
const StatusStable = "stable"

StatusStable is the default lifecycle status when `status` is absent (§5.4).

View Source
const TargetOkfVersion = "0.2"

TargetOkfVersion is the version this migrator upgrades a bundle TO. It is a migrate-local constant, deliberately independent of SpecVersion (the build's pinned version): closing the SpecVersion gap is separate, tracked work.

Variables

View Source
var DefaultSkipDirs = map[string]bool{
	".git":          true,
	".hg":           true,
	".svn":          true,
	".okfctl":       true,
	"node_modules":  true,
	".venv":         true,
	"venv":          true,
	"env":           true,
	"__pycache__":   true,
	".mypy_cache":   true,
	".pytest_cache": true,
	".tox":          true,
	".ruff_cache":   true,
	"site-packages": true,
	"vendor":        true,
	"target":        true,
	"dist":          true,
	"build":         true,
	".next":         true,
	".cache":        true,
	".idea":         true,
	".vscode":       true,
}

DefaultSkipDirs is the base-name skip list applied to the bundle walk. These are vendored (third-party, checked-in or installed) and derived (build/tool output) directories whose .md files nobody authored as knowledge: a Python virtualenv or a build-output tree sitting under the bundle root would otherwise become part of the graph. The set is matched by directory BASE NAME at any depth, so tool/.venv and web/node_modules are both skipped. It is a default, not a policy: --no-ignore (WithNoIgnore) restores the full walk, and the skip is never silent (Bundle.SkippedDirs records what was pruned so the caller can announce it). The bundle root itself is never skipped even if its own base name matches.

This deliberately does NOT consult .gitignore (couples curation scope to version-control scope, two different questions) and is a built-in default rather than a required .okfctlignore (the tool must be usable on a real tree with no config); a project-level ignore file composes cleanly on top later.

View Source
var LifecycleStatuses = map[string]bool{
	"draft":      true,
	"stable":     true,
	"deprecated": true,
}

LifecycleStatuses is the §5.4 status lifecycle enum: the only values `status` may hold. Absent ⇒ stable. This is a spec-DEFINED closed set, unlike the open `type` vocabulary (§7.4) or the unknown `epistemic` key (§11): status is named and enumerated by §5.4, so a value outside this set is a genuine defect. It drives lint's status-lifecycle check (soft guidance), never a validate floor rejection — §11 forbids rejecting a bundle for an optional field.

View Source
var ReservedFiles = map[string]bool{"index.md": true, "log.md": true}

ReservedFiles are the reserved base names that are not concept nodes. A file named index.md or log.md is reserved at ANY depth: a knowledge base commonly keeps a per-neighborhood index.md and log.md (e.g. wine/index.md, security/auth/log.md), and those are structural files (front door / append-only change history), not concepts. Recognition is by base name, so use isReservedPath to test a bundle-relative path.

Functions

func AppendLog

func AppendLog(root, message string) error

AppendLog prepends a timestamped entry to log.md (newest-first), creating the file with a heading when absent. A multi-line message is flattened to its first line to keep the log well-formed; an empty message is rejected.

func ApplyMove

func ApplyMove(root string, b *Bundle, old, new string, rewrites []LinkRewrite) error

ApplyMove performs the file move old->new on disk and applies the planned inbound-link rewrites. It is the single writer for a move. root is the bundle root; b is the loaded bundle the plan was computed against.

func Clone

func Clone(url, dir string) error

Clone runs `git clone <url> <dir>`, materializing a remote bundle source into a local directory. It returns an error that includes git's own stderr so a failure (bad url, unreachable host, auth) is actionable.

func GitAvailable

func GitAvailable() bool

GitAvailable reports whether the git binary is on PATH.

func GitLastCommitDate

func GitLastCommitDate(root, relPath string) (time.Time, bool, error)

GitLastCommitDate returns the committer date of the most recent commit that touched the bundle-relative file relPath, resolving git from within root.

The three return values distinguish "no answer" from "error": ok=false with a nil error means git could not answer (git binary absent, root is not a git repo, or the file is untracked / has no commits) — a normal, non-fatal state that callers degrade on. A non-nil error is a genuine failure (git present and invoked but failing for an unexpected reason). This keeps the drift check working the same way as `index check`: git is a source of truth when present, and silently unavailable when not, never a crash.

func GitLastCommitDateIgnoring added in v0.2.0

func GitLastCommitDateIgnoring(root, relPath string, ignore map[string]bool) (time.Time, string, bool, error)

GitLastCommitDateIgnoring returns the committer date AND SHA of the most recent commit that touched relPath whose SHA is NOT in the ignore set, resolving git from within root. It is the drift-comparison primitive that lets a bulk mechanical commit opt out: when the file's last-touching commit is listed in `.okf-drift-ignore-revs` (loaded via LoadDriftIgnoreRevs), the comparison walks back to the prior real commit instead of collapsing the node's authoring history into the migration date. This mirrors the established `git blame --ignore-revs-file` convention.

An ignore entry matches a commit when it equals the full 40-char SHA or is a prefix of it (>= 7 chars), so an abbreviated SHA in the file still opts the commit out — the same spelling tolerance git blame gives.

The four return values distinguish "no answer" from "error": ok=false with a nil error means git could not answer (git binary absent, root is not a git repo, the file is untracked, or EVERY commit that touched it is ignored) — all normal, non-fatal states callers degrade on. A non-nil error is a genuine failure. When ignore is nil or empty this behaves exactly like a `git log -1`.

func IndexDirs

func IndexDirs(b *Bundle) []string

IndexDirs returns the sorted bundle-relative directories that should carry an index.md (OKF §8: an index MAY appear in any directory and enumerates that directory's contents). A directory qualifies when it directly holds a concept node OR is an ancestor of a directory that does — exactly the set a reader traverses for progressive disclosure. The bundle root ("") is always included when the bundle has any concept node. Empty directories, and directories whose entire subtree has no concept, are excluded. This is the single source of truth shared by WriteIndex (build) and IndexInSync (check).

func IndexInSync

func IndexInSync(b *Bundle) (bool, string)

IndexInSync reports whether the on-disk nested index tree matches what WriteIndex would generate for the current bundle. It is in sync only when EVERY content-bearing directory has an index.md equal to its RenderDirIndex output AND no orphaned generated index.md exists in a directory that should carry none. A missing, stale, or orphaned index counts as out of sync, and the report names the first offending path.

func IsGitWorkTree

func IsGitWorkTree(dir string) bool

IsGitWorkTree reports whether dir is inside a git work tree. It degrades to false (never an error) when git is absent or dir is not a repo, mirroring the "git as an optional source of truth" discipline in gitmeta.go.

func IsReservedPath

func IsReservedPath(rel string) bool

IsReservedPath reports whether a bundle-relative slash path names a reserved file (index.md or log.md) at any depth.

func LoadDriftIgnoreRevs added in v0.2.0

func LoadDriftIgnoreRevs(root string) (map[string]bool, error)

LoadDriftIgnoreRevs reads DriftIgnoreRevsFile from the bundle root and returns the set of commit SHAs to opt out of drift comparison. The file is OPTIONAL: its absence yields an empty (non-nil) set and no error — the common case.

Format (identical to git's ignore-revs file so users already understand it):

  • one SHA per line
  • blank lines ignored
  • a line beginning with '#' is a comment, ignored
  • an inline '#' comment after a SHA is stripped
  • surrounding whitespace trimmed

SHAs are lower-cased so matching is case-insensitive against git's %H output.

func MigrateApply added in v0.2.0

func MigrateApply(root string, b *Bundle, plan MigratePlan) error

MigrateApply applies a plan's deterministic edits to disk, order-preserving and additive-only. root is the bundle root; b is the loaded bundle the plan was computed against. It writes each edited node in place, then bumps the version markers (.okf sidecar and bundle-root index.md). It never touches a judgment item — those stay for the plan's consumer.

func NewNode

func NewNode(root, relPath, typ, title string) (string, error)

NewNode creates a conformant concept node at relPath (bundle-relative) with a required non-empty type (§7.2). It refuses an empty type and refuses to overwrite an existing file. Frontmatter is YAML-marshaled (never concatenated) so a type/title containing newlines or YAML metacharacters is safely quoted and cannot inject additional frontmatter keys. Returns the absolute path written.

NewNode is the no-template path: it delegates to NewNodeFromTemplate with an empty template, so both paths share one containment/marshal/write mechanism.

func NewNodeFromTemplate

func NewNodeFromTemplate(root, relPath, typ, title string, t Template) (string, error)

NewNodeFromTemplate creates a node conformant to both the spec floor and a governing type template (PRD §9.3). Beyond the required type + title, it stubs the template's required fields (with a "TODO" placeholder so the node starts free of template drift) and recommended fields (empty), and lays down its body_sections as empty `## ` headings. An empty Template scaffolds nothing — that is the plain NewNode path. Existing-file, containment, and empty-type refusals apply. Returns the absolute path written.

func ParseFrontmatter

func ParseFrontmatter(src []byte) (map[string]any, string, error)

ParseFrontmatter splits a source file into its YAML frontmatter map and the Markdown body. Missing frontmatter yields an empty (non-nil) map and no error; malformed YAML frontmatter is an error.

func PlanRemoveOrphans

func PlanRemoveOrphans(b *Bundle, path string) ([]string, error)

PlanRemoveOrphans returns the bundle-relative nodes that become orphaned (zero inbound links) as a direct consequence of removing path. It is pure.

func PromotableIndexes added in v0.2.0

func PromotableIndexes(b *Bundle) []string

PromotableIndexes returns the sorted bundle-relative paths of every NON-ROOT index.md that carries a non-empty frontmatter block — the exact shape validateReserved flags as "index files contain no frontmatter (§8)". The bundle-root index.md is excluded (its §12 okf_version carve-out is legal), and a non-root index with no frontmatter is already conformant and excluded. A non-root index whose frontmatter failed to parse (Frontmatter == nil) is a different failure class and is not promotable — promote does not guess at broken YAML.

func PromoteApply added in v0.2.0

func PromoteApply(root string, b *Bundle, changes []PromoteChange) error

PromoteApply performs each planned promotion on disk: it applies the inbound link rewrites, writes the promoted directory-concept index into its new sibling concept file with the body preserved VERBATIM and `created` immutable, and removes the old index.md (a clean, frontmatter-free index is regenerated by WriteIndex at the command layer). root is the bundle root; b is the loaded bundle the plan was computed against. It is the single writer for a promotion.

func PullFastForward

func PullFastForward(dir string) error

PullFastForward runs `git -C <dir> pull --ff-only`, updating an existing checkout without ever rewriting local history. A divergence (local commits that are not on the remote) fails rather than merging.

func ReadLog

func ReadLog(root string) (string, error)

ReadLog returns the log.md body (empty string if the file is absent).

func RefreshApply

func RefreshApply(changes []RefreshChange) error

RefreshApply writes each planned change to disk, rewriting only the frontmatter `modified` field via the order- and body-preserving writer. `created` is never touched. It is safe to call with an empty plan (no-op).

func RenderDirIndex

func RenderDirIndex(b *Bundle, dir string) string

RenderDirIndex produces the deterministic index.md body for one directory of the bundle (dir is bundle-relative slash form; "" is the bundle root), per OKF §8: it enumerates ONLY that directory's own immediate contents — its content-bearing child directories (linked dir-relatively as `child/`) under a "Subdirectories" section, and the concept nodes living directly in it (linked by base name) under a "Concepts" section, each carrying the linked concept's description from frontmatter. Links are relative to dir itself, never bundle-relative. Only the bundle-root index carries frontmatter (the §12 okf_version carve-out); every nested index carries none. Output is byte-stable (all ordering via sort) and passes Validate.

func RenderIndex

func RenderIndex(b *Bundle) string

RenderIndex renders the bundle-ROOT index. It is RenderDirIndex(b, "") — kept as a named helper so existing call sites and tests that mean "the root index" stay expressive.

func Scaffold

func Scaffold(dir string) error

Scaffold writes a minimal conformant bundle into dir: a reserved index.md and log.md and an .okf spec pin. The result passes Validate with zero findings (it has no concept nodes yet, so the type floor is vacuously satisfied).

The scaffolded index.md carries NO frontmatter (OKF §8). The bundle's okf_version is pinned by the .okf sidecar; `okfctl index build` surfaces it as the sole permitted index frontmatter key (§12) once the index is regenerated.

func TemplateScaffold

func TemplateScaffold(t Template) (fields []string, sections []string)

TemplateScaffold returns the required fields to stub as empty frontmatter keys and the body sections to lay down as empty `## ` headings for a node created from t (PRD §9.3). recommended_fields are stubbed alongside required ones.

func Templates

func Templates(b *Bundle) map[string]Template

Templates folds every `type: Type Template` node in the bundle into a map keyed by the target_type it governs. A bundle should not ship two templates for one target_type; if it does, the lexicographically-last path wins (stable, since paths are visited in sorted order).

func TouchModifiedFile

func TouchModifiedFile(abs string, at time.Time) error

TouchModifiedFile refreshes the frontmatter `modified` field of the node at abs to `at` (RFC3339 UTC), writing the file back in place. It is order- and body-preserving: the frontmatter block is round-tripped through a yaml.Node so existing keys keep their order and the Markdown body is preserved verbatim. `created` is never rewritten (only modified is touched); a node without a `modified` key gains one appended to the end of its frontmatter, and one without frontmatter at all gains a minimal block. It never fabricates created.

This is the single, order-preserving writer for a timestamp refresh — it edits the frontmatter block only, so it cannot drop the body the way rewriting a parsed sub-region over the whole file would.

func WriteIndex

func WriteIndex(b *Bundle) error

WriteIndex regenerates one index.md per content-bearing directory (OKF §8), from the current bundle. It is the single writer for the reserved index (both `index build` and the automatic create/edit/delete/rename maintenance call it) so the two paths cannot diverge on how indexes are produced. Directories are created as needed; a directory that already holds concept files always exists, but IndexDirs may include an ancestor that is only implied by a deeper node.

WriteIndex also self-heals the tree: an index.md left behind in a directory that is no longer content-bearing (e.g. after a node moved or was removed out of it) is pruned, so a subsequent `index check` is clean. This is the stale parent/sibling index class the pre-§8 flat model left behind.

Types

type AccuracyClaim added in v0.3.0

type AccuracyClaim struct {
	Claim             string `json:"claim"`
	ExpectedGrounding string `json:"expected_grounding"` // filled by the judge
}

AccuracyClaim is one factual claim extracted from a node body, paired with an empty grounding slot for a human or LLM judge to fill in: does the node's cited source actually support this claim? okfctl extracts the claim; it never judges the grounding.

type Actor added in v0.2.0

type Actor string

Actor is a provenance actor: generated.by / verified[].by (§7). Its recorded form is one of `<producer>/<version>`, `human:<id>`, or `process:<id>`.

func (Actor) IsHuman added in v0.2.0

func (a Actor) IsHuman() bool

IsHuman reports whether the actor is a human per the §7 `human:` prefix — the key trust classification keys off (§5.3).

type AlignmentCheck added in v0.3.0

type AlignmentCheck struct {
	Question string `json:"question"` // title + description: what the node set out to answer
	Answered string `json:"answered"` // filled by the judge: yes/no/partial + why
}

AlignmentCheck asks whether the node answers the question it set out to. The question is the node's title/description; Answered is filled by the judge.

type AnalyzeNodeRef

type AnalyzeNodeRef struct {
	Path string `json:"path"`
}

AnalyzeNodeRef is a bare reference to a node by path (used where a finding carries no extra fields).

type AnalyzeOptions

type AnalyzeOptions struct {
	// StaleDays: age (days) past which a node's freshness basis date is stale.
	StaleDays int
	// TimeSensitiveFraction: a time-sensitive node surfaces once its age is
	// >= TimeSensitiveFraction * StaleDays; undated marked nodes always surface.
	TimeSensitiveFraction float64
	// ThinLines: body line count (blank lines excluded) below which a node is
	// "thin".
	ThinLines int
	// ClusterMin: minimum nodes sharing a tag to flag a synthesis cluster.
	ClusterMin int
	// CoverageThreshold is passed through to Lint's coverage-gap check.
	CoverageThreshold int
}

AnalyzeOptions configures the proactive curation report. Zero values are filled with defaults by DefaultAnalyzeOptions / the command layer.

func DefaultAnalyzeOptions

func DefaultAnalyzeOptions() AnalyzeOptions

DefaultAnalyzeOptions returns the report defaults, mirroring the reference okf_analyze.py: 180-day staleness, 0.5 time-sensitive fraction, 15-line thin threshold, 3-node cluster minimum.

type AnalyzeReport

type AnalyzeReport struct {
	Summary      AnalyzeSummary     `json:"summary"`
	Coverage     CoverageReport     `json:"coverage_gaps"`
	Epistemic    EpistemicReport    `json:"epistemic"`
	Freshness    FreshnessReport    `json:"freshness"`
	Connectivity ConnectivityReport `json:"connectivity"`
	Clusters     []ClusterFinding   `json:"clusters"`
	Structure    StructureReport    `json:"structure"`
}

AnalyzeReport is the structured curation report across the five dimensions. It is JSON-serializable for the machine path (the curation sweep files research cards from it) and consumed by the human renderer.

func Analyze

func Analyze(b *Bundle, opts AnalyzeOptions) AnalyzeReport

Analyze runs the five-dimension proactive curation report over a loaded bundle. It is READ-ONLY, pure (aside from the package clock for freshness), and deterministic (all output ordered by sort). It NEVER mutates the bundle and NEVER fails on findings — the caller decides exit semantics (report, not gate: exit 0 on a successful analysis regardless of finding count).

type AnalyzeSummary

type AnalyzeSummary struct {
	Nodes              int `json:"nodes"`
	TotalInternalLinks int `json:"total_internal_links"`
	StaleThresholdDays int `json:"stale_threshold_days"`
}

AnalyzeSummary carries corpus-level counts and the thresholds in effect.

type Bundle

type Bundle struct {
	Root       string
	Nodes      map[string]*Node // concept nodes only (excludes reserved)
	Reserved   map[string]*Node // index.md, log.md
	OkfVersion string           // okf_version from the bundle's .okf, or SpecVersion if absent
	// SkippedDirs holds the bundle-relative slash paths of directories pruned
	// from the walk by the default skip list (see DefaultSkipDirs), sorted.
	// Empty when WithNoIgnore was passed or nothing matched. The CLI announces
	// these on stderr so an excluded subtree is never a silent omission.
	SkippedDirs []string
	// contains filtered or unexported fields
}

Bundle is a loaded OKF bundle: concept nodes keyed by bundle-relative path, plus the reserved files, plus the derived link graph.

func Load

func Load(root string, opts ...LoadOption) (*Bundle, error)

Load walks root, parses every .md file, and builds the in-memory graph.

By default the walk prunes vendored and derived directories (DefaultSkipDirs) so content nobody authored as knowledge never enters the graph; pass WithNoIgnore to restore the full walk. Applying the skip once here means every consumer (lint, analyze, validate, search, graph, index) inherits identical scope — divergent per-command scope would be its own bug class.

func (b *Bundle) OutboundLinks(path string) []string

OutboundLinks returns the in-bundle nodes that path links to.

type CalibrationCheck added in v0.3.0

type CalibrationCheck struct {
	CurrentGrade   string `json:"current_grade"`    // the grade being re-checked
	HoldsOnRecheck string `json:"holds_on_recheck"` // filled by the judge: yes/no + note
}

CalibrationCheck measures whether a node's grade holds up on re-check, so a periodic sample can tell whether the "VERIFIED" rate is calibrated. CurrentGrade is what okfctl records today; HoldsOnRecheck is filled by the judge.

type ClusterFinding

type ClusterFinding struct {
	Tag   string   `json:"tag"`
	Nodes []string `json:"nodes"`
}

ClusterFinding is a tag shared by >= ClusterMin nodes with no synthesis node.

type ConnectivityReport

type ConnectivityReport struct {
	Orphans      []AnalyzeNodeRef `json:"orphans"`
	WeaklyLinked []WeaklyLinked   `json:"weakly_linked"`
}

ConnectivityReport groups the connectivity findings.

type CoverageReport

type CoverageReport struct {
	DanglingLinks  []DanglingLink   `json:"dangling_links"`
	ThinNodes      []ThinNode       `json:"thin_nodes"`
	Uncited        []AnalyzeNodeRef `json:"uncited"`
	SingleCitation []AnalyzeNodeRef `json:"single_citation"`
	KnownGaps      []string         `json:"known_gaps"`
}

CoverageReport groups the coverage / gap findings.

type DanglingLink struct {
	From   string `json:"from"`
	Target string `json:"target"`
}

DanglingLink is a body link whose .md target resolves to no node.

type DriftFinding

type DriftFinding struct {
	Path       string
	TargetType string
	Message    string
}

DriftFinding is a single template-overlay violation (PRD §9.4). It is a warning-class finding, never a spec-floor failure.

func TemplateDrift

func TemplateDrift(b *Bundle) []DriftFinding

TemplateDrift reports where nodes diverge from the template governing their type (PRD §9.4): a required field missing/empty, or a body_section heading absent. recommended_fields are advisory and never reported here. A node whose type has no governing template never drifts (unknown types are fine, §7.4). Output is deterministic (sorted by node path, then finding order).

type DuplicateGroup

type DuplicateGroup struct {
	Members []string `json:"members"`
}

DuplicateGroup is a set of node paths whose titles fold to one key.

type EpistemicCount added in v0.3.0

type EpistemicCount struct {
	Value string `json:"value"`
	Count int    `json:"count"`
}

EpistemicCount is one observed epistemic value and how many nodes carry it.

type EpistemicReport added in v0.3.0

type EpistemicReport struct {
	Distribution []EpistemicCount `json:"distribution"`
	Untagged     int              `json:"untagged"`
}

EpistemicReport surfaces the observed distribution of the `epistemic` grade key (§11 unknown key). It is OBSERVATIONAL, not a gate: analyze recognizes the key and reports whatever values appear so a curator can spot an outlier or typo, but okfctl never enum-checks or rejects a value (over-conformance on an unknown key is a spec violation). Untagged counts nodes with no epistemic key.

type EvalFinding added in v0.3.0

type EvalFinding struct {
	Check   string `json:"check"`
	Path    string `json:"path"`
	Message string `json:"message"`
}

EvalFinding is one TACA-Transparency observation about a node's provenance. Like a LintFinding it is curation guidance, never a spec-floor failure: eval is advisory by default and only gates under --strict. The four checks: "grade-missing" | "grade-vocabulary" | "uncited" | "citation-unresolved".

func EvalTransparency added in v0.3.0

func EvalTransparency(b *Bundle, opts EvalOptions) []EvalFinding

EvalTransparency runs the deterministic, stdlib-only, offline TACA-Transparency checks over a bundle and returns findings sorted by (path, check). It never mutates the bundle and never touches the network — external http(s) citations are deliberately out of scope (that is the eval-sample / human pass, see verifying-citation-link-fit). This is the only TACA dimension okfctl can honestly automate; Accuracy/Alignment/Calibration are scaffolded by EvalSample instead.

type EvalOptions added in v0.3.0

type EvalOptions struct {
	// GradeVocabularyFloor is the minimum number of nodes that must carry a
	// given epistemic/authority value for that value to count as part of the
	// corpus vocabulary. A value carried by fewer nodes is reported as a
	// vocabulary outlier (a likely typo/drift). Zero means the default.
	//
	// Calibrated against the real cwest/knowledge-base corpus. As measured on
	// 2026-08-09 (corpus HEAD 895693b, 246 nodes) the legitimate epistemic
	// vocabulary bottoms out at "draft"/"active" (2 nodes each) and authority at
	// "SYNTHESIS" (2 nodes); the genuine drift lives at count 1 ("authority:
	// high", "authority: DEPRECATED"). A floor of 2 therefore isolates exactly
	// the count-1 outliers without false-positiving a small-but-real grade. See
	// docs/specs/2026-08-07-taca-eval.md.
	GradeVocabularyFloor int
}

EvalOptions configures the Transparency gate. Zero values take the defaults.

type Finding

type Finding struct {
	Path    string
	Message string
}

Finding is a single spec-floor violation. Path is bundle-relative.

func DriftFindings

func DriftFindings(b *Bundle) []Finding

DriftFindings reports concept nodes whose frontmatter `modified` contradicts the file's git last-commit date — the tool noticing when the hand-maintained field has gone stale (or been bumped ahead of reality). It is READ-ONLY: it never rewrites a node (following the `index check` precedent — report, and let a write command fix it).

It degrades cleanly: outside a git repo, when git is unavailable, or for an untracked file, there is no source of truth to compare against and no finding is produced. A node without a `modified` field cannot contradict anything and is skipped. Findings are returned sorted by path for deterministic output.

func Validate

func Validate(b *Bundle) []Finding

Validate enforces the OKF spec floor (PRD §6.2, §7.1):

  • frontmatter must be parseable (nil frontmatter == parse failure);
  • every concept node has a non-empty `type` (§7 rule 2);
  • reserved index.md files carry no frontmatter, with the single §12 carve-out for an okf_version-only block on the bundle-root index.

It never enforces a taxonomy of type VALUES (§7.4): unknown types pass. It returns findings; an empty slice means the bundle passes the floor.

The index-frontmatter rule closes the loop: okfctl generates index.md, so its own validator must reject an index that violates §8/§12 — otherwise a generator regression (e.g. re-introducing `type: Index`) passes validation unnoticed, which is the exact defect this floor exists to catch.

type FreshnessReport

type FreshnessReport struct {
	Stale         []StaleNode         `json:"stale"`
	TimeSensitive []TimeSensitiveNode `json:"time_sensitive"`
}

FreshnessReport groups the freshness findings.

type GeneratedEdit added in v0.2.0

type GeneratedEdit struct {
	By string `json:"by"`
	At string `json:"at"`
}

GeneratedEdit is the planned §13.1 `timestamp` → `generated { by, at }` rename for one node. At is the legacy timestamp value carried over verbatim; By is the supplied/inferred actor (§7).

type Generation added in v0.2.0

type Generation struct {
	By Actor
	At time.Time
}

Generation records how the current content was produced (§5.2): who (an actor) and when (last meaningful change).

type Graph

type Graph struct {
	Nodes []GraphNode `json:"nodes"`
	Edges []GraphEdge `json:"edges"`
}

Graph is a serializable view of a bundle's concept-node link graph. Reserved files (index.md/log.md) are not graph nodes, but their outbound links confer inbound reachability for orphan detection (consistent with lint).

func BuildGraph

func BuildGraph(b *Bundle) Graph

BuildGraph derives the serializable graph from a loaded bundle. Nodes are sorted by path; edges by (from, to). The orphan flag reuses inboundCounts — the same inbound source of truth lint uses — so graph and lint can never disagree about what is orphaned.

type GraphEdge

type GraphEdge struct {
	From string `json:"from"`
	To   string `json:"to"`
}

GraphEdge is a resolved in-bundle link from one concept node to another.

type GraphNode

type GraphNode struct {
	Path         string `json:"path"`
	Title        string `json:"title"`
	Type         string `json:"type"`
	Neighborhood string `json:"neighborhood"`
	Orphan       bool   `json:"orphan"`
}

GraphNode is a single concept node in the graph.

type JudgmentItem added in v0.2.0

type JudgmentItem struct {
	Path    string       `json:"path"`
	Kind    JudgmentKind `json:"kind"`
	Context string       `json:"context"`
}

JudgmentItem is one thing the migrator refuses to guess. Path is the node it belongs to; Context is verbatim material (the citation line, the timestamp value) the consumer needs to resolve it.

type JudgmentKind added in v0.2.0

type JudgmentKind string

JudgmentKind classifies why an item could not be migrated deterministically.

const (
	// JudgmentProseCitation: a `# Citations` item with no follow-able resource
	// (§5.1 requires `resource`). The consumer must supply a resource, reclassify
	// it (e.g. to §5.2 `verified`), or drop it.
	JudgmentProseCitation JudgmentKind = "prose-citation"
	// JudgmentMissingActor: a `timestamp` rename with no actor supplied and none
	// inferable (§7). The consumer must supply `generated.by`.
	JudgmentMissingActor JudgmentKind = "missing-actor"
)

type LinkRewrite

type LinkRewrite struct {
	NodePath string // bundle-relative path of the node whose body is edited
	Old      string // exact link target text being replaced (URL + optional title)
	New      string // replacement target text (same relative form as Old)
}

LinkRewrite is a single planned edit to a node's body: replace the link target text Old with New, preserving the author's relative link form.

func PlanMove

func PlanMove(b *Bundle, old, new string) ([]LinkRewrite, error)

PlanMove computes the inbound-link rewrites needed to move old->new. It is pure (no disk access) and preserves each author's relative link form: a link that resolved root-relative stays root-relative; a link that resolved dir-relative is recomputed relative to the linking node's directory.

type LintFinding

type LintFinding struct {
	Check   string `json:"check"` // "orphan" | "missing-xref" | "coverage-gap" | "type-hygiene" | "broken-link" | "status-lifecycle" | "spec-version"
	Path    string `json:"path"`  // node path the finding is about ("" for bundle-level findings)
	Message string `json:"message"`
}

LintFinding is one judgment-worthy observation about bundle health. Unlike a validate Finding (a spec-floor violation), a lint finding is curation guidance — never a format failure.

func Lint

func Lint(b *Bundle, opts LintOptions) []LintFinding

Lint runs the deterministic, stdlib-only structural checks over a bundle and returns findings sorted by path then check. It never mutates the bundle.

func LintSemantic

func LintSemantic(b *Bundle, idx SemanticIndex, opts SemanticOptions) []LintFinding

LintSemantic runs the similarity-driven curation checks the PRD (§8.6) calls for: pairs that read alike but carry no edge, and nodes with no semantically close kin at all. It is the semantic counterpart to the structural checks in Lint — structural asks "is anything linked to this?", semantic asks "is anything even about the same thing?".

idx supplies the neighbor sets; a node absent from idx is reported once as index drift rather than silently skipped, so a partial answer never reads as a complete one. Findings are sorted by path then check, so the same inputs always produce byte-identical output.

type LintOptions

type LintOptions struct {
	// CoverageThreshold is the number of distinct nodes that must mention a
	// term (with no node of its own) before it is reported as a coverage gap.
	// Zero means the default (3).
	CoverageThreshold int
}

LintOptions configures the deterministic structural checks.

type LoadOption added in v0.2.0

type LoadOption func(*loadConfig)

LoadOption configures Load. The zero set of options is the default behavior: the bundle walk skips vendored/derived directories (DefaultSkipDirs).

func WithNoIgnore added in v0.2.0

func WithNoIgnore() LoadOption

WithNoIgnore restores the full walk: no directory is skipped, so the loaded graph is byte-identical to the pre-skip-list behavior. It is the escape hatch for a bundle that deliberately authored real content into a directory whose name happens to match the skip list.

type MigratePlan added in v0.2.0

type MigratePlan struct {
	TargetVersion string          `json:"target_version"`
	Nodes         []NodeMigration `json:"nodes"`
	Judgment      []JudgmentItem  `json:"judgment"`
}

MigratePlan is the full, JSON-serializable migration plan: the deterministic per-node edits, the enumerated judgment items, and the target version. It is the review surface — for Casey and for a solo user alike.

func PlanMigration added in v0.2.0

func PlanMigration(b *Bundle, generatedBy string) (MigratePlan, error)

PlanMigration computes the v0.1 → v0.2 migration plan for a loaded bundle. It is PURE: it reads the bundle and writes nothing. generatedBy is the actor (§7) used for every `timestamp` rename; when empty and no actor is inferable, each such node becomes a JudgmentMissingActor item instead of a guessed rename.

type Neighbor

type Neighbor struct {
	Path  string
	Score float64
}

Neighbor is one ranked semantic neighbor of a node: the neighbor's path and its cosine similarity to the subject node.

type NeighborResult

type NeighborResult struct {
	Path         string `json:"path"`
	Title        string `json:"title"`
	Type         string `json:"type"`
	Neighborhood string `json:"neighborhood"`
	Depth        int    `json:"depth"`
}

NeighborResult is one node reached by graph-structural traversal from a start node, along with its hop distance (the start node is depth 0 and is excluded from results).

func Neighborhood

func Neighborhood(b *Bundle, start string, depth int) ([]NeighborResult, bool)

Neighborhood returns the nodes within depth hops of start in the bundle's concept-node link graph, treating edges as UNDIRECTED: a node is a neighbor whether it links to start or start links to it (a reader traverses both ways). The start node itself is excluded. depth < 1 is treated as 1. Results are sorted by (depth, path) so the closest neighbors come first, deterministically. An unknown start path returns (nil, false).

type Node

type Node struct {
	Path        string         // bundle-relative, e.g. "wine/tannin.md"
	Frontmatter map[string]any // parsed YAML frontmatter
	Body        string         // markdown after the frontmatter
}

Node is a single OKF concept: its bundle-relative path is its identity, its frontmatter carries typed metadata (type is required, §7), and body holds the Markdown after the frontmatter block.

func (*Node) Epistemic added in v0.3.0

func (n *Node) Epistemic() (string, bool)

Epistemic returns the node's `epistemic` grade value, and whether the key is present. `epistemic` is NOT an OKF-defined field: it is an unknown frontmatter key (§11) the corpus carries to preserve the pre-v0.2 conflated grade verbatim (migration 13.1). okfctl RECOGNIZES it — surfacing its value distribution in analyze — but never enum-gates it, because §11 forbids rejecting a bundle for an unknown key or its values. A non-string or empty value reports present=true with the value rendered as text, so a typo is surfaced rather than dropped.

func (*Node) Generated added in v0.2.0

func (n *Node) Generated() (Generation, bool)

Generated returns how the content was produced (§5.2). §13.1 fallback: when `generated` is absent, fall back to the legacy `timestamp` for `.At` (with an empty By — v0.1 recorded no author). ok is false when neither yields a usable date.

func (*Node) IsStale added in v0.2.0

func (n *Node) IsStale(today time.Time) bool

IsStale reports whether the node is stale as of `today` (§5.5): stale when today >= stale_after. A node with no (or unparseable) stale_after is never stale.

func (*Node) SourceCitations added in v0.2.0

func (n *Node) SourceCitations() int

SourceCitations returns how many provenance entries a node carries (§13.1 fallback). It reads frontmatter `sources` first; for a v0.1 document with no `sources`, it falls back to counting the legacy body `# Citations` list.

func (*Node) Sources added in v0.2.0

func (n *Node) Sources() []Source

Sources returns the parsed `sources` list (§5.1). Entries missing the REQUIRED `resource` are dropped — the reader surfaces only well-formed sources. The shared `usage_window` sibling is framed onto every entry; an entry MAY carry its own `usage_window` to override it. Returns an empty slice when `sources` is absent.

func (*Node) StaleAfter added in v0.2.0

func (n *Node) StaleAfter() (time.Time, bool)

StaleAfter returns the absolute stale-after date (§5.5, YYYY-MM-DD). ok is false when the field is absent or unparseable.

func (*Node) Status added in v0.2.0

func (n *Node) Status() string

Status returns the lifecycle status (§5.4). Absent ⇒ stable.

func (*Node) Tags added in v0.2.0

func (n *Node) Tags() []string

Tags returns the node's frontmatter tags (§4.1), nil when absent. Tags are an optional YAML list; a scalar tag normalizes to a one-element list and non-string scalars coerce to their string form. This is the exported accessor for callers outside the package (e.g. semantic-search filters) that need a node's tags without reaching into frontmatter directly.

func (*Node) TrustTier added in v0.2.0

func (n *Node) TrustTier() TrustTier

TrustTier derives the trust tier from `verified` (§5.3): no verified ⇒ unverified; non-human actors only ⇒ machine-confirmed; any human:<id> ⇒ human-reviewed. Derived, never stored.

func (*Node) Type

func (n *Node) Type() string

Type returns the node's type value ("" if absent or not a string).

func (*Node) Verified added in v0.2.0

func (n *Node) Verified() []Verification

Verified returns the verification events (§5.2). §11 MUST: a BARE MAPPING is treated as a one-element list. Returns an empty slice when `verified` is absent.

type NodeEvalScaffold added in v0.3.0

type NodeEvalScaffold struct {
	Path        string           `json:"path"`
	Title       string           `json:"title"`
	Description string           `json:"description"`
	Epistemic   string           `json:"epistemic"`
	Authority   string           `json:"authority"`
	TrustTier   string           `json:"trust_tier"`
	Sources     []string         `json:"sources"`
	Accuracy    []AccuracyClaim  `json:"accuracy"`
	Alignment   AlignmentCheck   `json:"alignment"`
	Calibration CalibrationCheck `json:"calibration"`
}

NodeEvalScaffold is the per-node eval-set entry the sampler emits for the three un-automatable TACA dimensions. okfctl pre-populates every field it can extract (path, title, description, grades, sources, candidate claims) and leaves the judgment slots empty. It computes NO truth verdict.

func EvalSample added in v0.3.0

func EvalSample(b *Bundle, opts SampleOptions) []NodeEvalScaffold

EvalSample selects a spot-check sample of nodes and returns an eval-set scaffold per node for the Accuracy/Alignment/Calibration dimensions that okfctl cannot automate. It is deterministic: an explicit Paths set is honored verbatim (sorted); otherwise a seeded pseudo-random Count sample is drawn reproducibly.

type NodeMigration added in v0.2.0

type NodeMigration struct {
	Path      string         `json:"path"`
	Generated *GeneratedEdit `json:"generated,omitempty"`
	Sources   []SourceEdit   `json:"sources,omitempty"`
}

NodeMigration is the set of deterministic frontmatter edits planned for one node. A nil Generated means no timestamp rename is planned; an empty Sources means no citation became a source. Only nodes with at least one edit appear.

type PromoteChange added in v0.2.0

type PromoteChange struct {
	OldPath  string // bundle-relative, e.g. "gke-pm-map/index.md"
	NewPath  string // bundle-relative, e.g. "gke-pm-map/gke-pm-map.md"
	Rewrites []LinkRewrite
}

PromoteChange is a single planned promotion of one directory-as-concept index.md into a sibling concept file, together with the inbound-link rewrites that keep every reference to it resolving after the move. OldPath is the non-root index.md that carries frontmatter; NewPath is the sibling concept file it becomes (dir/<basename>.md). Rewrites are the edits to OTHER files' bodies whose links pointed at the old directory-concept in either directory spelling.

func PromotePlan added in v0.2.0

func PromotePlan(b *Bundle, basename string) ([]PromoteChange, error)

PromotePlan computes the promotion of every directory-as-concept index into a sibling concept file, plus the inbound-link rewrites needed to keep references resolving. basename is the concept file's base name for every promoted node ("" means default to the directory's own base name). It is PURE: it reads the loaded bundle and writes nothing to disk.

A destination that already exists as a concept node is a hard error — promote never overwrites authored content.

type RefreshChange

type RefreshChange struct {
	Path        string // bundle-relative, e.g. "wine/tannin.md"
	AbsPath     string // absolute on-disk path to rewrite
	OldModified string // current modified, "2006-01-02"
	NewModified string // target modified, RFC3339 (e.g. "2026-07-20T00:00:00Z")
	Commit      string // SHA of the git commit the drift comparison used
}

RefreshChange is a single planned timestamp correction: the drift finding's remediation. AbsPath is the file to rewrite; OldModified is the current frontmatter date (bare-date form, for display); NewModified is the value the refresh will write — the git last-commit calendar day stamped in the corpus's RFC3339-at-midnight-UTC form, so it both resolves the drift and matches the bare-date convention the corpus already uses (minimising the git diff).

func RefreshPlan

func RefreshPlan(b *Bundle) []RefreshChange

RefreshPlan is the read-only remediation companion of DriftFindings: it reports the timestamp correction the refresh would make for every drifting node, in path order. It writes nothing. Outside a git repo the plan is empty (no source of truth, nothing to fix).

func RefreshPlanNode

func RefreshPlanNode(b *Bundle, relPath string) ([]RefreshChange, error)

RefreshPlanNode narrows RefreshPlan to a single bundle-relative path. An honest (non-drifting) node yields an empty plan — a clean no-op, not an error. A path that is not a node in the bundle is a real caller error.

type RefreshGuardResult added in v0.2.0

type RefreshGuardResult struct {
	Triggered bool   // the plan is large and dominated by one commit
	Commit    string // the dominant commit's SHA (empty when not triggered)
	Count     int    // how many of the plan's changes that commit accounts for
	Total     int    // total changes in the plan
}

RefreshGuardResult reports whether a refresh plan looks like the remediation of a bulk mechanical commit, and the evidence for it.

func RefreshGuard added in v0.2.0

func RefreshGuard(plan []RefreshChange) RefreshGuardResult

RefreshGuard inspects a refresh plan for the bulk-mechanical-commit signature: a large plan in which a single commit accounts for an implausible share of the changes. Such a plan is not an incremental cleanup — running it would flatten every distinct authoring date the commit touched into the migration date. The caller uses the result to refuse (or require explicit confirmation) and to point the user at `.okf-drift-ignore-revs`, which resolves the case without destroying data. RefreshGuard is pure: it reads the plan and writes nothing.

type SampleOptions added in v0.3.0

type SampleOptions struct {
	Paths []string // explicit node paths to scaffold (wins over Count/Seed)
	Count int      // size of a random sample when Paths is empty
	Seed  int64    // seed for reproducible sampling (0 ⇒ a fixed default seed)
}

SampleOptions selects which nodes the spot-check scaffold covers.

Precedence: an explicit Paths set wins (used by the --changed-since curation hook, resolved to node paths in the cmd layer). Otherwise a deterministic pseudo-random Count sample seeded by Seed is drawn. Count <= 0 with no Paths yields no scaffolds.

type SearchField

type SearchField string

SearchField names a lexical match surface. A lexical query with no field restriction matches ANY of these; a field-restricted query matches only the named one. This is the core-search surface of PRD §6.3: title, tag, type, and content substring, all case-insensitive.

const (
	// FieldAny matches title, tag, type, or body substring (the default).
	FieldAny SearchField = ""
	// FieldTitle matches only the node title (frontmatter title, or file base).
	FieldTitle SearchField = "title"
	// FieldTag matches only a node's frontmatter tags.
	FieldTag SearchField = "tag"
	// FieldType matches only the node's type value.
	FieldType SearchField = "type"
	// FieldBody matches only the node's body substring.
	FieldBody SearchField = "body"
)

type SearchResult

type SearchResult struct {
	Path         string   `json:"path"`
	Title        string   `json:"title"`
	Type         string   `json:"type"`
	Neighborhood string   `json:"neighborhood"`
	MatchedOn    []string `json:"matched_on"`
}

SearchResult is one lexical hit: the matched node plus the fields the query matched on (sorted, deduped), so a caller can explain WHY a node matched.

func Search(b *Bundle, query string, field SearchField) []SearchResult

Search runs a case-insensitive lexical query over a bundle's concept nodes. Reserved files (index.md/log.md) are never search results. An empty query returns no results (a lexical search needs a term). Results are sorted by path for deterministic output. field restricts the match surface; FieldAny searches title, tag, type, and body substring together.

type SemanticIndex

type SemanticIndex map[string][]Neighbor

SemanticIndex maps a node path to its ranked neighbors (self excluded). It is deliberately a plain data shape rather than a search-package type: the checks below are pure functions over similarity scores, so internal/okf stays free of any dependency on the index format or the embedder that produced it. The caller (cmd) reads the real index and adapts it into this shape.

type SemanticOptions

type SemanticOptions struct {
	// SimilarityThreshold is the cosine score at or above which two UNLINKED
	// nodes are reported as a possible missing link. Zero means the default.
	SimilarityThreshold float64
	// IsolationFloor is the score a node's BEST neighbor must reach for the node
	// to count as semantically connected. Zero means the default.
	IsolationFloor float64
}

SemanticOptions tunes the similarity-driven checks.

type SlugPair

type SlugPair struct {
	A string `json:"a"`
	B string `json:"b"`
}

SlugPair is two node paths whose base names are within edit-distance 1.

type Source added in v0.2.0

type Source struct {
	ID           string
	Resource     string
	Title        string
	Author       string       // actor (§7); an authority signal
	UsageCount   *int         // adoption/liveness signal; nil when absent
	LastModified string       // YYYY-MM-DD; recency signal
	UsageWindow  *UsageWindow // in-effect window (shared or per-entry override)
}

Source is one parsed `sources` entry (§5.1). Resource is REQUIRED within an entry; the other fields are optional credibility signals. UsageWindow is the window in effect for this entry: the shared sibling window framed onto it, or the entry's own override when present.

type SourceEdit added in v0.2.0

type SourceEdit struct {
	Resource string `json:"resource"`
}

SourceEdit is one planned §5.1 `sources` entry derived from a body citation. Resource is REQUIRED and preserves the author's form (absolute URL, or a bundle-relative path kept relative per §6).

type StaleNode

type StaleNode struct {
	Path    string `json:"path"`
	AgeDays *int   `json:"age_days"`
	Basis   string `json:"basis"` // the date string compared, or "(none)"
}

StaleNode is a node whose freshness basis date is older than the threshold, or that carries no basis date at all (AgeDays nil, Basis "(none)").

type StructureReport

type StructureReport struct {
	DuplicateTitles    []DuplicateGroup `json:"duplicate_titles"`
	NearDuplicateSlugs []SlugPair       `json:"near_duplicate_slugs"`
}

StructureReport groups the structural findings.

type Template

type Template struct {
	TargetType        string
	RequiredFields    []string
	RecommendedFields []string
	BodySections      []string
	Path              string // bundle-relative path of the template node
}

Template is a parsed type-template node (PRD §9.2). It governs nodes whose type equals TargetType.

type ThinNode

type ThinNode struct {
	Path      string `json:"path"`
	BodyLines int    `json:"body_lines"`
}

ThinNode is a node whose body is below the thin-lines threshold.

type TimeSensitiveNode

type TimeSensitiveNode struct {
	Path    string   `json:"path"`
	AgeDays *int     `json:"age_days"`
	Markers []string `json:"markers"`
}

TimeSensitiveNode is a marked node aged past the time-sensitive gate.

type TrustTier added in v0.2.0

type TrustTier string

TrustTier is the derived trust level (§5.3), lowest to highest. It is DERIVED, never stored.

const (
	TrustUnverified       TrustTier = "unverified"        // §5.3: no verified key
	TrustMachineConfirmed TrustTier = "machine-confirmed" // §5.3: non-human actors only
	TrustHumanReviewed    TrustTier = "human-reviewed"    // §5.3: any human:<id> verifier
)

type UsageWindow added in v0.2.0

type UsageWindow struct {
	From string
	To   string
}

UsageWindow is the { from, to } date range that frames a usage_count (§5.1).

type Verification added in v0.2.0

type Verification struct {
	By Actor
	At time.Time
}

Verification is one verification event (§5.2): an actor and an instant.

type WeaklyLinked

type WeaklyLinked struct {
	Path string `json:"path"`
	In   int    `json:"in"`
	Out  int    `json:"out"`
}

WeaklyLinked is a node with exactly one total in-bundle link.

Jump to

Keyboard shortcuts

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