docir 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docir-0.1.0/PKG-INFO +205 -0
- docir-0.1.0/README.md +187 -0
- docir-0.1.0/pyproject.toml +117 -0
- docir-0.1.0/src/docir/__init__.py +25 -0
- docir-0.1.0/src/docir/__main__.py +7 -0
- docir-0.1.0/src/docir/config/__init__.py +0 -0
- docir-0.1.0/src/docir/config/settings.py +157 -0
- docir-0.1.0/src/docir/entry_points/__init__.py +0 -0
- docir-0.1.0/src/docir/entry_points/cli/__init__.py +0 -0
- docir-0.1.0/src/docir/entry_points/cli/app.py +516 -0
- docir-0.1.0/src/docir/entry_points/cli/body_input.py +32 -0
- docir-0.1.0/src/docir/entry_points/cli/rendering.py +209 -0
- docir-0.1.0/src/docir/entry_points/cli/runner.py +103 -0
- docir-0.1.0/src/docir/entry_points/composition.py +208 -0
- docir-0.1.0/src/docir/entry_points/daemon/__init__.py +0 -0
- docir-0.1.0/src/docir/entry_points/daemon/cmds.py +69 -0
- docir-0.1.0/src/docir/entry_points/daemon/lifecycle.py +138 -0
- docir-0.1.0/src/docir/entry_points/daemon/socket_executor.py +32 -0
- docir-0.1.0/src/docir/entry_points/dispatch.py +225 -0
- docir-0.1.0/src/docir/modules/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/agents/CONTRACT.md +50 -0
- docir-0.1.0/src/docir/modules/agents/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/agents/api.py +46 -0
- docir-0.1.0/src/docir/modules/agents/application/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/agents/application/ports.py +31 -0
- docir-0.1.0/src/docir/modules/agents/application/service.py +146 -0
- docir-0.1.0/src/docir/modules/agents/domain/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/agents/domain/rendering.py +84 -0
- docir-0.1.0/src/docir/modules/agents/domain/results.py +35 -0
- docir-0.1.0/src/docir/modules/agents/domain/targets.py +66 -0
- docir-0.1.0/src/docir/modules/agents/infra/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/agents/infra/file_sink.py +19 -0
- docir-0.1.0/src/docir/modules/agents/infra/template_provider.py +22 -0
- docir-0.1.0/src/docir/modules/agents/infra/templates/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/agents/infra/templates/skill.md +201 -0
- docir-0.1.0/src/docir/modules/documents/CONTRACT.md +54 -0
- docir-0.1.0/src/docir/modules/documents/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/api.py +45 -0
- docir-0.1.0/src/docir/modules/documents/application/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/application/dto.py +209 -0
- docir-0.1.0/src/docir/modules/documents/application/services/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/application/services/document_service.py +398 -0
- docir-0.1.0/src/docir/modules/documents/application/services/id_generator.py +40 -0
- docir-0.1.0/src/docir/modules/documents/application/services/maintenance_service.py +174 -0
- docir-0.1.0/src/docir/modules/documents/domain/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/domain/entities/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/domain/entities/document.py +83 -0
- docir-0.1.0/src/docir/modules/documents/domain/entities/relation.py +23 -0
- docir-0.1.0/src/docir/modules/documents/domain/schema.py +163 -0
- docir-0.1.0/src/docir/modules/documents/domain/services/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/domain/services/graph_checks.py +223 -0
- docir-0.1.0/src/docir/modules/documents/domain/services/markdown_sections.py +66 -0
- docir-0.1.0/src/docir/modules/documents/domain/services/similarity_lint.py +69 -0
- docir-0.1.0/src/docir/modules/documents/domain/services/slugify.py +21 -0
- docir-0.1.0/src/docir/modules/documents/domain/services/validation.py +93 -0
- docir-0.1.0/src/docir/modules/documents/domain/value_objects/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/domain/value_objects/identifiers.py +76 -0
- docir-0.1.0/src/docir/modules/documents/domain/value_objects/queries.py +24 -0
- docir-0.1.0/src/docir/modules/documents/domain/value_objects/relations.py +45 -0
- docir-0.1.0/src/docir/modules/documents/infra/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/documents/infra/default_schema.py +43 -0
- docir-0.1.0/src/docir/modules/documents/infra/profiles.py +176 -0
- docir-0.1.0/src/docir/modules/documents/infra/schema_loader.py +177 -0
- docir-0.1.0/src/docir/modules/indexing/CONTRACT.md +31 -0
- docir-0.1.0/src/docir/modules/indexing/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/indexing/api.py +44 -0
- docir-0.1.0/src/docir/modules/indexing/application/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/indexing/application/ports/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/indexing/application/ports/scheduler.py +37 -0
- docir-0.1.0/src/docir/modules/indexing/domain/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/indexing/domain/results.py +13 -0
- docir-0.1.0/src/docir/modules/indexing/domain/scoring.py +73 -0
- docir-0.1.0/src/docir/modules/indexing/infra/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/indexing/infra/scheduler.py +115 -0
- docir-0.1.0/src/docir/modules/tags/CONTRACT.md +32 -0
- docir-0.1.0/src/docir/modules/tags/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/tags/api.py +12 -0
- docir-0.1.0/src/docir/modules/tags/application/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/tags/application/dto.py +13 -0
- docir-0.1.0/src/docir/modules/tags/application/services/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/tags/application/services/tag_service.py +109 -0
- docir-0.1.0/src/docir/modules/tags/domain/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/tags/domain/entities/__init__.py +0 -0
- docir-0.1.0/src/docir/modules/tags/domain/entities/tag.py +19 -0
- docir-0.1.0/src/docir/platform/__init__.py +0 -0
- docir-0.1.0/src/docir/platform/clock/__init__.py +6 -0
- docir-0.1.0/src/docir/platform/clock/port.py +19 -0
- docir-0.1.0/src/docir/platform/clock/system.py +14 -0
- docir-0.1.0/src/docir/platform/embedding/__init__.py +10 -0
- docir-0.1.0/src/docir/platform/embedding/deterministic.py +49 -0
- docir-0.1.0/src/docir/platform/embedding/fastembed.py +51 -0
- docir-0.1.0/src/docir/platform/embedding/port.py +30 -0
- docir-0.1.0/src/docir/platform/embedding/vector.py +49 -0
- docir-0.1.0/src/docir/platform/errors/__init__.py +123 -0
- docir-0.1.0/src/docir/platform/filesystem/__init__.py +7 -0
- docir-0.1.0/src/docir/platform/filesystem/markdown_store.py +185 -0
- docir-0.1.0/src/docir/platform/filesystem/ports.py +62 -0
- docir-0.1.0/src/docir/platform/filesystem/tag_store.py +35 -0
- docir-0.1.0/src/docir/platform/persistence/__init__.py +0 -0
- docir-0.1.0/src/docir/platform/persistence/alembic/env.py +45 -0
- docir-0.1.0/src/docir/platform/persistence/alembic/script.py.mako +26 -0
- docir-0.1.0/src/docir/platform/persistence/alembic/versions/0001_initial_index.py +106 -0
- docir-0.1.0/src/docir/platform/persistence/alembic/versions/0002_typed_edges_and_staleness.py +46 -0
- docir-0.1.0/src/docir/platform/persistence/engine.py +45 -0
- docir-0.1.0/src/docir/platform/persistence/models.py +96 -0
- docir-0.1.0/src/docir/platform/persistence/ports.py +134 -0
- docir-0.1.0/src/docir/platform/persistence/repositories.py +362 -0
- docir-0.1.0/src/docir/platform/persistence/sqlalchemy_uow.py +61 -0
- docir-0.1.0/src/docir/platform/persistence/unit_of_work.py +51 -0
- docir-0.1.0/src/docir/platform/transport/__init__.py +0 -0
- docir-0.1.0/src/docir/platform/transport/client.py +47 -0
- docir-0.1.0/src/docir/platform/transport/messages.py +70 -0
- docir-0.1.0/src/docir/platform/transport/protocol.py +45 -0
- docir-0.1.0/src/docir/platform/transport/server.py +70 -0
docir-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: docir
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Doc-Index CLI — git-backed markdown documents with a derived SQLite/FTS5/semantic index, served by a warm local daemon.
|
|
5
|
+
Author: Sergei Konovalov
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Dist: typer>=0.15
|
|
8
|
+
Requires-Dist: rich>=13.9
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: python-frontmatter>=1.1
|
|
11
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
12
|
+
Requires-Dist: alembic>=1.14
|
|
13
|
+
Requires-Dist: pydantic-settings>=2.7
|
|
14
|
+
Requires-Dist: fastembed>=0.5 ; extra == 'embeddings'
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Provides-Extra: embeddings
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
|
|
21
|
+
<picture>
|
|
22
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo/docir-lockup-dark.png" />
|
|
23
|
+
<img src="assets/logo/docir-lockup.png" alt="docir" width="260" />
|
|
24
|
+
</picture>
|
|
25
|
+
|
|
26
|
+
**doc**uments as **IR** — a CLI that *compiles* git-backed markdown<br />into a verifiable, read-optimized index for AI coding agents.
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/docir/) [](https://pypi.org/project/docir/) [](https://github.com/l0kifs/docir/actions) [](LICENSE)
|
|
29
|
+
|
|
30
|
+
[The idea](#the-idea) · [Quickstart](#quickstart) · [Why not just…](#why-not-just) · [Commands](#commands) · [Docs](docs/)
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## The idea
|
|
37
|
+
|
|
38
|
+
"IR" is *intermediate representation* — the thing a compiler turns source code into.
|
|
39
|
+
docir treats your markdown the same way: the **files are the source**, and the SQLite
|
|
40
|
+
index (metadata + FTS5 full-text + a typed relation graph + semantic embeddings) is a
|
|
41
|
+
**derived artifact you can throw away and rebuild**.
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
source of truth docir derived index
|
|
45
|
+
canonical the compiler rebuildable · gitignored
|
|
46
|
+
─────────────── ────────────────── ─────────────────────────
|
|
47
|
+
decisions/*.md parse · validate metadata · FTS5
|
|
48
|
+
issues/*.md ──▶ allocate ids ──▶ relation graph (typed)
|
|
49
|
+
tags.yaml embed (deferred) vector embeddings
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **Git is canonical.** `docir reindex` rebuilds the entire index from the files. When the
|
|
53
|
+
> database and the files disagree, the files win.
|
|
54
|
+
|
|
55
|
+
## Why not just…
|
|
56
|
+
|
|
57
|
+
| | plain `.md` files | RAG over your docs | **docir** |
|
|
58
|
+
|---|:---:|:---:|:---:|
|
|
59
|
+
| Consistent frontmatter / schema | ❌ | ❌ | ✅ enforced |
|
|
60
|
+
| Retrieval by meaning | ❌ | ✅ | ✅ (lexical + semantic) |
|
|
61
|
+
| Typed relation graph | ❌ | ❌ | ✅ |
|
|
62
|
+
| Knows what's stale | ❌ | ❌ | ✅ |
|
|
63
|
+
| Works offline, nothing to run | ✅ | ⚠️ | ✅ |
|
|
64
|
+
| Token-cheap for agents | ❌ | ⚠️ | ✅ skeletons |
|
|
65
|
+
|
|
66
|
+
*Rough orientation, not a benchmark — the right tool depends on your setup.*
|
|
67
|
+
|
|
68
|
+
## Quickstart
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 1. install
|
|
72
|
+
uv tool install docir # or: pipx install docir
|
|
73
|
+
|
|
74
|
+
# 2. scope docs to this repo (creates ./.docir, like `git init`)
|
|
75
|
+
docir init
|
|
76
|
+
|
|
77
|
+
# 3. teach this repo's AI agent to drive docir (writes a Claude Code skill)
|
|
78
|
+
docir agent install # add --agent agents for an AGENTS.md block
|
|
79
|
+
|
|
80
|
+
# 4. capture a decision…
|
|
81
|
+
docir add --type decision --title "Auth strategy" \
|
|
82
|
+
--description "How the service authenticates API clients." --stdin < draft.md
|
|
83
|
+
|
|
84
|
+
# 5. …and retrieve it by intent, next session
|
|
85
|
+
docir context "implement a new auth endpoint"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
In a terminal, `docir context` prints ranked, body-less **skeletons** — frontmatter and
|
|
89
|
+
typed edges, no body — so you scan wide, then fetch a body by id with `docir get`:
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
$ docir context "implement a new auth endpoint"
|
|
93
|
+
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
|
|
94
|
+
┃ id ┃ type ┃ status ┃ title ┃ description ┃ score ┃
|
|
95
|
+
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
|
|
96
|
+
│ adr-0001 │ decision │ proposed │ Auth strategy │ How the service authenticates │ 0.033 │
|
|
97
|
+
│ │ │ │ │ API clients. │ │
|
|
98
|
+
│ issue-0001 │ issue │ open │ Token refresh race │ Refresh token race under │ 0.016 │
|
|
99
|
+
│ │ │ │ │ concurrent logins. │ │
|
|
100
|
+
└────────────┴──────────┴──────────┴────────────────────┴──────────────────────────────────┴───────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Built for agents, though: when the output is **captured** (stdout isn't a TTY), the same
|
|
104
|
+
command emits **compact, trimmed JSON** — no borders, empty fields dropped, ~40% fewer tokens:
|
|
105
|
+
|
|
106
|
+
```console
|
|
107
|
+
$ docir context "implement a new auth endpoint" | cat
|
|
108
|
+
[{"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,"via_graph":false}, ...]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
*An absent field means its default (no owner, not stale); the relevance `score` is a
|
|
112
|
+
reciprocal-rank fusion of full-text + semantic matches, so ordering is the point. `--json`
|
|
113
|
+
forces JSON anywhere, `--pretty` forces the table, `--no-trim` keeps every field.*
|
|
114
|
+
|
|
115
|
+
## The model
|
|
116
|
+
|
|
117
|
+
- **Git is the source of truth.** The index is a compile artifact — derived,
|
|
118
|
+
`.gitignore`d, rebuildable. Nothing lives only in the database.
|
|
119
|
+
- **One write path.** Agents never edit markdown directly; every write goes through the
|
|
120
|
+
CLI, which guarantees frontmatter/schema consistency and collision-free id allocation.
|
|
121
|
+
- **Reads return skeletons.** `query` / `search` / `context` return frontmatter + typed
|
|
122
|
+
edges + staleness — *no body*. Fetch bodies by id with `get`. An agent scans wide cheaply,
|
|
123
|
+
then reads deep only where it matters.
|
|
124
|
+
- **Staleness is data, not a guess.** Optional `owner` / `verified` fields plus a per-type
|
|
125
|
+
review cadence make "is this doc still true?" a first-class, checkable fact (`docir check`).
|
|
126
|
+
- **Relations are typed.** A `related` edge carries a *kind* (`supersedes`, `depends_on`,
|
|
127
|
+
`implements`, …) — a real graph, not a bag of links.
|
|
128
|
+
|
|
129
|
+
## Commands
|
|
130
|
+
|
|
131
|
+
| Command | What it does |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `docir init` | Scope docs to a project-local `./.docir` store (like `git init`) |
|
|
134
|
+
| `docir add` | Create a document — the single write path |
|
|
135
|
+
| `docir update` | Edit content, metadata, or relations of an existing document |
|
|
136
|
+
| `docir context <query>` | Hybrid lexical + semantic ranked relevant set (skeletons) |
|
|
137
|
+
| `docir search` / `query` | Full-text search / structured filter (skeletons) |
|
|
138
|
+
| `docir get <id>` | Full document with body |
|
|
139
|
+
| `docir check` | Structural graph warnings — staleness, unknown types (`--strict` for CI) |
|
|
140
|
+
| `docir agent install` | Teach this repo's AI agent to drive docir |
|
|
141
|
+
|
|
142
|
+
### Full command reference
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
init · add · update · archive · unarchive · delete
|
|
146
|
+
get · query · search · context
|
|
147
|
+
tag {add, list, rename, rm}
|
|
148
|
+
agent {install, update}
|
|
149
|
+
check · lint · reindex · embed · version
|
|
150
|
+
daemon serve
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Store precedence (highest first): `--home` → `DOCIR_HOME` → a project-local `.docir/`
|
|
154
|
+
found by walking up from the CWD → the global `~/.docir` default. `--no-daemon` runs any
|
|
155
|
+
command in-process instead of over the daemon socket. Output is a Rich table at a TTY and
|
|
156
|
+
compact JSON when piped; `--json` / `--pretty` force either, and `--no-trim` keeps every field.
|
|
157
|
+
|
|
158
|
+
## How state is stored
|
|
159
|
+
|
|
160
|
+
State lives in one resolved store per invocation. Run `docir init` in a repo to keep its
|
|
161
|
+
docs with the code: `.docir/docs/` and `docs-schema.yaml` are **committed**; the derived
|
|
162
|
+
index (SQLite + embeddings) is **gitignored** and rebuilds with `docir reindex`. Without
|
|
163
|
+
`init`, docir falls back to a global `~/.docir`.
|
|
164
|
+
|
|
165
|
+
The daemon keeps the embedding model warm and serializes writes; the CLI is a thin,
|
|
166
|
+
stateless client that spawns and respawns it transparently. Embeddings are the one
|
|
167
|
+
deferred, eventually-consistent piece — a content change flags the vector dirty and returns;
|
|
168
|
+
everything else (file, metadata, FTS, relations) is synchronous. Force a flush with
|
|
169
|
+
`--wait-embeddings`, `docir embed --flush`, or `docir reindex --embeddings`.
|
|
170
|
+
|
|
171
|
+
## Schema: core + profiles
|
|
172
|
+
|
|
173
|
+
Documents are constrained by a per-type schema (required fields, status grammar, allowed
|
|
174
|
+
relations). docir ships a frozen, domain-agnostic **core** plus swappable **profiles** —
|
|
175
|
+
`software` (default: `decision` / `issue` / `architecture`), `research`, `ops`, `legal`.
|
|
176
|
+
A `docs-schema.yaml` merges `core → profiles → inline`, so you extend it without mutating
|
|
177
|
+
the base.
|
|
178
|
+
|
|
179
|
+
## Architecture
|
|
180
|
+
|
|
181
|
+
Vertical bounded-context **modules** (`documents`, `tags`, `indexing`, `agents`) over a
|
|
182
|
+
shared **platform**, wired by thin **entry_points**. Dependencies flow
|
|
183
|
+
`entry_points → modules → platform → config`; boundaries are enforced by
|
|
184
|
+
[tach](https://docs.gauge.sh) in CI — not by convention. Each module exposes exactly one
|
|
185
|
+
public file (`api.py`) described by a `CONTRACT.md`.
|
|
186
|
+
|
|
187
|
+
See [docs/doc-index-architecture.md](docs/doc-index-architecture.md) for the design
|
|
188
|
+
rationale and [docs/architecture-rules.md](docs/architecture-rules.md) for the module rules.
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
Issues and PRs welcome. Read the [architecture rules](docs/architecture-rules.md) and the
|
|
193
|
+
[ADRs](docs/adr/) first — module boundaries are machine-checked by [tach](https://docs.gauge.sh)
|
|
194
|
+
in CI, alongside lint, type-check, and a coverage gate. Every design deviation is recorded
|
|
195
|
+
as an ADR.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv sync # dev environment
|
|
199
|
+
uv run pytest --cov=docir --cov-fail-under=90 # tests + coverage gate
|
|
200
|
+
uv run ruff check . && uv run ty check && uv run tach check
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT © Sergei Konovalov
|
docir-0.1.0/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo/docir-lockup-dark.png" />
|
|
5
|
+
<img src="assets/logo/docir-lockup.png" alt="docir" width="260" />
|
|
6
|
+
</picture>
|
|
7
|
+
|
|
8
|
+
**doc**uments as **IR** — a CLI that *compiles* git-backed markdown<br />into a verifiable, read-optimized index for AI coding agents.
|
|
9
|
+
|
|
10
|
+
[](https://pypi.org/project/docir/) [](https://pypi.org/project/docir/) [](https://github.com/l0kifs/docir/actions) [](LICENSE)
|
|
11
|
+
|
|
12
|
+
[The idea](#the-idea) · [Quickstart](#quickstart) · [Why not just…](#why-not-just) · [Commands](#commands) · [Docs](docs/)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The idea
|
|
19
|
+
|
|
20
|
+
"IR" is *intermediate representation* — the thing a compiler turns source code into.
|
|
21
|
+
docir treats your markdown the same way: the **files are the source**, and the SQLite
|
|
22
|
+
index (metadata + FTS5 full-text + a typed relation graph + semantic embeddings) is a
|
|
23
|
+
**derived artifact you can throw away and rebuild**.
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
source of truth docir derived index
|
|
27
|
+
canonical the compiler rebuildable · gitignored
|
|
28
|
+
─────────────── ────────────────── ─────────────────────────
|
|
29
|
+
decisions/*.md parse · validate metadata · FTS5
|
|
30
|
+
issues/*.md ──▶ allocate ids ──▶ relation graph (typed)
|
|
31
|
+
tags.yaml embed (deferred) vector embeddings
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> **Git is canonical.** `docir reindex` rebuilds the entire index from the files. When the
|
|
35
|
+
> database and the files disagree, the files win.
|
|
36
|
+
|
|
37
|
+
## Why not just…
|
|
38
|
+
|
|
39
|
+
| | plain `.md` files | RAG over your docs | **docir** |
|
|
40
|
+
|---|:---:|:---:|:---:|
|
|
41
|
+
| Consistent frontmatter / schema | ❌ | ❌ | ✅ enforced |
|
|
42
|
+
| Retrieval by meaning | ❌ | ✅ | ✅ (lexical + semantic) |
|
|
43
|
+
| Typed relation graph | ❌ | ❌ | ✅ |
|
|
44
|
+
| Knows what's stale | ❌ | ❌ | ✅ |
|
|
45
|
+
| Works offline, nothing to run | ✅ | ⚠️ | ✅ |
|
|
46
|
+
| Token-cheap for agents | ❌ | ⚠️ | ✅ skeletons |
|
|
47
|
+
|
|
48
|
+
*Rough orientation, not a benchmark — the right tool depends on your setup.*
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 1. install
|
|
54
|
+
uv tool install docir # or: pipx install docir
|
|
55
|
+
|
|
56
|
+
# 2. scope docs to this repo (creates ./.docir, like `git init`)
|
|
57
|
+
docir init
|
|
58
|
+
|
|
59
|
+
# 3. teach this repo's AI agent to drive docir (writes a Claude Code skill)
|
|
60
|
+
docir agent install # add --agent agents for an AGENTS.md block
|
|
61
|
+
|
|
62
|
+
# 4. capture a decision…
|
|
63
|
+
docir add --type decision --title "Auth strategy" \
|
|
64
|
+
--description "How the service authenticates API clients." --stdin < draft.md
|
|
65
|
+
|
|
66
|
+
# 5. …and retrieve it by intent, next session
|
|
67
|
+
docir context "implement a new auth endpoint"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In a terminal, `docir context` prints ranked, body-less **skeletons** — frontmatter and
|
|
71
|
+
typed edges, no body — so you scan wide, then fetch a body by id with `docir get`:
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
$ docir context "implement a new auth endpoint"
|
|
75
|
+
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
|
|
76
|
+
┃ id ┃ type ┃ status ┃ title ┃ description ┃ score ┃
|
|
77
|
+
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
|
|
78
|
+
│ adr-0001 │ decision │ proposed │ Auth strategy │ How the service authenticates │ 0.033 │
|
|
79
|
+
│ │ │ │ │ API clients. │ │
|
|
80
|
+
│ issue-0001 │ issue │ open │ Token refresh race │ Refresh token race under │ 0.016 │
|
|
81
|
+
│ │ │ │ │ concurrent logins. │ │
|
|
82
|
+
└────────────┴──────────┴──────────┴────────────────────┴──────────────────────────────────┴───────┘
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Built for agents, though: when the output is **captured** (stdout isn't a TTY), the same
|
|
86
|
+
command emits **compact, trimmed JSON** — no borders, empty fields dropped, ~40% fewer tokens:
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
$ docir context "implement a new auth endpoint" | cat
|
|
90
|
+
[{"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,"via_graph":false}, ...]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
*An absent field means its default (no owner, not stale); the relevance `score` is a
|
|
94
|
+
reciprocal-rank fusion of full-text + semantic matches, so ordering is the point. `--json`
|
|
95
|
+
forces JSON anywhere, `--pretty` forces the table, `--no-trim` keeps every field.*
|
|
96
|
+
|
|
97
|
+
## The model
|
|
98
|
+
|
|
99
|
+
- **Git is the source of truth.** The index is a compile artifact — derived,
|
|
100
|
+
`.gitignore`d, rebuildable. Nothing lives only in the database.
|
|
101
|
+
- **One write path.** Agents never edit markdown directly; every write goes through the
|
|
102
|
+
CLI, which guarantees frontmatter/schema consistency and collision-free id allocation.
|
|
103
|
+
- **Reads return skeletons.** `query` / `search` / `context` return frontmatter + typed
|
|
104
|
+
edges + staleness — *no body*. Fetch bodies by id with `get`. An agent scans wide cheaply,
|
|
105
|
+
then reads deep only where it matters.
|
|
106
|
+
- **Staleness is data, not a guess.** Optional `owner` / `verified` fields plus a per-type
|
|
107
|
+
review cadence make "is this doc still true?" a first-class, checkable fact (`docir check`).
|
|
108
|
+
- **Relations are typed.** A `related` edge carries a *kind* (`supersedes`, `depends_on`,
|
|
109
|
+
`implements`, …) — a real graph, not a bag of links.
|
|
110
|
+
|
|
111
|
+
## Commands
|
|
112
|
+
|
|
113
|
+
| Command | What it does |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `docir init` | Scope docs to a project-local `./.docir` store (like `git init`) |
|
|
116
|
+
| `docir add` | Create a document — the single write path |
|
|
117
|
+
| `docir update` | Edit content, metadata, or relations of an existing document |
|
|
118
|
+
| `docir context <query>` | Hybrid lexical + semantic ranked relevant set (skeletons) |
|
|
119
|
+
| `docir search` / `query` | Full-text search / structured filter (skeletons) |
|
|
120
|
+
| `docir get <id>` | Full document with body |
|
|
121
|
+
| `docir check` | Structural graph warnings — staleness, unknown types (`--strict` for CI) |
|
|
122
|
+
| `docir agent install` | Teach this repo's AI agent to drive docir |
|
|
123
|
+
|
|
124
|
+
### Full command reference
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
init · add · update · archive · unarchive · delete
|
|
128
|
+
get · query · search · context
|
|
129
|
+
tag {add, list, rename, rm}
|
|
130
|
+
agent {install, update}
|
|
131
|
+
check · lint · reindex · embed · version
|
|
132
|
+
daemon serve
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Store precedence (highest first): `--home` → `DOCIR_HOME` → a project-local `.docir/`
|
|
136
|
+
found by walking up from the CWD → the global `~/.docir` default. `--no-daemon` runs any
|
|
137
|
+
command in-process instead of over the daemon socket. Output is a Rich table at a TTY and
|
|
138
|
+
compact JSON when piped; `--json` / `--pretty` force either, and `--no-trim` keeps every field.
|
|
139
|
+
|
|
140
|
+
## How state is stored
|
|
141
|
+
|
|
142
|
+
State lives in one resolved store per invocation. Run `docir init` in a repo to keep its
|
|
143
|
+
docs with the code: `.docir/docs/` and `docs-schema.yaml` are **committed**; the derived
|
|
144
|
+
index (SQLite + embeddings) is **gitignored** and rebuilds with `docir reindex`. Without
|
|
145
|
+
`init`, docir falls back to a global `~/.docir`.
|
|
146
|
+
|
|
147
|
+
The daemon keeps the embedding model warm and serializes writes; the CLI is a thin,
|
|
148
|
+
stateless client that spawns and respawns it transparently. Embeddings are the one
|
|
149
|
+
deferred, eventually-consistent piece — a content change flags the vector dirty and returns;
|
|
150
|
+
everything else (file, metadata, FTS, relations) is synchronous. Force a flush with
|
|
151
|
+
`--wait-embeddings`, `docir embed --flush`, or `docir reindex --embeddings`.
|
|
152
|
+
|
|
153
|
+
## Schema: core + profiles
|
|
154
|
+
|
|
155
|
+
Documents are constrained by a per-type schema (required fields, status grammar, allowed
|
|
156
|
+
relations). docir ships a frozen, domain-agnostic **core** plus swappable **profiles** —
|
|
157
|
+
`software` (default: `decision` / `issue` / `architecture`), `research`, `ops`, `legal`.
|
|
158
|
+
A `docs-schema.yaml` merges `core → profiles → inline`, so you extend it without mutating
|
|
159
|
+
the base.
|
|
160
|
+
|
|
161
|
+
## Architecture
|
|
162
|
+
|
|
163
|
+
Vertical bounded-context **modules** (`documents`, `tags`, `indexing`, `agents`) over a
|
|
164
|
+
shared **platform**, wired by thin **entry_points**. Dependencies flow
|
|
165
|
+
`entry_points → modules → platform → config`; boundaries are enforced by
|
|
166
|
+
[tach](https://docs.gauge.sh) in CI — not by convention. Each module exposes exactly one
|
|
167
|
+
public file (`api.py`) described by a `CONTRACT.md`.
|
|
168
|
+
|
|
169
|
+
See [docs/doc-index-architecture.md](docs/doc-index-architecture.md) for the design
|
|
170
|
+
rationale and [docs/architecture-rules.md](docs/architecture-rules.md) for the module rules.
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Issues and PRs welcome. Read the [architecture rules](docs/architecture-rules.md) and the
|
|
175
|
+
[ADRs](docs/adr/) first — module boundaries are machine-checked by [tach](https://docs.gauge.sh)
|
|
176
|
+
in CI, alongside lint, type-check, and a coverage gate. Every design deviation is recorded
|
|
177
|
+
as an ADR.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv sync # dev environment
|
|
181
|
+
uv run pytest --cov=docir --cov-fail-under=90 # tests + coverage gate
|
|
182
|
+
uv run ruff check . && uv run ty check && uv run tach check
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT © Sergei Konovalov
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "docir"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Doc-Index CLI — git-backed markdown documents with a derived SQLite/FTS5/semantic index, served by a warm local daemon."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Sergei Konovalov" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"typer>=0.15",
|
|
11
|
+
"rich>=13.9",
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
"python-frontmatter>=1.1",
|
|
14
|
+
"sqlalchemy>=2.0",
|
|
15
|
+
"alembic>=1.14",
|
|
16
|
+
"pydantic-settings>=2.7",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
# Real ONNX semantic embeddings. Kept optional so the default install and the
|
|
21
|
+
# test suite stay light and hermetic and fall back to the deterministic embedder.
|
|
22
|
+
embeddings = ["fastembed>=0.5"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
docir = "docir.entry_points.cli.app:main"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.3",
|
|
30
|
+
"pytest-cov>=6.0",
|
|
31
|
+
"ruff>=0.9",
|
|
32
|
+
"tach>=0.35.0",
|
|
33
|
+
"ty>=0.0.1a1",
|
|
34
|
+
"vulture>=2.14",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.11,<0.13"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[tool.uv.build-backend]
|
|
42
|
+
module-name = "docir"
|
|
43
|
+
module-root = "src"
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Ruff — linter + formatter (refined against docs.astral.sh/ruff)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py312"
|
|
51
|
+
src = ["src", "tests"]
|
|
52
|
+
extend-exclude = ["migrations", "src/docir/platform/persistence/alembic"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I", "N", "UP", "B", "C4", "SIM", "RUF"]
|
|
56
|
+
ignore = []
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
"**/__init__.py" = ["F401"]
|
|
60
|
+
"tests/**" = ["N802", "N803", "S101"]
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# ty — Astral type checker (refined against docs.astral.sh/ty)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
[tool.ty.environment]
|
|
66
|
+
python-version = "3.12"
|
|
67
|
+
root = ["./src"]
|
|
68
|
+
|
|
69
|
+
[tool.ty.src]
|
|
70
|
+
include = ["src"]
|
|
71
|
+
# The fastembed adapter imports an optional, not-installed dependency and is
|
|
72
|
+
# exercised only when the `embeddings` extra is present.
|
|
73
|
+
exclude = ["src/docir/platform/embedding/fastembed.py"]
|
|
74
|
+
|
|
75
|
+
[tool.ty.terminal]
|
|
76
|
+
output-format = "concise"
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Vulture — dead-code finder
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
[tool.vulture]
|
|
82
|
+
paths = ["src"]
|
|
83
|
+
min_confidence = 70
|
|
84
|
+
exclude = ["*/alembic/*"]
|
|
85
|
+
ignore_decorators = [
|
|
86
|
+
"@app.command",
|
|
87
|
+
"@app.callback",
|
|
88
|
+
"@tag_app.command",
|
|
89
|
+
"@daemon_app.command",
|
|
90
|
+
"@agent_app.command",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Pytest / coverage
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
testpaths = ["tests"]
|
|
98
|
+
addopts = "-ra"
|
|
99
|
+
markers = ["slow: end-to-end tests that spawn a real daemon subprocess"]
|
|
100
|
+
|
|
101
|
+
[tool.coverage.run]
|
|
102
|
+
source = ["docir"]
|
|
103
|
+
branch = true
|
|
104
|
+
omit = [
|
|
105
|
+
"*/alembic/*",
|
|
106
|
+
"*/platform/embedding/fastembed.py",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
[tool.coverage.report]
|
|
110
|
+
show_missing = true
|
|
111
|
+
skip_covered = false
|
|
112
|
+
exclude_lines = [
|
|
113
|
+
"pragma: no cover",
|
|
114
|
+
"if TYPE_CHECKING:",
|
|
115
|
+
"raise NotImplementedError",
|
|
116
|
+
"@abstractmethod",
|
|
117
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# docir — Doc-Index CLI.
|
|
2
|
+
#
|
|
3
|
+
# A git-backed markdown document system with a derived, read-optimized index.
|
|
4
|
+
# The markdown files under the docs root are the source of truth; the SQLite
|
|
5
|
+
# index (metadata + FTS5 + relation graph + semantic embeddings) is a
|
|
6
|
+
# rebuildable projection on top of them.
|
|
7
|
+
#
|
|
8
|
+
# The package is split into four Clean Architecture layers, each in its own
|
|
9
|
+
# sub-package with dependencies pointing strictly inward:
|
|
10
|
+
#
|
|
11
|
+
# presentation -> application -> domain
|
|
12
|
+
# infrastructure ------------^ (implements domain ports)
|
|
13
|
+
#
|
|
14
|
+
# * domain — enterprise rules: entities, value objects, ports
|
|
15
|
+
# (interfaces), domain services, errors. Depends on
|
|
16
|
+
# nothing else in the package.
|
|
17
|
+
# * application — use cases orchestrating the domain via its ports,
|
|
18
|
+
# plus the DTOs that cross the boundary.
|
|
19
|
+
# * infrastructure — concrete adapters implementing the domain ports
|
|
20
|
+
# (SQLAlchemy index, filesystem store, embedder,
|
|
21
|
+
# scheduler, daemon transport).
|
|
22
|
+
# * presentation — the Typer/Rich CLI and the composition root that
|
|
23
|
+
# wires infrastructure adapters into the use cases.
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
File without changes
|