docir — design documents Graph

Documents / Architecture / arch-7fd54a82f7d6

Doc-Index CLI — the CLI surface

The command vocabulary agents drive docir with, the static site build, and a worked flow through them end to end.

arch-7fd54a82f7d6architectureactive#architecture
View as Markdown◉ View in graph

CLI commands

Every command below exists in docir --help; the groups are agent, daemon, mcp, schema, self and tag. Global flags come before the command (docir --pretty get <id>): --home, --store, --no-daemon, --json, --pretty, --no-trim.

Readquery/search/context return skeletons (no body); only get returns one.

Command Purpose
docir get <id> [<id>...] [--section "<heading>"] Documents in full — several in one command, <id>#<heading> for one section of one, --section for the single-document form. Two or more ids answer with {documents, missing}; a ref that does not resolve is reported there rather than failing the read
docir query --type decision --status accepted --tag auth Structured filtering; also --owner, --stale, --code, and --expr for a JMESPath question the flags cannot ask, --code, --limit/--offset
docir search "<text>" Full-text over title, description and body; --explain for the rank and BM25 behind each hit
docir context "<agent task>" Ranked minimal set (FTS5 + semantic, fused) plus graph neighbours; --expand bounds the neighbour slots, --min-score filters on similarity, --also adds a phrasing of your own (several queries take turns rather than pooling), --explain returns the terms behind each rank

Write — the single sanctioned path to a markdown file.

Command Purpose
docir add --type <t> --title "..." --description "..." Create a document with valid frontmatter; id allocated for you
docir update <id> --status resolved Metadata patch and/or a body edit (--append-section, --replace-section, --replace-body --force)
docir archive <id> / docir unarchive <id> Soft-remove/restore from active search, reversible
docir delete <id> [--force] Hard-delete file + index rows; blocked by incoming related links unless forced, which unlinks them
docir tag add <key> --description "..." / docir tag list Manage the tag registry
docir tag rename <old> <new> / docir tag rm <key> [--force] Rename across every referencing document / remove it

Maintenance — the derived index and the corpus's shape.

Command Purpose
docir reindex [--changed] Rebuild the index from the canonical files; re-embeds what it re-saves; read documents_skipped
docir check [--strict] [--strict-all] [--fix] Tier 1 structural findings, including unblocked — a document whose every blocker has closed; --strict is the CI gate (errors only), --fix repairs what needs no guess
docir lint --deep Tier 2 advisories (content similarity, scope creep)
docir bench <fixture.yaml> Score this store's retrieval against tasks whose answers you know
docir embed --flush Force a synchronous recompute of dirty vectors
docir schema show / docir schema validate Inspect the merged schema / check docs-schema.yaml before a write reaches it

Bootstrap, serving and the installation itself.

Command Purpose
docir init [DIR] [--profiles ...] Create a project-local .docir store that commands discover by walking up
docir build --out site/ Render the corpus as a self-contained static site
docir agent install / docir agent update Install or refresh AI-assistant instruction files
docir daemon start / status / stop Escape hatches; the daemon is otherwise spawned and reaped for you
docir mcp serve Expose the same vocabulary as MCP tools over the same dispatcher
docir self status / docir self upgrade What is installed and whether it is current / upgrade and resync the store
docir version Print the docir version

---|---| | docir get <id> | Return one document in full | | docir query --type decision --status accepted --tag auth | Structured filtering | | docir search "<text>" | Full-text search | | docir context "<agent task>" | Ranked, minimal relevant document set | | docir update <id> --status resolved | Update a document via CLI (writes file + indexes metadata/FTS/relations synchronously, embedding async) | | docir add | Create a new document with valid frontmatter (writes file + indexes synchronously, embedding async) | | docir archive <id> / docir unarchive <id> | Soft-remove/restore a document from active search, reversible | | docir delete <id> [--force] | Hard-delete file + all index rows; blocked by incoming related links unless forced | | docir tag add <key> --description "..." / docir tag list | Manage the tag registry | | docir tag rename <old> <new> / docir tag rm <key> [--force] | Rename a tag across all documents / remove it (blocked while in use unless forced, which strips the key from referencing documents) | | docir reindex [--changed] | Manual fallback: rebuild index after external/manual file edits |


Publishing the corpus

docir build --out site/ renders the whole store as a self-contained static site — one HTML page per document plus an index, no external requests, so it works from file:// and publishes to GitHub Pages or S3 unchanged. It is what turns the corpus into something reviewable by people who will never run the CLI.

The site is derived like the index, and is guarded accordingly: every *.html in --out is removed before writing, so a document deleted from the store cannot survive as an orphaned page nobody can reach and nobody knows is stale. "Delete everything here first" has to be sure it owns "here" — a previous docir build is recognised, and any other non-empty directory is refused unless --force, because --out is a path a person types.

The build does one query and then one batched get for every body. Bodies are absent from every list path by contract, so a build that stopped at query would report the right document count and publish empty pages — which looks exactly like success. It reads the whole corpus deeply, which is the shape the batch exists for; missing is ignored, because these ids came from query a moment ago and one that has since gone is a page not to publish rather than an error, and an empty store skips the pass instead of asking get to tolerate an empty list.

Architecturally, publishing is a leaf module: it takes documents as data (the docir get JSON shape) rather than importing documents.api. The site is a projection of the public contract, not a second reader of the aggregate — do not "simplify" it by handing it a DocumentService.

End-to-end business flow example

Scenario: an agent is asked to implement a new authentication endpoint.

  1. Discover context Agent runs docir context "implement new auth endpoint". The CLI ranks the corpus twice — FTS5 over the text and cosine over the vectors, per document and per section — fuses the two rankings, then expands one hop across the relation graph inside the same --limit budget. Expansion follows outgoing edges and incoming successor edges, so a decision that supersedes a hit arrives with it rather than sitting one hop away backwards. It returns a handful of documents instead of the whole store — as skeletons: frontmatter, typed edges, staleness, and no bodies. That is the contract that makes the step cheap.

  2. Read only what matters Agent judges relevance from the skeletons — by similarity, the raw cosine, never by score, which is rank-derived and says nothing about how good a match was — then calls docir get once for every body it actually needs: docir get adr-3f9a2b1c7d4e arch-0002 issue-0003. A hit that named a matched_section is addressed inline as <id>#<heading>, so the agent pays for one section instead of a body ten times its size, and one process instead of one per document. That second saving is the larger one — a docir read is dominated by starting the interpreter, not by retrieval (issue-9509f9fa3631).

  3. Check what the change is governed by docir query --code src/auth/login.py answers the other direction: which documents declared they govern the files about to change. The patterns are matched as text, so a file the branch deletes still finds its decisions.

  4. Implement the feature (outside the doc system).

  5. Record a new decision or open issue If the implementation surfaces a new tradeoff: docir add --type decision --title "Refresh token rotation" --description "..." --tags auth,api --related adr-3f9a2b1c7d4e --code "src/auth/**" auth and api must already exist in the tag registry and the --related target must exist, or the call fails Tier 0. The CLI allocates the id from the index counter — never by scanning files, which is what keeps parallel agents from minting the same one — writes the file with valid frontmatter, and indexes it. The vector is queued, not computed: add --wait-embeddings if the next command must find it semantically.

  6. Update the status of the resolved issue docir update issue-7d1e4b9c02fa --status resolved. The CLI validates the transition against the type's state machine, rewrites the frontmatter, and updates metadata, FTS and relations synchronously — the index is current when the command returns. A status-only change does not touch the embedding.

  7. Commit Agent or human commits the changed .md files, purely as a history action. The index was already updated in steps 5–6 and does not depend on the commit; it is gitignored, and a fresh clone rebuilds it with docir reindex.

  8. Human review A teammate reviews the diff as plain markdown — no database inspection — and traces the decision's history with git log on the file. For a reader who will not run the CLI, docir build --out site/ renders the same corpus as a browsable site with the relation graph in both directions.

To amend: Re-verify: