Skip to content

Build KB foundation library: discovery, config, schema, frontmatter parsing #635

Description

@williamthorsen

Problem

The three planned KB skills (kb-retrieve, kb-add, kb-curate) all need the same plumbing: a way to find a knowledge base on disk, load its schema and aliases, parse and write note frontmatter, and validate notes against the schema. Without a shared library, each skill would reimplement that plumbing or shell out to vault-local scripts, leading to drift between skills, no typed contract, and no single place to evolve the schema. A future MCP server wrapping these operations needs a clean library surface to wrap.

Context

The vault at github.com/williamthorsen/vaults.coding already implements equivalent logic in bash and TypeScript: scripts/add-frontmatter.sh for frontmatter generation, scripts/check-notes/ for validation, and .kb/schema.yaml plus .kb/tag-aliases.yaml as data. Those implementations are vault-local. The goal here is to extract the durable concepts (not literal code) into a typed monorepo library with an ergonomic, MCP-wrappable surface that any KB-aware skill can consume.

Monorepo conventions to mirror: async I/O via node:fs/promises, Zod 4 for input validation at API boundaries, subpath exports for tree-shaking, sibling __tests__/ directories per module. The reference shape is @codeassembly/run-core.

A separate follow-up ticket will deliver an MCP server (@codeassembly/kb-mcp) that wraps this library; the skill tickets (kb-retrieve, kb-add, kb-curate) depend on both.

Proposed solution

Ship @codeassembly/kb-core at packages/kb-core/, with five subpath exports addressing distinct concerns:

  • discovery: finds the active KB by ancestor walk for .kb/, and loads and merges the user-global (~/.claude/kb.yaml) and project-local (.agents/kb.yaml) registries. The on-disk config is a kbs: { name: { path, description?, default?, readonly? } } map; project entries replace user entries by name on collision; relative paths resolve against the config file's directory; tilde expands to $HOME. Path existence is not checked at load time.

  • schema: exposes a bundled default schema (Diátaxis four types plus the canonical required and optional field set), reads per-KB .kb/schema.yaml, and merges them under narrow-only rules. Types may be narrowed (subset of default), required and optional may be extended, but a default-required field cannot be demoted to optional. Violations of these rules are rejected at load time.

  • frontmatter: parses a note (from path or literal string) into a ParsedNote carrying typed Frontmatter (required fields strongly typed; optional fields preserved through an extra map), and writes a Frontmatter plus body back to YAML. Round-trip is idempotent.

  • tags: loads .kb/tag-aliases.yaml into a typed AliasMap and exposes canonicalize / findAliasFor for tag resolution.

  • rules: exposes frontmatterRule and tagAliasRule as KbRule objects ({ name, check(input): Finding[] }), plus a runRules aggregator. Skills compose whichever rules they need; the interface is the extension point for future rules.

All public functions take single-object inputs so a future MCP wrapper can mechanically bind Zod-validated payloads. The library throws on errors; success/failure shaping is left to consumers like the future kb-mcp server.

Out of scope for this ticket: the vault index, content-shape rules (wikilinks, hardcoded paths), the MCP server, and any user-facing skill.

Acceptance criteria

  • @codeassembly/kb-core is importable by other monorepo packages via five subpath exports (./discovery, ./schema, ./frontmatter, ./tags, ./rules).
  • findKbRoot walks ancestor directories for .kb/ and returns the discovered root or null.
  • loadKbConfig reads ~/.claude/kb.yaml and .agents/kb.yaml, merges with project entries overriding user entries by name, expands tilde paths, and resolves relative paths against the source file's directory.
  • The bundled default schema is exported as a typed constant exposing the Diátaxis four types and the documented required and optional fields.
  • loadSchema returns the default verbatim when no .kb/schema.yaml exists, merges per-KB overrides under narrow-only rules, and rejects illegal overrides (new types, demoted required fields, fields in both required and optional) at load time.
  • parseNote and writeFrontmatter round-trip the vault's frontmatter shape exactly: required fields, optional fields preserved, UTC dates, list-form tags.
  • loadAliases reads .kb/tag-aliases.yaml, rejects collisions and self-aliases at load time, and produces an AliasMap consumed by canonicalize and findAliasFor.
  • frontmatterRule and tagAliasRule produce findings whose rule codes (frontmatter.*) and severities match the vault's check-notes output on a vendored fixture set representative of each finding code; verified by a parity test against a checked-in golden file.
  • All new behavior is covered by tests; tests live in sibling __tests__/ directories per module with co-located fixtures.
  • The kb.yaml configuration schema is documented (package README and/or .agents/PROJECT.md entry) so a future project can declare a KB without reading source.

Dependencies

None. Blocks the planned @codeassembly/kb-mcp package ticket and the three skill tickets (kb-retrieve, kb-add, kb-curate).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions