---
name: docujoint-operating
description: Run a docujoint vault day to day — serve the dashboard locally, use `dj lint` as the gate that decides when an edit is done, and collect what readers submitted through the page's forms (the local ledger and a hosted capture endpoint). Use when serving, reviewing, publishing or harvesting feedback rather than authoring a definition.
---

# Operating a docujoint vault

Authoring skills say what to write. This one is the loop around it: **serve →
read → lint → collect**. Three commands carry it, and each has a local and a
hosted form that behave the same way on purpose.

## Serve it locally

```sh
dj dashboard --vault . --format format.yaml --dashboard dashboard.yaml
#   → http://localhost:4173   (--port to move it)
```

Serving is the default because it is what you want while writing. The page
re-renders from disk on every request, so edit a document, refresh, see the
effect — no build step, no watcher. That covers `format.yaml` and
`dashboard.yaml` too: declare a block and a view over it in the same sitting,
refresh, and both are there. A `format.yaml` caught mid-save prints its errors
in the server's console and keeps serving the last one that compiled, so a
half-written file never takes the page down.

What the served page can do that a file cannot:

| | |
|---|---|
| **Forms write back** | a form the format declares edits the real markdown, through the linter |
| **Refusal** | an edit that would introduce an error is rejected, not committed — you get the finding back in the page |
| **Capture** | reader input lands in the ledger, never in a document |
| **`--propose`** | stage every edit into a change set instead of writing it, for reviewing before applying |

`--out file.html` writes one self-contained file instead: it opens from
`file://` with no server, and forms in it are read-only unless the host gives
them somewhere to POST (below). That file is the artifact you send someone.

Nothing in the page is coded per vault. The renderer supplies the components;
`dashboard.yaml` **picks** from `dj catalog` and can never inject one — which is
why a definition written by someone else is safe to serve.

## Produce the inventory — `dj scan`

`--inventory artifacts.json` is only as true as the file behind it, and the file
is a scan of the world outside the documents:

```sh
dj scan --source app=../app --source lib=../lib --out artifacts.json
```

- A citation is `<scheme>://<source>/<path>`, so `--source` binds the **first
  path segment**, never the scheme: `app=../checkout` makes `repo://app/src/x.ts`
  and `test://app/test/x.test.ts` both resolve, because they are the same
  checkout. The scanned directory's own name never appears in a URI.
- Repeat `--source` per checkout. Only schemes that name files are walked
  (`repo`, `test`, `src`, `code`, `file`, plus any you claim with `--scheme`);
  `api://`, `db://`, `event://` and `route://` name things no directory walk can
  confirm, so they are reported as skipped rather than emitted green.
- Run it on **every push**, before lint. A scan is a fact about now: delete a
  cited file and the next scan turns that row from `built` into a finding the
  same day, instead of a hand audit finding it months later.

## Lint is the gate, not a linter

```sh
dj lint --all                                  # while writing
dj lint --all --inventory artifacts.json --warnings-as-errors   # what CI runs
```

Treat it as the referee that decides when work is finished, not as advice:

- **Errors** mean the parser broke or a document asserts something the format
  forbids. Fix before committing — nothing downstream is trustworthy until they
  are gone.
- **Warnings** are the incompleteness report: unbound rows, untested features,
  broken links, stale indexes. Burn them down; gate merges with
  `--warnings-as-errors` once a vault is mature.
- **`--inventory`** is what makes lint able to disagree with the documents. A
  row citing evidence the scan does not contain is a finding
  (`evidence-broken`), not merely a colour on a chart — so a single
  `dj lint --all --inventory … --warnings-as-errors` is the whole gate. There is
  no second reference-checking command to remember.
- Name paths instead of `--all` to check just what you touched; cross-document
  rules (links, index coverage, evidence) are still evaluated over everything,
  and only the reporting narrows.

Anything that edits documents goes through the same referee: `dj annotate`
applies one declarative form action from the terminal and is refused on the same
terms the page is. If a script edits markdown any other way, the gate has been
bypassed.

## Collect what readers submitted

Readers answer questions, resolve anomalies and attach evidence through the
page's forms. Two different things can happen with that input, and the format
decides which:

- **A form action** edits a row through the linter — the document changes.
- **A capture** appends to a **ledger**, a sidecar file the documents never see.

Capture exists so a reader can say something without holding write access to the
vault, and so an answer is never applied on someone's behalf. The ledger is
`feedback.jsonl` beside the vault by default:

```sh
dj feedback list                       # everything captured
dj feedback list --status new          # only what nobody has actioned
dj feedback list --concept "Flows/Purchase Flow.md"
dj feedback set 7f3a --status applied  # once you have written it into the docs
dj feedback rm 7f3a                    # junk only — a status is the reversible path
```

Statuses are your format's (`feedback:` declares them) — the engine ships no
vocabulary here either. The loop is: read the capture, decide whether it is
true, write it into the document yourself, then mark the record actioned so it
leaves everyone's queue.

### Local vs hosted capture

They differ only in where the POST goes:

```sh
# local: the served page posts to itself; records land in ./feedback.jsonl,
# image attachments in ./feedback-media/
dj dashboard --dashboard dashboard.yaml

# hosted: one static file, told where the host accepts capture
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 \
  --feedback-status-url https://docs.example.com/api/feedback/status \
  --feedback-delete-url https://docs.example.com/api/feedback/delete

# a static file that DISPLAYS a ledger you already have, read-only
dj dashboard --dashboard dashboard.yaml --out dashboard.html --feedback feedback.jsonl
```

Each URL is a CAPABILITY the host asserts, and the page renders only the
controls the host backs: `--feedback-url` alone gives capture forms and reads
records from a `GET` of the same URL; add `--feedback-status-url` and the
`review:` verdict buttons appear; add `--feedback-delete-url` and Delete does.
Serve mode declares all of them against itself. A host that implements only
capture never shows a button that would 404.

`--feedback-url` is where the page POSTs a capture record and GETs back what has
been captured; `--feedback-upload-url` is where image answers go. A host that
implements those two endpoints gets the same capture behaviour as the local
server, and the records keep the same shape — so `dj feedback` reads a ledger
exported from a hosted deployment without conversion.

If the page offers an image answer and no upload endpoint is configured, the
submission fails loudly rather than dropping the attachment.

## The rule underneath all of it

**Documents change by editing documents.** The dashboard is a reader and a
referee-gated writer; the ledger is an inbox. Nothing else — not a database, not
the hosted app, not an ingest job — is allowed to be the place a fact lives,
because everything downstream is rebuilt from the markdown and would overwrite
it.
