@danxbot/docs
v0.1.57
Published
Repo-agnostic docs package (DX-3336/DX-3337): a typed, versioned node-file format, a loader that builds an in-memory graph from a docs root, and a fail-loud `check` CLI.
Readme
@danxbot/docs
Repo-agnostic docs package for the PLN-12 documentation system. Nothing in this package is danxbot-specific (R-1): any repo can adopt it and store its documentation as typed node files, one file per node.
What the docs are for (schema v3)
Docs explain what a system does and why, in words a non-engineer can follow: its parts, the real things people ask about within them, how things happen, the rules and decisions behind them, and how to run things locally. They then point engineers at where to look. They never map individual classes, API routes, schemas or code contracts — that couples the docs to the code, guarantees drift, and repeats what the code already says (PLN-12 R-11).
A node is a substantial topic someone would look up — the system, a major
part of it, or a real part of that part (an app, a component, a domain idea)
— with a full page (PLN-12 R-13). A rule, a decision, a caveat, a sequence of
steps or a set of states is never its own node: it is a section of the page
it belongs to. A node that would hold only one sentence belongs as a bullet
or a section inside its parent's page instead of a node of its own — check
enforces a fifty-word floor on a System/Area/Topic body precisely so a docs
tree can't quietly fill up with these thin, single-sentence nodes again.
Node file format
A node file is markdown with YAML frontmatter:
---
id: area.issues
type: Area
title: Issue cards
summary: Issue cards track work as it moves from an idea to finished and merged.
schema_version: 3
part_of: system.danxbot
look_in: [src/issues]
color: iris
rules:
- Status is always derived from lifecycle timestamps, never written directly.
---
Issue cards are the unit of work danxbot hands to its agents...Required fields: id (stable, kebab/dot form, e.g. area.issues or
topic.ready-at), type, title, summary (capped at
SUMMARY_MAX_LENGTH, 200 characters), schema_version (must be exactly
SCHEMA_VERSION_MAX, currently 3), and part_of (a single parent node id —
required on every type except System, and forbidden on System, the
one root of the tree).
Node types
Each type has a plain-language label and a one-line explanation, exported as
NODE_TYPE_INFO so every view shows those words instead of a bare type name.
| Type | Shown as | Answers |
|---|---|---|
| System | The system | What this system is, who it is for, and what it does for them |
| Area | Part of the system | A major part of the system, and what it is responsible for |
| Topic | Topic | A real part of an area people ask about — an app, a component or a domain idea — explained in full |
| Guide | How to | Step-by-step instructions for a task |
| Path | Learning path | A reading order through the docs for one kind of reader |
Concept, Flow, Lifecycle and Decision are gone as node types — see
"Page sections" below for where their content now lives.
part_of — the one navigation relation
part_of is the only relation that shapes the tree every view lays out. It
is a single required scalar (never a list), so "exactly one parent" is the
shape itself, not a rule that has to count: a docs root has exactly one
System (the root, with no part_of), and every other node has exactly one
part_of parent, with no cycles. Which type may be part-of which is fixed
(PART_OF_TARGET_TYPES):
| A … | may be part_of a … |
|---|---|
| Area | System only |
| Topic | an Area, or another Topic |
| Guide | a System, an Area, or a Topic |
| Path | System only |
A Path is never anyone's part_of parent.
see_also — the one cross-link
see_also is a plain list of other node ids, rendered as inline "See also"
text links on the page — never a ring node, never a typed relation. It
replaces v2's uses, leads-to, explained-by and step-of.
Page sections
A page's structured content is data, not free markdown, so the app can
render each section with the right component (a sequence diagram, a state
diagram, ...). Every section below is optional, and (other than Path,
whose only content is its steps[]) any of System/Area/Topic/Guide
may carry any of them:
| Field | Shape | Renders as |
|---|---|---|
| why_it_matters | a plain string | a short callout |
| how_it_works | {actor, action, target?, notes?}[] | a sequence diagram |
| states | {subject, states: {id, label?}[], transitions: {from, to, trigger?, guard?}[]} | a state diagram — every transitions[].from/.to must name a declared states[].id |
| rules | a non-empty string list | a rules list |
| decisions | {choice, why, rejected?, trade_off?}[] | a "why it's built this way" list |
| watch_out_for | a non-empty string list | a caveats list |
Optional fields
see_also[] (node ids), look_in[] (engineer pointers at area level: a
directory, package or service name — a source file such as
src/orders/service.ts, or a file-and-symbol pointer such as
src/orders.ts#placeOrder, is refused), provenance ({plans?, cards?,
commits?}), audience[] (developer | operator | integrator | agent),
icon (an @danxbot/ui icon name), color (a key into @danxbot/ui's
palette — PALETTE_KEYS: iris, aqua, mint, amber, rose, azure,
slate).
Type-specific structured fields: System nodes may declare value_props[]
({icon, title, summary, color?}) — its own selling points, separate from
its Areas; Path nodes require steps[] ({node, note?}, node
referencing another node's id) and carry no page sections at all. Every
other type carries only the common fields above plus the page sections table.
The full schema is defined once, in code, at src/schema.ts — that is the
single source of truth for the validator, the loader and any later consumer
(search, the docs app).
Only one schema version is read
There is no reader for older versions and no automatic migration. A file
whose schema_version is not exactly the current one is refused by check
with a message saying so — a v2 file (with Concept, Flow, Lifecycle or
Decision nodes, or a relations[] list) and a v1 file (with Component,
Contract, Invariant or Domain nodes, code anchors or verified_at)
must both be rewritten as a v3 node (CLAUDE.md Core Principle 1 — fail loud,
no legacy reader, no version-conditional logic).
Install
npm install @danxbot/docsUsage
import { loadDocsGraph, loadDocsRoot, runChecks } from "@danxbot/docs";
// Fail-loud: throws DocsValidationError if anything is wrong.
const graph = loadDocsGraph("./docs");
// Or collect every problem yourself (this is what the CLI does):
const loadResult = loadDocsRoot("./docs");
const problems = runChecks(loadResult);npx tsx src/cli.ts check ./docs # dev
danxbot-docs check ./docs # after `npm run build`
danxbot-docs search ./docs "how does X work" [--limit N] # compact YAML, for agents
danxbot-docs tree ./docs [--root <id>] [--depth N] # compact YAML of the part-of hierarchy, for agents
danxbot-docs show ./docs <id> # one node's full content, for agents
danxbot-docs serve ./docs [--port N] # the docs app, live (default port 4300)
danxbot-docs build ./docs ./out # the docs app, as a static site
danxbot-docs demo [--port N] # serves the bundled example project — see below
danxbot-docs promotion-guide # the generic "draft a changeset from a plan" protocol, for agentsFor agents: tree → search → show (DX-3581, PLN-12 architecture §6)
The short instruction an agent is pointed at ships as AGENTS.md; this section is the longer explanation.
An agent gains context on a docs root in three steps, never by reading every file:
treeto orient.danxbot-docs tree ./docsprints the wholepart_ofhierarchy at depth 2 (the System and its Areas and Topics) as compact YAML:id,type,title,summary,fileand a realchildrencount per node — enough to see the shape of the system and pick a branch, without reading any page body.--root <id>narrows to one branch (depth counted from that node);--depth Nwidens or narrows how far the walk descends. An unknown--rootfails loud, naming the nearest real ids.searchto find.danxbot-docs search ./docs "<query>"(see above) ranks nodes (BM25 over title, summary and body, with stop words dropped, words stemmed, a bonus for a phrase a page actually says, and a small boost for graph proximity), returning one line per match:id,type,title,summary, the page'sfile, its size inbytesandlines, andlook_inwhen the page declares code pointers (DX-5582). No body and no ancestor or related lists: the size says whether to read the file whole or in slices. The same answer comes from the danx-dashboard MCP'sdocs_searchfor this checkout, another docs root or a team repo's hosted docs. Six matches by default;--limit Nfor more.show <id>to read.danxbot-docs show ./docs <id>prints that ONE node in full: its complete frontmatter (every section it carries — rules, decisions, how_it_works, states, watch_out_for, ...) and its markdown body, plus its ancestor chain and its direct children — both reduced toid+summaryonly, never expanded. Reading stays granular:shownever pulls in a child's own content, so the next step is always another explicittree/search/showcall, not an ever-growing dump. An unknown id fails loud, naming the nearest real ids.
Read a source file directly only when show's look_in pointers say to
look there — the docs themselves never name classes, routes or symbols
(R-11), so there is nothing to dig for inside the page content itself.
For agents: promoting a plan into docs (DX-3674)
A separate, generic protocol from the read-side one above: how to turn a
FINISHED PLAN into one docs changeset against an existing root — reading the
whole plan, reading the landed commits, deciding create/update/merge/
supersede against the current graph, and validating with check before
committing. It ships as PROMOTION.md; an agent reads it
with danxbot-docs promotion-guide rather than needing to know where inside
node_modules this package physically unpacked. This package is
repo-agnostic and knows nothing about WHICH plan or WHERE to push the
result — the dispatch that points an agent at this guide supplies that.
Demo (DX-3369)
The package ships a realistic example docs project — Ringlet, a fictional
service for selling event tickets — at examples/ringlet/, separate from the
neutral test fixtures. It is written at schema v3's granularity: a handful of
substantial Areas, each with a few Topics, plus Guides and learning Paths —
never a thin node holding just one sentence. It names no classes, routes or
schemas; each area points engineers at where to look instead.
One command serves it:
npm run demo # dev, via tsx — reads examples/ringlet live
node dist/cli.js demo # after `npm run build` — same, from the built CLIBoth are exactly danxbot-docs demo [--port N] under the hood (default port
4300, same as serve) — a thin wrapper around serve pointed at the bundled
examples/ringlet directory, resolved from the package's own install
location rather than the current working directory, so it works from
anywhere the package is installed or checked out.
The docs app (serve / build)
serve and build both need the app's own assets pre-built once via
npm run build:app (or npm run build, which does both the CLI and the app) —
they read dist/app rather than compiling anything on the fly. serve is a
plain node:http static file server: no database, no framework. It re-reads
the given docs root and rebuilds the embedded data on every request to /,
so editing a node file shows up on the next browser reload with no restart.
build writes the same app as a fully static site: the built assets plus one
index.html with that docs root's data embedded, deployable to any static
host. Both refuse (fail loud, R-3) to serve/build an invalid docs root —
exactly the same validation check runs.
The app itself: a children-only focus web (a page's part_of children fan
out around it; a cross-link is an inline "See also" text link, never a ring
node), one page per node (zero-context summary first, then its page sections
rendered with the matching component, "where to look" pointers and
provenance), and search results rendered as small clickable graphs (the same
engine search uses). Real chrome throughout — nav, part-of breadcrumbs, a
command-palette search, appearance controls (theme/text size/contrast/
motion), deep links — built entirely from @danxbot/ui, responsive down to
phone width. Source under src/app/ (the React app) and src/server/ (the
CLI-facing serve/build layer); see src/app-data.ts for the one contract
between them.
Check rules
check exits non-zero and lists every problem, one per line, with the file
it was found in:
schema_violation— schema errors: missing/invalid frontmatter, a field that fails the schema, a removed node type, apart_ofmissing on a non-System node or present on a System, alook_innaming a source file or a symbolunsupported_schema_version— aschema_versionother than the current one (never auto-migrated — CLAUDE.md Core Principle 1)unreadable_file— a.mdfile or a directory under the root that cannot be read (a dangling symlink, a permission error); it is reported and the rest of the tree still loads (DX-5597)duplicate_id— the sameiddeclared by more than one filedangling_relation— apart_oforsee_alsopointing at an unknown idpart_of_cycle— apart_ofcyclepart_of_target_type— apart_ofparent of a type the tree's shape doesn't allow (see thepart_oftable above)multiple_systems— more than one System in a docs rootsummary_too_long— asummaryover the 200-character cappath_unknown_node— a Path step naming an unknown node idunknown_icon_name— anicon(node-level or on avalue_propsentry) that isn't a real@danxbot/uiicon nameunknown_color— acolor(node-level or on avalue_propsentry) that isn't aPALETTE_KEYSpalette keythin_page— a System/Area/Topic body under the fifty-word floor a real page needs (R-13); Guides and Paths are exempthow_it_works_code_symbol— ahow_it_worksactor or target written as code (name::method,name(),name->method, or a source-file or file-and-symbol shape) instead of a role in the system (R-11); prose fields are never scanned, and a bare class name is left toPROMOTION.md's audience test
Tests
tests/fixtures/valid/ is a small neutral example project (every node type,
every page section, look_in, color, icon and value_props each
exercised at least once) that passes check cleanly. tests/fixtures/invalid/
has one fixture directory per check rule above, each proving that rule alone
fires, plus: v1-node and v2-node (both retired schema versions, refused
the same way), removed-type (a v3 file using a type v3 deleted),
multiple-problems-one-file (two rules firing on the same file at once), and
look-in-file-symbol (a schema-level look_in shape refusal). examples/
ringlet/ (see "Demo" above) is a separate, much larger project meant for a
human to browse, not as a minimal test fixture — it is also covered by its
own test asserting it passes check with zero problems, and by
tree-show-ringlet.test.ts, which exercises tree and show against it
(card AC "tests cover both commands on the Ringlet example").
