Skip to main content
docir

documents as IR — a CLI that compiles git-backed markdown
into a verifiable, read-optimized index for AI coding agents.

PyPI Python CI License: MIT

The idea · Quickstart · Why not just… · Commands · Docs


The idea

"IR" is intermediate representation — the thing a compiler turns source code into. docir treats your markdown the same way: the files are the source, and the SQLite index (metadata + FTS5 full-text + a typed relation graph + semantic embeddings) is a derived artifact you can throw away and rebuild.

  source of truth     docir                  derived index
  canonical           the compiler           rebuildable · gitignored
  ───────────────     ──────────────────     ─────────────────────────
  decisions/*.md      parse · validate       metadata · FTS5
  issues/*.md     ──▶ allocate ids       ──▶ relation graph (typed)
  tags.yaml           embed (deferred)       vector embeddings

Git is canonical. docir reindex rebuilds the entire index from the files. When the database and the files disagree, the files win.

Why not just…

plain .md files RAG over your docs docir
Consistent frontmatter / schema ✅ enforced
Retrieval by meaning ✅ lexical + semantic †
Typed relation graph
Knows what's stale
Works offline, nothing to run ⚠️ ✅ after the model downloads once †
Token-cheap for agents ⚠️ ✅ skeletons

Orientation, not a shoot-out — the right tool depends on your setup.

† What semantic retrieval costs you

Semantic search runs on a real embedding model, installed by default. It is quantized, CPU-only, and runs locally — nothing is sent anywhere — but it is not free:

Model BAAI/bge-small-en-v1.5, 384-dim, quantized ONNX
Download ~64 MB, once, on first use — the only step that needs network
Install ~240 MB of dependencies (onnxruntime, numpy, tokenizers, …)
Runtime CPU only, no GPU, no API key; the daemon keeps the model warm

If that is too heavy — a CI image, a container you keep small, an air-gapped box — opt out and docir falls back to a dependency-free hashing embedder:

export DOCIR_EMBEDDER=deterministic

That embedder scores similarity by shared vocabulary rather than meaning, which is the same signal the full-text index already provides. The cost is measured, not asserted: docir context scores recall@5 0.96 with the model against 0.93 without it, and puts the right document first far more often (MRR 0.95 vs 0.80). Isolate the embedding signal by turning graph expansion off, and on questions phrased in words the documents never use the model gets 0.86 where the fallback gets 0.79 — below the 0.79 that plain full-text search manages on its own. Corpus, tasks, judgments and caveats are in benchmarks/; uv run python benchmarks/run.py reproduces it.

Switching embedders re-embeds rather than mixing vector spaces: docir records which model produced each vector, ignores the others, and recomputes them on the next write or docir embed --flush.

Quickstart

# 1. install
uv tool install docir          # or: pipx install docir

# 2. scope docs to this repo (creates ./.docir, like `git init`)
#    skip it and docs go to the global ~/.docir — docir warns if you are in a repo
docir init

# 3. teach this repo's AI agent to drive docir (writes a Claude Code skill)
docir agent install            # add --agent agents for an AGENTS.md block

# 4. capture a decision…
docir add --type decision --title "Auth strategy" \
    --description "How the service authenticates API clients." --stdin < draft.md

# 5. …and retrieve it by intent, next session
docir context "implement a new auth endpoint"

In a terminal, docir context prints ranked, body-less skeletons — frontmatter and typed edges, no body — so you scan wide, then fetch a body by id with docir get:

$ docir context "implement a new auth endpoint"
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ id         ┃ type     ┃ status   ┃ title              ┃ description                      ┃ score ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ adr-0001   │ decision │ proposed │ Auth strategy      │ How the service authenticates    │ 0.033 │
│            │          │          │                    │ API clients.                     │       │
│ issue-0001 │ issue    │ open     │ Token refresh race │ Refresh token race under         │ 0.016 │
│            │          │          │                    │ concurrent logins.               │       │
└────────────┴──────────┴──────────┴────────────────────┴──────────────────────────────────┴───────┘

Built for agents, though: when the output is captured (stdout isn't a TTY), the same command emits compact, trimmed JSON — no borders, empty fields dropped, ~40% fewer tokens:

$ docir context "implement a new auth endpoint" | cat
[{"id":"adr-0001","title":"Auth strategy","description":"How the service authenticates API clients.","type":"decision","status":"proposed","tags":["auth"],"archived":false,"stale":false,"score":0.0328,"similarity":0.8951,"via_graph":false}, ...]

An absent field means its default (no owner, not stale). score is a reciprocal-rank fusion of the full-text and vector rankings, so ordering is the point and the absolute value means little — a nonsense query against a one-document store scores about the same as a perfect match. similarity is the raw cosine against your query and does carry absolute meaning, which is what --min-score filters on: with it, an empty result is a real answer rather than an impossible one. --json forces JSON anywhere, --pretty forces the table, --no-trim keeps every field.

The model

  • Git is the source of truth. The index is a compile artifact — derived, .gitignored, rebuildable. Nothing lives only in the database.
  • One write path. Agents never edit markdown directly; every write goes through the CLI, which guarantees frontmatter/schema consistency and collision-free id allocation. You are not an agent: the files are yours, and the rule for humans is narrower — see what you may edit by hand.
  • Reads return skeletons. query / search / context return frontmatter + typed edges + staleness — no body. Fetch bodies by id with get. An agent scans wide cheaply, then reads deep only where it matters.
  • Staleness is data, not a guess. Optional owner / verified fields plus a per-type review cadence make "is this doc still true?" a first-class, checkable fact — and a worklist: docir query --owner platform-team --stale is one steward's review queue, cleared a document at a time with docir update <id> --verified.
  • Relations are typed. A related edge carries a kind (supersedes, depends_on, implements, …) — a real graph, not a bag of links.

What you may edit by hand

The files are git-backed markdown and docir reindex exists precisely to pick up an outside change — so hand-editing is supported, but not on every field:

by hand instead
document body
docs-schema.yaml, docs/tags.yaml no CLI write path for the schema
tags, status, related, type docir update --set-tags / --status / --set-related
id ❌ never it is the primary key; changing it orphans every inbound link
verified ❌ never docir update <id> --verified — it asserts a human re-read the doc

Then run docir reindex && docir check. Reindex reports documents_skipped for files whose frontmatter will not parse — those are absent from every read path, not merely flagged — and check catches unregistered tags, undeclared statuses, unknown types, dangling links and duplicate ids. A hand-written verified date is the one thing nothing can verify, which is exactly why it should not be written by hand.

Commands

Command What it does
docir init Scope docs to a project-local ./.docir store (like git init)
docir add Create a document — the single write path
docir update Edit content, metadata, or relations of an existing document
docir context <query> Ranked relevant set (skeletons) — full-text + vector, fused (--min-score to filter noise)
docir search / query Full-text search / structured filter (query --owner X --stale is a review queue)
docir get <id> Full document with body
docir check Structural findings — duplicate ids, dangling edges, staleness (--strict gates CI on errors, --fix repairs them)
docir agent install Teach this repo's AI agent to drive docir

Full command reference

init · add · update · archive · unarchive · delete
get · query · search · context
tag {add, list, rename, rm}
agent {install, update}
schema {show, validate}
check [--fix] · lint · reindex · embed · version
daemon serve

Store precedence (highest first): --homeDOCIR_HOME → a project-local .docir/ found by walking up from the CWD → the global ~/.docir default. --no-daemon runs any command in-process instead of over the daemon socket. Output is a Rich table at a TTY and compact JSON when piped; --json / --pretty force either, and --no-trim keeps every field. That applies to --help too — docir --help | cat returns the command vocabulary as JSON, so an agent can discover the CLI without parsing box-drawing characters.

How state is stored

State lives in one resolved store per invocation. Run docir init in a repo to keep its docs with the code: .docir/docs/ and docs-schema.yaml are committed; the derived index (SQLite + embeddings) is gitignored and rebuilds with docir reindex. Without init, docir falls back to a global ~/.docir.

The daemon keeps the embedding model warm and serializes writes; the CLI is a thin, stateless client that spawns and respawns it transparently. Embeddings are the one deferred, eventually-consistent piece — a content change flags the vector dirty and returns; everything else (file, metadata, FTS, relations) is synchronous. Force a flush with --wait-embeddings, docir embed --flush, or docir reindex --embeddings.

Schema: core + profiles

Documents are constrained by a per-type schema (required fields, status grammar, allowed relations). docir ships a frozen, domain-agnostic core plus swappable profilessoftware (default: decision / issue / architecture / release_note), research, ops, qa, legal. A docs-schema.yaml merges core → profiles → inline, so you extend it without mutating the base.

docir init --profiles software,qa   # pick profiles up front
docir init --id-style sequential    # readable adr-0007 instead of the default random
docir schema show                   # the merged result — what validation enforces
docir schema validate               # check an edit before it reaches a write

docir init writes id_style: random by default — ids like adr-3f9a2b1c7d4e, which two branches can never mint identically. Pass --id-style sequential for human-friendly adr-0007 numbering; it is collision-free within one store, but a merge can bring two branches that each allocated the same number (docir check reports it as duplicate-id).

The generated docs-schema.yaml carries a commented-out worked example of the inline types: / relation_types: syntax, so the grammar is discoverable at the point of use.

Architecture

Vertical bounded-context modules (documents, tags, indexing, agents) over a shared platform, wired by thin entry_points. Dependencies flow entry_points → modules → platform → config; boundaries are enforced by tach in CI — not by convention. Each module exposes exactly one public file (api.py) described by a CONTRACT.md.

See docs/doc-index-architecture.md for the design rationale and docs/architecture-rules.md for the module rules.

Contributing

Issues and PRs welcome. Read the architecture rules and the ADRs first — module boundaries are machine-checked by tach in CI, alongside lint, type-check, and a coverage gate. Every design deviation is recorded as an ADR.

uv sync                                              # dev environment
uv run python benchmarks/run.py                      # retrieval quality + token cost
uv run pytest --cov=docir --cov-fail-under=90        # tests + coverage gate
uv run ruff check . && uv run ty check && uv run tach check

License

MIT © Sergei Konovalov

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

docir-0.7.0.tar.gz (113.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

docir-0.7.0-py3-none-any.whl (159.0 kB view details)

Uploaded Python 3

File details

Details for the file docir-0.7.0.tar.gz.

File metadata

  • Download URL: docir-0.7.0.tar.gz
  • Upload date:
  • Size: 113.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for docir-0.7.0.tar.gz
Algorithm Hash digest
SHA256 0ed9090cacbaa8c23ec189d809b298ab7da955d5ecc4d4897bcc430e64fc4c6c
MD5 674f939e6c1dae25ee8a49beb176d3c7
BLAKE2b-256 eab9d91ce2ec797c03e1b4ff1aa5c97fe2f3d68d1f690c2a99aa8b0065690cab

See more details on using hashes here.

Provenance

The following attestation bundles were made for docir-0.7.0.tar.gz:

Publisher: publish-to-pypi.yml on l0kifs/docir

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docir-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: docir-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 159.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for docir-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c42526a47d7ba8a3c94a2d6a49be69534612b8715aea647c61d06dca7cca11a0
MD5 988e372778a746a826917e736be447d0
BLAKE2b-256 249b4e59973774fdaa3e46cf17e2aa371edcfb9f5a73781cd6750383b2163b6f

See more details on using hashes here.

Provenance

The following attestation bundles were made for docir-0.7.0-py3-none-any.whl:

Publisher: publish-to-pypi.yml on l0kifs/docir

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.19.0

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.1

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.1

2 files

This release

0.7.0 This release

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page