kb

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: May 29, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package kb is shared infrastructure for ox's knowledge-base git repos: the per-project ledger and team-context clones. They have the same shape — a managed git working tree the daemon pulls and the CLI pushes to — and the same multi-writer hazards: server-side seed, CLI-side seed, several coworkers committing in parallel.

Anything that's true for both repo types belongs here. Things that are only true for one of them stay in their own package (internal/ledger/* for ledger-specific paths and lifecycle; the team-context layer in cmd/ox + internal/teamdocs for TC-specific behavior).

Index

Constants

View Source
const (
	KeySessionRecordingMode = "features.session_recording.mode"
	KeyMurmursEnabled       = "features.murmurs.enabled"
	KeyVisibility           = "features.visibility"
)

KeySessionRecordingMode is the dotted key for the session recording mode.

View Source
const (
	LayerScopeEnv     = "env"
	LayerScopeKB      = "kb"
	LayerScopeUser    = "user"
	LayerScopeDefault = "default"
)

Layer scope labels — short, stable, used by --show-origin and JSON output.

View Source
const (
	ScopeExclusive = "exclusive"
	ScopeSubtree   = "subtree"
)

Scope values. Only ScopeExclusive is honored in v1; ScopeSubtree is reserved and rejected at resolve time (see ErrSubtreeOverridesNotSupported).

Variables

View Source
var ErrSubtreeOverridesNotSupported = errors.New("subtree overrides are reserved in ADR-017 v1; nested .sageox/ markers are not supported")

ErrSubtreeOverridesNotSupported is returned when the resolver finds a .sageox/ marker that is itself nested inside another .sageox/-rooted tree. ADR-017 §6 reserves the design space for subtree overrides but v1 rejects them rather than ship undefined semantics around session state straddling a boundary.

KnownKeys is the whitelist of keys `ox kb config get/list` accepts in v1.

View Source
var MergeUnionPaths = []string{
	"AGENTS.md",
	"CLAUDE.md",
	"README.md",
	"CONVENTIONS.md",
	"SOUL.md",
	".gitignore",
}

MergeUnionPaths lists the root metadata files where multiple writers (server-side seed, CLI seed, coworker edits, doctor) routinely touch the same file concurrently in BOTH ledger and team-context repos. Declaring merge=union for these tells git to concatenate both sides on conflict instead of halting the rebase, which turns "wedged repo after first push" into "repo keeps moving; duplicate lines can be deduped later by a doctor pass."

merge=union is the same driver git uses for changelogs and NEWS files. It is safe for append-mostly metadata. It is NOT applied to session, history, murmur, or memory entry paths — those are conflict-free by construction since each entry has a unique timestamp- or id-based path.

Files included:

  • AGENTS.md, CLAUDE.md, README.md, CONVENTIONS.md — appear at the root of both repo types; touched by humans, AI coworkers, server seed, ox prime injection.
  • SOUL.md — team-context "soul" doc; multi-coworker writes are the point of the file.
  • .gitignore — both repo types may have ignore patterns added by either side.

Exported so external tooling and tests can introspect the canonical list without re-deriving it.

Functions

func EnsureMergeAttributes

func EnsureMergeAttributes(repoPath string) (changed bool, err error)

EnsureMergeAttributes writes or updates the ox-managed merge-driver block in the KB repo's per-clone attributes file (.git/info/attributes). Idempotent. Preserves any content outside the managed block. Returns true if the file changed, false if it was already up to date.

The repo must already be a git working tree (have a .git directory). Caller passes the working-tree root, not the .git directory.

Designed to be safe to call on every pull/push cycle: idempotent, atomic write (temp + rename), best-effort error handling.

func IsKnownKey added in v0.9.0

func IsKnownKey(key string) bool

IsKnownKey reports whether key is in the v1 whitelist.

func NormalizeSlugArg added in v0.9.0

func NormalizeSlugArg(s string) string

NormalizeSlugArg strips a leading "#" from a user-supplied slug argument. Use on input received from CLI argv before resolving the slug. "#marketing" -> "marketing"; "marketing" -> "marketing"; "" -> "".

Only one leading "#" is stripped — "##weird" becomes "#weird". That is intentional: a double-prefix is almost certainly a typo we'd rather surface as a "kb not found" error than silently coerce away.

func ResolveCurrentKBIDAndType added in v0.9.0

func ResolveCurrentKBIDAndType(cwd string) (kbID, kbType string)

ResolveCurrentKBIDAndType returns (kbID, kbType) for the current cwd, or ("", "") if no current KB. Convenience wrapper for callers that just want to pass these two values to config.ResolveSessionRecording.

Errors are intentionally swallowed: callers in this position (session-recording resolution, hook banners, doctor) always have a well-defined fallback when there is no binding, so a resolver failure should degrade to "no KB" rather than propagate up.

Types

type Bubble added in v0.8.0

type Bubble struct {
	// KBID is the immutable kb identifier from /api/v1/kb. Empty for
	// legacy rows that haven't been migrated into the kb table yet.
	KBID string

	// Type is the kb_type bucket. Legacy team rows synthesize KBTypeTeam;
	// legacy ledger rows synthesize KBTypeRepo. Unknown server values are
	// already collapsed to KBTypeUnknown by the kb client.
	Type api.KBType

	// Slug is the human-readable slug (kebab-case). Used as the secondary
	// dedup key together with Endpoint when neither KBID nor RepoID matches.
	Slug string

	// Name is the display name.
	Name string

	// ViewerRole is the caller's role on this bubble ("owner", "member",
	// "viewer"). Only kb-API rows populate this today.
	ViewerRole string

	// LocalPath is the on-disk checkout path. For kb-API rows this is the
	// canonical KBDir(kb_id); for legacy rows it's whatever path the
	// synthesizer reports (team context dir or ledger dir).
	LocalPath string

	// RepoURL is the git clone URL when known.
	RepoURL string

	// RepoID is the SageOx repo_id, populated for legacy ledger rows and
	// for kb-API rows when the server supplies it. Used as the secondary
	// dedup key.
	RepoID string

	// Endpoint is the SageOx API endpoint this row belongs to (normalized
	// via endpoint.NormalizeEndpoint). Used together with Slug as the
	// tertiary dedup key.
	Endpoint string

	// Source identifies which fan-out branch produced this row.
	Source Source

	// Legacy is true for synthesized rows from /api/v1/cli/repos and the
	// local ledger registry. kb-API rows have Legacy=false.
	Legacy bool
}

Bubble is the unified row returned by Merge. It's a superset of the fields produced by the three sources — empty values are normal for any field that the source row didn't supply.

type EffectiveValue added in v0.9.0

type EffectiveValue struct {
	Key             string
	Effective       string
	Layers          []LayerValue
	SafetyInversion bool
}

EffectiveValue is the computed result for one key.

func ResolveEffective added in v0.9.0

func ResolveEffective(key string, envVal, kbVal, userVal, defaultVal string) EffectiveValue

ResolveEffective computes the effective value for a key given pre-resolved per-layer string values. The caller is responsible for extracting each layer's string from its underlying config; this function applies precedence and safety inversion only.

For session_recording.mode: kb/user layers combine via kbconfig.ResolveEffectiveMode so a "disabled" on either side vetoes the other. Env wins outright when set; default backstops when everything else is empty.

For all other keys: env > kb > user > default precedence.

type KBBinding added in v0.9.0

type KBBinding struct {
	// KBID is the immutable kb identifier (kb_xxx) from the binding file.
	// Required; empty values are treated as "no binding".
	KBID string

	// KBType is the kb_type bucket (personal|profile|team|repo|custom|channel).
	// The resolver does NOT populate this — it requires a kb-API lookup or
	// merge.go output to determine type from kb_id. Callers enrich as needed;
	// the field exists on the struct so the resolver's return value can carry
	// it once enrichment has happened.
	KBType string

	// Source is the relative path of the marker file that produced this
	// binding, either ".sageox/config.yaml" (current) or ".sageox/config.json"
	// (legacy). Used by doctor and `ox kb config --show-origin` to attribute
	// values to their on-disk source.
	Source string

	// Anchor is the absolute path of the directory containing the .sageox/
	// marker — i.e. the workspace root, not the marker file itself.
	Anchor string

	// Scope is "exclusive" (default) for now. ADR-017 §6 reserves "subtree"
	// for nested overrides; the v1 resolver rejects nested markers entirely.
	Scope string

	// Endpoint is the SageOx API endpoint this binding belongs to (normalized
	// via endpoint.NormalizeEndpoint). If the binding file carries an explicit
	// `endpoint:` field, that value is used; otherwise the resolver falls back
	// to endpoint.Get() so callers always have a non-empty value.
	//
	// Note: this field is not in ADR-017 §1's struct diagram but the bead
	// description (ox-z526) requires it for downstream consumers (doctor's
	// kb-binding-endpoint-mismatch check, multi-endpoint dispatch). It is
	// always populated by ResolveCurrentKB.
	Endpoint string
}

KBBinding is the resolved binding for the directory tree rooted at Anchor. It is the only sanctioned answer to "which KB does this path belong to?" — session recording, murmurs, prime, etc. all consume this struct.

Field semantics match ADR-017 §1, with one addition (Endpoint) called out below.

func ResolveCurrentKB added in v0.9.0

func ResolveCurrentKB(cwd string) (*KBBinding, error)

ResolveCurrentKB walks up from cwd searching for a .sageox/ marker and returns the nearest binding, or (nil, nil) if no marker is found.

Return semantics:

  • (nil, nil) — no marker found between cwd and the filesystem root. This is the normal "outside any KB-bound tree" case and is not an error.
  • (b, nil) — marker found, binding parsed successfully.
  • (nil, err) — filesystem read error, malformed binding file, or a subtree override (ErrSubtreeOverridesNotSupported).

Marker priority within a single directory: config.yaml > config.json. Once the resolver finds a marker, it then checks ancestors for additional markers — any nested-marker arrangement returns ErrSubtreeOverridesNotSupported rather than silently picking one.

Endpoint resolution: if the binding file carries `endpoint:`, that value is used (normalized). Otherwise endpoint.Get() supplies the default. Callers can detect "no explicit endpoint" by comparing against the file source if they need to surface a kb-binding-endpoint-mismatch warning.

This function does NOT make network calls. KBType remains empty; callers enrich via the kb merger or KBClient.ListBubbles.

func ResolveCurrentKBEnriched added in v0.9.0

func ResolveCurrentKBEnriched(cwd string) (*KBBinding, error)

ResolveCurrentKBEnriched walks up from cwd like ResolveCurrentKB, then enriches the binding's KBType (and any slug surfaced through the binding's Source path) from the local KB metadata at <paths.KBDir(KBID)>/.sageox/meta.json (written by the daemon during sync).

Local-only — no network. If meta.json is missing or unreadable, KBType remains empty (the resolver still returns the KBID/Endpoint). This is the expected state for brand-new bubbles the daemon hasn't synced yet.

func (*KBBinding) IsWorkspace added in v0.9.0

func (b *KBBinding) IsWorkspace() bool

IsWorkspace reports whether the binding's anchor is a full workspace (ledger + cache + indexing state) vs a binding-only tree (just config.yaml).

A workspace has at least one of:

  • .sageox/cache/ (codedb, whisper, session state)
  • .sageox/ledger/ (ledger checkout symlink target)
  • .sageox/kb/* (per-project kb symlinks)

Binding-only trees omit all three. Per ADR-017 §5, binding-only trees do NOT spawn a daemon; workspace trees do.

type KBSource added in v0.8.0

type KBSource interface {
	ListBubbles(ctx context.Context) ([]api.KB, error)
}

KBSource is the contract for fetching new-API kb rows. Defined as an interface so tests can supply fakes without spinning up an httptest server when they only care about the merge logic.

type LayerValue added in v0.9.0

type LayerValue struct {
	Scope   string // "env" | "kb" | "user" | "default"
	Source  string // human label e.g. "OX_SESSION_RECORDING", ".sageox/config.yaml"
	Value   string // empty if unset at this layer
	Trigger bool   // true when safety-inversion fired *because of* this layer
}

LayerValue is one layer in the precedence chain.

type LedgerSource added in v0.8.0

type LedgerSource interface {
	ListLedgers(ctx context.Context) ([]LegacyLedgerRow, error)
}

LedgerSource is the contract for enumerating local ledger registry entries. Implementations typically scan paths.LedgersDataDir(...) for each known endpoint and read each ledger's project config.

type LegacyLedgerRow added in v0.8.0

type LegacyLedgerRow struct {
	RepoID   string // SageOx repo_id (extracted from path or project config)
	Name     string // display name (typically the host repo's name)
	Slug     string // optional kebab-case slug
	LocalDir string // on-disk ledger checkout path
	Endpoint string // SageOx API endpoint this ledger is bound to
	URL      string // git clone URL when known (rare for legacy ledgers)
}

LegacyLedgerRow is the merger-facing projection of a local ledger.

type LegacyTeamRow added in v0.8.0

type LegacyTeamRow struct {
	TeamID   string // team_xxx — used as RepoID-equivalent for dedup
	Name     string
	Slug     string
	URL      string // git clone URL
	LocalDir string // on-disk team-context checkout (may be empty if not yet cloned)
}

LegacyTeamRow is the merger-facing projection of a /api/v1/cli/repos team-context entry. Decoupled from api.RepoInfo so a future API shape change doesn't ripple into the merger.

type LegacyTeamSource added in v0.8.0

type LegacyTeamSource interface {
	ListTeamContexts(ctx context.Context) (rows []LegacyTeamRow, endpoint string, err error)
}

LegacyTeamSource is the contract for fetching legacy team-context rows from /api/v1/cli/repos. The merger only needs the repo map and the endpoint the rows came from — the rest of ReposResponse is unused.

type MergeResult added in v0.8.0

type MergeResult struct {
	Bubbles  []Bubble
	Warnings []SourceWarning
}

MergeResult is the return value of Merge. Bubbles is the deduped union; Warnings is one entry per source that errored (non-fatally).

type Merger added in v0.8.0

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

Merger fans out to the three sources and merges the results. Construct via NewMerger; the zero value is not usable.

func NewMerger added in v0.8.0

func NewMerger(kb KBSource, teams LegacyTeamSource, ledger LedgerSource) *Merger

NewMerger constructs a Merger. Any of the three sources may be nil — a nil source contributes zero rows and never produces a warning, which is the expected shape during daemon startup or in narrow tests that only exercise one branch.

func (*Merger) Merge added in v0.8.0

func (m *Merger) Merge(ctx context.Context) (MergeResult, error)

Merge fans out in parallel to all three sources, deduplicates by stable identifier, and returns the unified result. The returned error is reserved for catastrophic failures unrelated to any single source — today it's always nil; per-source failures land in Warnings.

type Source added in v0.8.0

type Source string

Source identifies which of the three fan-out sources produced a Bubble or a warning. Stable strings — surfaced in JSON output and logs.

const (
	SourceKB         Source = "kb"
	SourceTeamLegacy Source = "team_legacy"
	SourceLedger     Source = "ledger_legacy"
)

type SourceWarning added in v0.8.0

type SourceWarning struct {
	Source Source
	Err    string
}

SourceWarning is a non-fatal error from one of the three sources. The merger collects these into MergeResult.Warnings so the caller can render them without losing the rows from sources that did succeed.

Jump to

Keyboard shortcuts

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