# docujoint

**Declare the format once. Everything else is derived.**

docujoint is a deterministic engine for markdown documentation. You describe
your format in one YAML file — document types, typed tables, evidence schemes,
the rules that matter to you — and it parses every document into data it can
check, chart, query, and refuse to let drift.

No database and no proprietary format: one markdown file per concept, ordinary
links, readable in any editor and diffable in any repo. Your `format.yaml` is a
profile over [Google's Open Knowledge Format](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf).

MIT · Node 20+ · [docujoint.com](https://docujoint.com)

```sh
npm i -g docujoint
dj init my-docs && cd my-docs

dj lint --all --warnings-as-errors                 # the gate
dj dashboard --dashboard dashboard.yaml            # the app, served on :4173
```

`dj` is the command; `docujoint` works too.

---

## What "derived" means

You write a row. The engine works out what it means.

```markdown
## Features
| ID | Feature            | Implemented           | Gap           |
|----|--------------------|-----------------------|---------------|
| f1 | Buyer pays by card | repo://shop/pay.ts    | —             |
| f2 | Saved wallets      | repo://shop/wallet.ts | —             |
| f3 | Apple Pay          | —                     | not built yet |
```

Given a scan of your codebase (`--inventory artifacts.json`), f1 is **built**,
f2 is **drift** — it cites a file the scan no longer has — and f3 is
**missing**. None of those words appears in the document, and none of them is
built into the engine either. Your format declares them:

```yaml
blocks:
  features:
    heading: Features
    columns:
      - { name: ID, id_grammar: "f<n>", unique: error }
      - { name: Implemented, refs: [repo, api] }
      - { name: Gap }
    derive:
      state:
        - { when: "implemented && !gap && ref_broken", then: drift }
        - { when: "implemented && !gap",               then: built }
        - { when: "!implemented && gap",               then: missing }
    forbid_columns:                    # a Status column is a lie waiting to happen
      - { name: Status, severity: error }
```

Two teams can use the same word for opposite things and both are right. The
engine ships **no vocabulary** — a test proves it, by running two formats whose
identical state names mean opposite things.

## Commands

| | |
|---|---|
| `dj init [dir]` | scaffold a working vault — `--template starter\|programming` |
| `dj lint --all` | check every document against the format. **This is the gate** |
| `dj dashboard` | render the app: served live, or `--out file.html` for one offline file |
| `dj report` | the numbers, headless — indicators, coverage, evidence, reverse gap |
| `dj graph` | project the vault into SQLite: nodes, edges, rows, citations |
| `dj index` | maintain `index.md` — surgical, never regenerated |
| `dj annotate` | apply one declarative form action to a document |
| `dj media` | move heavy media out of the vault, rewriting the references |
| `dj feedback` | the capture ledger — reader input, kept out of the documents |
| `dj catalog` | every name a definition may compose a UI from |
| `dj skills` | authoring skills for coding agents — list, or install |
| `dj parse` | the parsed vault as JSON — the IR everything else is computed from |

`dj <command> --help` for that command's own flags.

## The dashboard is declared, not coded

`dashboard.yaml` says what a reader sees. The renderer supplies the components;
a definition **picks** from `dj catalog` and can never inject one — which is what
makes a definition written by someone else safe to serve.

```yaml
views:
  - id: person-board
    label: "{title}"
    for_each: { type: person }         # one board per document of that type
    widgets:
      - widget: stat-row
        indicators:
          - { label: Assigned, select: rows, from: features,
              where: "owner contains '{path}'" }
      - widget: tabs
        tabs:
          - { widget: rows, label: Features,  from: features,       where: "owner contains '{path}'" }
          - { widget: rows, label: Questions, from: open-questions, where: "owner contains '{path}'" }
```

Served, the page writes back: forms your format declares edit rows through the
linter, so an edit that would introduce an error is **refused** rather than
committed. Written with `--out`, it is one self-contained file that opens from
`file://` with no server — a tested contract, not a best effort.

Reader input that is not an edit — an answer, a correction, a screenshot —
appends to a **ledger** beside the vault instead of to a document, and
`dj feedback` is the queue you work it from. Hosting the same page is two flags:

```sh
dj dashboard --dashboard dashboard.yaml --out dashboard.html \
  --feedback-url        https://docs.example.com/api/feedback \
  --feedback-upload-url https://docs.example.com/api/upload
```

A host that answers those two endpoints gets the local server's capture
behaviour, in the same record shape, so `dj feedback` reads a hosted ledger
without conversion. Documents still change only by editing documents.

## The vault as a graph

```sh
dj graph --out kb.db          # SQLite: node · edge · row_ · cite (+ FTS5)
dj graph --sql kb.sql         # a portable script, for a host with no driver
```

Every edge is one the documents already state — a resolved link, a declared
metadata value that names another document, a row belonging to its document, a
row citing evidence. Nothing is inferred and nothing is extracted by a model, so
rebuilding from the same vault produces the same bytes and an update is a
rebuild, never a migration.

The schema ships with a query library — neighbours, k-hop, distance, who-cites,
orphans, reverse gap, full-text, GraphRAG context — as SQL *text*, so the same
statements run against `node:sqlite`, a Cloudflare Durable Object and D1 alike.
Every one of them is executed against a real database in the test suite.

Semantic search is supported, and **the engine never calls a model**: vectors
arrive the way an inventory does, as a validated side input.
`dj graph --embed-plan <model>` writes what still needs one; `--embeddings`
files what comes back. SQLite narrows candidates with the same prefilters a
keyword search uses, and the host scores what survives.

## In CI

```sh
dj scan --source app=../app --source lib=../lib --out artifacts.json
dj lint --all --vault docs --format format.yaml \
        --inventory artifacts.json --warnings-as-errors
```

Exit 1 on any error, and on warnings too with that flag. A row citing evidence
your scan does not have is a finding (`evidence-broken`), not merely a colour on
a chart.

`dj scan` is what produces that inventory: it walks the checkouts you name and
writes every file as `<scheme>://<source>/<path>`, where the source is the first
path segment — so one `--source app=…` answers for `repo://app/…` and
`test://app/…` alike. Schemes a directory walk cannot confirm (`api://`, `db://`,
`event://`, `route://`) are reported as skipped rather than emitted: the command
reports what it checked and nothing more.

## For coding agents

```sh
dj skills install --all          # → .claude/skills, never overwrites
```

Six skills — format authoring, dashboard authoring, view composition, forms,
vault authoring, and operating — teach an agent your format's grammar, the
composable vocabulary, and the loop around them (serve, gate, collect). The
engine is the referee: the agent writes, `dj lint` decides when it is done, and
neither of them gets a vote.

## What a format can enforce

Reserved files · ignore globs · wikilink ban · broken internal links · tree
placement · evidence-scheme grammars · comment stripping · structural ambiguity
(unclosed fences and comments, comments that swallow headings, multiple H1) ·
frontmatter (required and optional per type, enums, list shape, placeholder
detection) · section expectations and duplicates · block tables (multi-table,
differing columns, cell counts, column enums) · id grammar, uniqueness and
sequence · forbidden columns · intra-file column references · derive rules ·
declarative row and block checks in a real expression language · index validity ·
inventory reconciliation · media resolution and size policy.

Definitions are **data**: the `when:` language is a parser, never `eval`.

## How it is built

```
src/core/     pure functions — no filesystem, no network
  definition  format.yaml → a compiled definition (grammars, enums, schemes)
  markdown    frontmatter, fence-aware sections and tables, escaped pipes
  expr        the when: language — a real parser; definitions are never executed
  parseDoc    one file → concept + findings
  tree        classification and placement
  links       internal links, inventory reconciliation, derived state
  engine      (files, definition, inventory?) → (IR, findings)
src/render/   the dashboard: payload, shell, catalogue, Astryx tokens
src/graph/    the SQLite projection: build · schema · embeddings
src/cli.ts    all I/O lives here
```

The core is I/O-free, which is why the same code runs in a CLI, a Worker and a
test with no seams.

## Requirements

Node **20+**. `dj graph --out kb.db` writes a real database through
`node:sqlite`, built in from Node **22.5**; on older runtimes use `--sql` for a
portable script — it tells you so rather than failing.

Three runtime dependencies: `yaml`, plus `mermaid` and `d3`, which are inlined
into a rendered dashboard only when a vault actually uses diagrams or graph
views.

## Tests

`npm test` runs 97 tests with no network and no fixtures to regenerate. Four of
them are golden tests that run the engine over a real vault and assert exact
numbers — the most valuable tests in the suite, and the only ones whose subject
cannot ship here, because a vault is someone's private documentation and its
shape is private with it. Both the vault and its expected numbers live outside
this repository (`DOCUJOINT_GOLDEN=/path/to/golden.json`; see
`test/reference-vault.ts`), so those four skip loudly and a fresh clone passes
the other 93.

## Licence

MIT. Third-party work bundled into generated output is credited in
[NOTICE](./NOTICE): design tokens from [Astryx](https://github.com/facebook/astryx)
(MIT), and mermaid and d3, inlined only when a vault needs them.
