doc-lattice 1.0.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.
- doc_lattice-1.0.0/.gitignore +23 -0
- doc_lattice-1.0.0/LICENSE +21 -0
- doc_lattice-1.0.0/PKG-INFO +430 -0
- doc_lattice-1.0.0/README.md +411 -0
- doc_lattice-1.0.0/pyproject.toml +94 -0
- doc_lattice-1.0.0/src/doc_lattice/__init__.py +3 -0
- doc_lattice-1.0.0/src/doc_lattice/cache.py +406 -0
- doc_lattice-1.0.0/src/doc_lattice/check.py +91 -0
- doc_lattice-1.0.0/src/doc_lattice/cli.py +692 -0
- doc_lattice-1.0.0/src/doc_lattice/config.py +141 -0
- doc_lattice-1.0.0/src/doc_lattice/constants.py +54 -0
- doc_lattice-1.0.0/src/doc_lattice/datetime_utils.py +12 -0
- doc_lattice-1.0.0/src/doc_lattice/discovery.py +145 -0
- doc_lattice-1.0.0/src/doc_lattice/error_types.py +51 -0
- doc_lattice-1.0.0/src/doc_lattice/frontmatter_parser.py +73 -0
- doc_lattice-1.0.0/src/doc_lattice/hashing.py +54 -0
- doc_lattice-1.0.0/src/doc_lattice/impact.py +105 -0
- doc_lattice-1.0.0/src/doc_lattice/linear_client.py +171 -0
- doc_lattice-1.0.0/src/doc_lattice/linear_fetch.py +45 -0
- doc_lattice-1.0.0/src/doc_lattice/linear_parser.py +92 -0
- doc_lattice-1.0.0/src/doc_lattice/linear_query.py +161 -0
- doc_lattice-1.0.0/src/doc_lattice/linear_render.py +85 -0
- doc_lattice-1.0.0/src/doc_lattice/lint.py +129 -0
- doc_lattice-1.0.0/src/doc_lattice/loader.py +201 -0
- doc_lattice-1.0.0/src/doc_lattice/model.py +181 -0
- doc_lattice-1.0.0/src/doc_lattice/orchestrate.py +79 -0
- doc_lattice-1.0.0/src/doc_lattice/path_utils.py +19 -0
- doc_lattice-1.0.0/src/doc_lattice/reconcile.py +173 -0
- doc_lattice-1.0.0/src/doc_lattice/render.py +152 -0
- doc_lattice-1.0.0/src/doc_lattice/report_render.py +81 -0
- doc_lattice-1.0.0/src/doc_lattice/resolve.py +70 -0
- doc_lattice-1.0.0/src/doc_lattice/scaffold.py +133 -0
- doc_lattice-1.0.0/src/doc_lattice/sections.py +214 -0
- doc_lattice-1.0.0/src/doc_lattice/stale_shipped.py +134 -0
- doc_lattice-1.0.0/src/doc_lattice/text_utils.py +24 -0
- doc_lattice-1.0.0/src/doc_lattice/tickets.py +66 -0
- doc_lattice-1.0.0/src/doc_lattice/version_check.py +115 -0
- doc_lattice-1.0.0/tests/conftest.py +51 -0
- doc_lattice-1.0.0/tests/fixtures/release-smoke/.doc-lattice.yml +6 -0
- doc_lattice-1.0.0/tests/fixtures/release-smoke/docs/seed.md +8 -0
- doc_lattice-1.0.0/tests/test_cache.py +747 -0
- doc_lattice-1.0.0/tests/test_check.py +178 -0
- doc_lattice-1.0.0/tests/test_cli.py +1383 -0
- doc_lattice-1.0.0/tests/test_config.py +268 -0
- doc_lattice-1.0.0/tests/test_constants.py +97 -0
- doc_lattice-1.0.0/tests/test_conventions.py +136 -0
- doc_lattice-1.0.0/tests/test_datetime_utils.py +15 -0
- doc_lattice-1.0.0/tests/test_discovery.py +195 -0
- doc_lattice-1.0.0/tests/test_error_types.py +73 -0
- doc_lattice-1.0.0/tests/test_frontmatter_parser.py +169 -0
- doc_lattice-1.0.0/tests/test_hashing.py +73 -0
- doc_lattice-1.0.0/tests/test_impact.py +288 -0
- doc_lattice-1.0.0/tests/test_linear_client.py +329 -0
- doc_lattice-1.0.0/tests/test_linear_fetch.py +147 -0
- doc_lattice-1.0.0/tests/test_linear_parser.py +239 -0
- doc_lattice-1.0.0/tests/test_linear_query.py +172 -0
- doc_lattice-1.0.0/tests/test_linear_render.py +207 -0
- doc_lattice-1.0.0/tests/test_lint.py +271 -0
- doc_lattice-1.0.0/tests/test_loader.py +421 -0
- doc_lattice-1.0.0/tests/test_model.py +157 -0
- doc_lattice-1.0.0/tests/test_orchestrate.py +167 -0
- doc_lattice-1.0.0/tests/test_package_metadata.py +174 -0
- doc_lattice-1.0.0/tests/test_path_utils.py +60 -0
- doc_lattice-1.0.0/tests/test_reconcile.py +374 -0
- doc_lattice-1.0.0/tests/test_render.py +233 -0
- doc_lattice-1.0.0/tests/test_report_render.py +121 -0
- doc_lattice-1.0.0/tests/test_resolve.py +101 -0
- doc_lattice-1.0.0/tests/test_scaffold.py +127 -0
- doc_lattice-1.0.0/tests/test_sections.py +255 -0
- doc_lattice-1.0.0/tests/test_stale_shipped.py +320 -0
- doc_lattice-1.0.0/tests/test_text_utils.py +47 -0
- doc_lattice-1.0.0/tests/test_tickets.py +133 -0
- doc_lattice-1.0.0/tests/test_version_check.py +252 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Claude Code / Codex working directories
|
|
2
|
+
.claude/
|
|
3
|
+
.codex
|
|
4
|
+
.worktrees/
|
|
5
|
+
.superpowers/
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
.venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.pyc
|
|
11
|
+
dist/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
# uv.lock and .python-version are intentionally committed (not ignored).
|
|
17
|
+
|
|
18
|
+
# Secrets
|
|
19
|
+
.env
|
|
20
|
+
|
|
21
|
+
# Scaffolding
|
|
22
|
+
.gx-new-version
|
|
23
|
+
setup-incomplete.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rick Passero
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doc-lattice
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Traceability engine for design and production documentation
|
|
5
|
+
Project-URL: Homepage, https://github.com/Guardantix/doc-lattice
|
|
6
|
+
Project-URL: Source, https://github.com/Guardantix/doc-lattice
|
|
7
|
+
Project-URL: Issues, https://github.com/Guardantix/doc-lattice/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Guardantix/doc-lattice/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Releases, https://github.com/Guardantix/doc-lattice/releases
|
|
10
|
+
Author-email: Rick Passero <24213466+riskandreason@users.noreply.github.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Requires-Dist: pydantic>=2
|
|
15
|
+
Requires-Dist: rich>=13
|
|
16
|
+
Requires-Dist: ruamel-yaml>=0.18
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# doc-lattice
|
|
21
|
+
|
|
22
|
+
A deterministic, offline traceability engine for design and production documentation.
|
|
23
|
+
|
|
24
|
+
doc-lattice tracks the dependencies *between* your markdown docs. When a downstream
|
|
25
|
+
document derives from an upstream one (a player-character spec built on the art direction,
|
|
26
|
+
an implementation plan built on a product brief), it records that link in frontmatter. When
|
|
27
|
+
the upstream changes, doc-lattice tells you exactly which downstream docs went stale, and a
|
|
28
|
+
CI gate keeps stale work from shipping silently.
|
|
29
|
+
|
|
30
|
+
It is pure tooling: no network (except the optional `linear` command), no secrets, no LLM,
|
|
31
|
+
no database. The dependency graph is derived from your docs on demand, never committed.
|
|
32
|
+
|
|
33
|
+
## The problem it solves
|
|
34
|
+
|
|
35
|
+
Design docs drift apart. Someone retunes the economy, edits the art direction, or rewrites
|
|
36
|
+
the core loop, and the dozen documents downstream of that decision keep citing the old
|
|
37
|
+
version. Nothing breaks loudly; the docs just quietly disagree, and the drift surfaces as a
|
|
38
|
+
bug, a re-do, or an argument weeks later.
|
|
39
|
+
|
|
40
|
+
doc-lattice makes those dependencies explicit and *checkable*. Each downstream doc declares
|
|
41
|
+
what it derives from and records a hash of what it last saw. A change upstream that the
|
|
42
|
+
downstream hasn't acknowledged is **drift**, and `check` fails CI on it until a human
|
|
43
|
+
consciously reconciles the link.
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
You annotate docs with two things:
|
|
48
|
+
|
|
49
|
+
- **Stable ids.** Every tracked file declares an `id` in its frontmatter. Sections are addressed
|
|
50
|
+
by their heading's GitHub slug by default; an explicit `{#anchor}` tag on the heading provides
|
|
51
|
+
a stable id independent of heading text. Section ids are file-scoped, so the same anchor in
|
|
52
|
+
two files does not collide with file ids or each other.
|
|
53
|
+
- **`derives_from` edges.** A downstream doc lists the upstream ids it depends on. Each edge
|
|
54
|
+
carries a `seen` hash: a fingerprint of the upstream content at the moment the dependency
|
|
55
|
+
was last reconciled.
|
|
56
|
+
|
|
57
|
+
From those annotations doc-lattice builds a **lattice**: an id-indexed graph of nodes
|
|
58
|
+
(your docs) and edges (the `derives_from` links). Every command reads from that one
|
|
59
|
+
structure. The `seen` hash is the load-bearing trick: comparing it against the upstream's
|
|
60
|
+
*current* content hash is what turns "these docs depend on each other" into "this dependency
|
|
61
|
+
is out of date."
|
|
62
|
+
|
|
63
|
+
### Drift states
|
|
64
|
+
|
|
65
|
+
`check` classifies every edge into one of four states:
|
|
66
|
+
|
|
67
|
+
| State | Meaning |
|
|
68
|
+
|-------|---------|
|
|
69
|
+
| **OK** | `seen` matches the upstream's current content. In sync. |
|
|
70
|
+
| **STALE** | The upstream changed since `seen` was locked. The downstream needs review. |
|
|
71
|
+
| **UNRECONCILED** | The edge has no `seen` yet. The dependency was declared but never acknowledged. |
|
|
72
|
+
| **BROKEN** | The ref points at an id that no longer exists. |
|
|
73
|
+
|
|
74
|
+
The content hash is `sha256` of a *canonicalized* copy of the text, truncated to 128 bits.
|
|
75
|
+
Canonicalization normalizes line endings, strips trailing whitespace per line, and trims
|
|
76
|
+
leading and trailing blank lines, so those cosmetic edits never trip drift. Internal
|
|
77
|
+
whitespace is preserved, so rewrapping a paragraph (which moves its line breaks) does count
|
|
78
|
+
as a change.
|
|
79
|
+
|
|
80
|
+
### A broken ref is a state, not a crash
|
|
81
|
+
|
|
82
|
+
The only thing that makes loading the lattice *fail* is a duplicate id, which makes the
|
|
83
|
+
index incoherent (exit 2). A ref that points at nothing is a normal, reportable lattice
|
|
84
|
+
state: `check` calls it BROKEN and exits 1. That is the core distinction the tool is built
|
|
85
|
+
on: **exit 1 means "the graph is coherent but drifting," exit 2 means "the index itself is
|
|
86
|
+
broken."**
|
|
87
|
+
|
|
88
|
+
### The authority ladder
|
|
89
|
+
|
|
90
|
+
Separately from drift, `lint` enforces a structural rule: authority only flows downhill.
|
|
91
|
+
Docs can declare an `authority` of `binding`, `derived`, or `exploratory`. A `derives_from`
|
|
92
|
+
edge from a more-authoritative doc to a less-authoritative one is an **inversion** (a binding
|
|
93
|
+
spec should not derive from an exploratory sketch), and `lint` fails on it. `lint` is pure
|
|
94
|
+
structure, independent of drift, and exits 1 on a violation just like `check`.
|
|
95
|
+
|
|
96
|
+
## A worked example
|
|
97
|
+
|
|
98
|
+
Two docs. The upstream owns a decision; the downstream depends on it.
|
|
99
|
+
|
|
100
|
+
`docs/art-direction.md`, the upstream:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
---
|
|
104
|
+
id: art-direction
|
|
105
|
+
layer: design
|
|
106
|
+
authority: binding
|
|
107
|
+
---
|
|
108
|
+
# Art Direction
|
|
109
|
+
|
|
110
|
+
## Accent Color {#accent}
|
|
111
|
+
Warm amber, used for every interactive highlight.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`docs/pc-design.md`, which derives from the accent decision:
|
|
115
|
+
|
|
116
|
+
```markdown
|
|
117
|
+
---
|
|
118
|
+
id: pc-design
|
|
119
|
+
layer: design
|
|
120
|
+
authority: derived
|
|
121
|
+
derives_from:
|
|
122
|
+
- ref: art-direction#accent
|
|
123
|
+
seen: 7f3a9c2e1b8d4f6a0c5e9d2b7a1f4e8c
|
|
124
|
+
tickets: [PC-228]
|
|
125
|
+
---
|
|
126
|
+
# Player Character Design
|
|
127
|
+
|
|
128
|
+
The PC's UI highlights use the accent color.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The ref `art-direction#accent` resolves file-scoped: it points at the section in the `art-direction`
|
|
132
|
+
file whose heading carries the `{#accent}` marker. Markers are optional; a heading with no marker is
|
|
133
|
+
addressed by its GitHub slug instead, and the `{#accent}` marker here pins a short stable id
|
|
134
|
+
regardless of the heading's wording. The `seen` hash records the accent text pc-design was last built
|
|
135
|
+
against.
|
|
136
|
+
|
|
137
|
+
Now someone changes the accent to "cool teal." The `{#accent}` section's content hash no
|
|
138
|
+
longer matches `seen`, so:
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
$ doc-lattice check
|
|
142
|
+
STALE pc-design -> art-direction#accent
|
|
143
|
+
|
|
144
|
+
$ doc-lattice impact art-direction#accent
|
|
145
|
+
pc-design (docs/pc-design.md) tickets: PC-228
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`check` exits 1, so CI is now red. A human reviews pc-design against the new accent, updates
|
|
149
|
+
the body if needed, and then locks in the new hash:
|
|
150
|
+
|
|
151
|
+
```console
|
|
152
|
+
$ doc-lattice reconcile pc-design
|
|
153
|
+
reconciled pc-design.md: art-direction#accent
|
|
154
|
+
|
|
155
|
+
$ doc-lattice check
|
|
156
|
+
OK pc-design -> art-direction#accent
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
That edit → `check` → review → `reconcile` loop is the whole workflow. `reconcile` is the
|
|
160
|
+
only command that writes to your docs, and it only ever rewrites the `seen` scalar.
|
|
161
|
+
|
|
162
|
+
## Quick start
|
|
163
|
+
|
|
164
|
+
### Prerequisites
|
|
165
|
+
|
|
166
|
+
- Python 3.13+
|
|
167
|
+
- [uv](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
|
|
168
|
+
|
|
169
|
+
### Install and run
|
|
170
|
+
|
|
171
|
+
Run the released CLI without installing it globally:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
uvx doc-lattice --help
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Or install it into an isolated tool environment:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv tool install doc-lattice
|
|
181
|
+
doc-lattice --help
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`pipx install doc-lattice` provides the same isolated installation. A conventional
|
|
185
|
+
`python -m pip install doc-lattice` is also supported when installing into an activated virtual
|
|
186
|
+
environment.
|
|
187
|
+
|
|
188
|
+
### Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv sync --group dev
|
|
192
|
+
uv run doc-lattice --help
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Test
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv run --group dev pytest # full suite (enforces coverage >= 80%)
|
|
199
|
+
uv run --group dev ruff check src tests
|
|
200
|
+
uv run --group dev ty check src
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Commands
|
|
204
|
+
|
|
205
|
+
| Command | What it does | Exits non-zero |
|
|
206
|
+
|---------|--------------|----------------|
|
|
207
|
+
| `check [--only STATE ...] [--format human\|json\|github]` | Classify every `derives_from` edge as OK / STALE / UNRECONCILED / BROKEN. | 1 on drift, 2 on tool error |
|
|
208
|
+
| `lint [--format human\|json\|github]` | Validate the authority ladder (binding > derived > exploratory) over the edges. | 1 on a violation, 2 on tool error |
|
|
209
|
+
| `impact TOKEN [--depth N]` | List every downstream doc affected by a change to TOKEN; `--depth N` bounds the walk to N hops. | 2 on tool error |
|
|
210
|
+
| `reconcile [ID] [--ref REF] [--all] [--dry-run]` | Set `seen` to current upstream hashes for the selected edges (the only command that mutates your tracked docs); `--dry-run` previews the plan without writing. | 2 on tool error |
|
|
211
|
+
| `graph [--format mermaid\|dot\|json]` | Emit the edge graph as Mermaid, DOT, or JSON. | 2 on tool error (including an unrecognized `--format`) |
|
|
212
|
+
| `linear [TARGET] [--from ID] [--exit-code] [--warn-exit]` | Report tickets shipped against a spec that has since drifted (needs `LINEAR_API_KEY`). | 1 with `--exit-code` on DANGER/BLOCKED (or WARNING too under `--warn-exit`), 2 on tool error |
|
|
213
|
+
| `init [--docs-root ...] [--linear-team KEY]` | Scaffold `.doc-lattice.yml` and print pre-commit and CI codegen. | 2 on tool error |
|
|
214
|
+
|
|
215
|
+
Only `check` and `lint` gate by default, exiting 1 when they find drift or an authority inversion.
|
|
216
|
+
`impact`, `reconcile`, `graph`, and `init` are informational and always exit 0 on success (2 only on
|
|
217
|
+
a tool error), so wiring `impact` into a CI gate never turns the build red. `linear` also exits 0 by
|
|
218
|
+
default; pass `--exit-code` to gate on any DANGER or BLOCKED finding, and add `--warn-exit` to gate on
|
|
219
|
+
WARNING as well.
|
|
220
|
+
|
|
221
|
+
Every command except `init` accepts `--config PATH` (path to `.doc-lattice.yml`; defaults to
|
|
222
|
+
the file in the current directory). `check`, `lint`, `impact`, `reconcile`, and `linear` accept
|
|
223
|
+
`--json` for machine-readable output. Run `uv run doc-lattice <command> --help` for the full
|
|
224
|
+
flag list.
|
|
225
|
+
|
|
226
|
+
Pass `--indent N` with JSON output on `check`, `lint`, `impact`, or `linear` to pretty-print the
|
|
227
|
+
JSON with `N` spaces per level. JSON output is selected by `--json`, or the equivalent
|
|
228
|
+
`--format json` on `check` and `lint`; `--indent` without JSON output is a usage error.
|
|
229
|
+
|
|
230
|
+
Use the global `--no-color` option before the command to disable colored output explicitly, for
|
|
231
|
+
example `doc-lattice --no-color check`. Rich also honors the [`NO_COLOR`](https://no-color.org/)
|
|
232
|
+
environment variable; `--no-color` is the command-line equivalent. Either one also strips the
|
|
233
|
+
styling from help and usage-error text even when a terminal-forcing variable is set.
|
|
234
|
+
|
|
235
|
+
`check` and `lint` also accept `--format human|json|github`. `human` is the default, and `json`
|
|
236
|
+
is equivalent to the existing `--json` alias. `github` emits one escaped GitHub Actions `::error`
|
|
237
|
+
workflow command per drift finding or ladder violation, each with a repo-relative file path, so
|
|
238
|
+
findings attach inline to the offending doc in the pull-request diff. Output selection never
|
|
239
|
+
changes gate exit codes. Do not combine `--json` with `--format github`.
|
|
240
|
+
|
|
241
|
+
`impact` walks the full transitive closure by default. Pass `--depth N` (N >= 1) to bound the
|
|
242
|
+
walk to N hops from TOKEN: `--depth 1` lists only the docs that derive directly from it. Human
|
|
243
|
+
output is unchanged, and each `--json` entry gains a `"depth"` field carrying the minimum number
|
|
244
|
+
of hops at which that doc is reached.
|
|
245
|
+
|
|
246
|
+
`check` accepts a repeatable `--only STATE` to narrow the display to specific states (case
|
|
247
|
+
insensitive, e.g. `--only stale --only broken`); an unrecognized state exits 2 and names the
|
|
248
|
+
valid set. Filtering is display-only: the exit code always reflects every edge, so `check --only
|
|
249
|
+
OK` on a drifting lattice still exits 1.
|
|
250
|
+
|
|
251
|
+
### `reconcile` selectors
|
|
252
|
+
|
|
253
|
+
`reconcile` needs either a downstream id or `--all` (running it with neither is an error):
|
|
254
|
+
|
|
255
|
+
- **`reconcile DOWNSTREAM_ID`**: reconcile every drifting edge of one downstream node.
|
|
256
|
+
- **`reconcile DOWNSTREAM_ID --ref REF`**: narrow to a single upstream ref on that node, selected
|
|
257
|
+
by resolved identity; refused if it targets a BROKEN edge.
|
|
258
|
+
- **`reconcile --all`**: clear every STALE/UNRECONCILED edge in the lattice. Skips BROKEN and
|
|
259
|
+
already-OK edges, and skips a node's broken edge rather than failing the node, so one dangling
|
|
260
|
+
ref never blocks the rest.
|
|
261
|
+
|
|
262
|
+
`reconcile` re-reads each downstream file fresh at write time, rewrites only the targeted `seen`
|
|
263
|
+
scalar through round-trip YAML (preserving your body, key order, and comments), and writes
|
|
264
|
+
atomically, so a concurrent edit is never clobbered.
|
|
265
|
+
|
|
266
|
+
Add `--dry-run` to any of the selectors above to preview the plan without writing: it prints
|
|
267
|
+
`would reconcile FILE: REF` per edge that would change (`nothing to reconcile` if none would),
|
|
268
|
+
and leaves every file byte-identical. Combine with `--json` for a machine-readable plan:
|
|
269
|
+
`{"dry_run": true, "reconciled": [{"path": ..., "ref": ..., "new_seen": ...}]}`, sorted by path
|
|
270
|
+
then ref. A real run with `--json` emits the same shape with `"dry_run": false`, after the
|
|
271
|
+
writes complete.
|
|
272
|
+
|
|
273
|
+
## Frontmatter reference
|
|
274
|
+
|
|
275
|
+
| Key | Where | Meaning |
|
|
276
|
+
|-----|-------|---------|
|
|
277
|
+
| `id` | every tracked file | The file's stable id. Required. |
|
|
278
|
+
| `title` | optional | Display title. |
|
|
279
|
+
| `layer` | optional | `design`, `technical`, or `production`. |
|
|
280
|
+
| `authority` | optional | `binding`, `derived`, or `exploratory`. Ranked by `lint`. |
|
|
281
|
+
| `derives_from` | downstream files | List of `{ ref, seen }` edges. |
|
|
282
|
+
| `derives_from[].ref` | each edge | The upstream id: bare (whole-file target, e.g. `art-direction`) or file-scoped (section target, e.g. `art-direction#accent`). |
|
|
283
|
+
| `derives_from[].seen` | each edge | The locked upstream hash, or omitted for a never-reconciled (UNRECONCILED) edge. |
|
|
284
|
+
| `tickets` | optional | Issue ids associated with the doc (used by `impact` and `linear`). |
|
|
285
|
+
|
|
286
|
+
Section ids are optional: a heading is addressed by its GitHub slug by default (e.g. `## Accent Color`
|
|
287
|
+
resolves to `accent-color`), and an explicit `{#anchor}` marker on the heading wins as an escape hatch for
|
|
288
|
+
a stable id independent of heading text (e.g. `## Accent Color {#accent-hue}`). Section refs are
|
|
289
|
+
file-scoped (`file#anchor`), so the same anchor in two files does not collide.
|
|
290
|
+
|
|
291
|
+
## Configuration
|
|
292
|
+
|
|
293
|
+
doc-lattice runs zero-config (defaulting to a `docs/` root), or reads `.doc-lattice.yml`
|
|
294
|
+
from the current directory:
|
|
295
|
+
|
|
296
|
+
```yaml
|
|
297
|
+
# doc-lattice configuration
|
|
298
|
+
docs_roots:
|
|
299
|
+
- docs # roots to scan for tracked .md files (default: ["docs"])
|
|
300
|
+
# ignore_globs: # paths to skip within those roots
|
|
301
|
+
# - "**/superpowers/plans/**"
|
|
302
|
+
# cache_key: my-docs # opt-in incremental load cache slot (see Load cache below)
|
|
303
|
+
# cache_trust_stat: false # opt-in stat fast tier for read-only commands (accepts the mtime caveat)
|
|
304
|
+
# linear_team: ENG # the Linear team the `linear` query targets
|
|
305
|
+
# binding_layers: null # accepted but inert today; setting it changes nothing (see below)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
All `docs_roots` must resolve inside the project root; an entry that escapes via `..`, an
|
|
309
|
+
absolute path, or a symlink is rejected before any read.
|
|
310
|
+
|
|
311
|
+
`binding_layers` is accepted in the config for forward compatibility but is inert today: setting it
|
|
312
|
+
changes nothing, because no command consults it. Authority ranking currently lives entirely in
|
|
313
|
+
`lint` (binding > derived > exploratory); see the
|
|
314
|
+
[lint design spec](docs/superpowers/specs/2026-06-28-doc-lattice-lint-design.md) for where that
|
|
315
|
+
ranking is defined.
|
|
316
|
+
|
|
317
|
+
### Load cache (opt-in)
|
|
318
|
+
|
|
319
|
+
Large doc sets (thousands of files) can skip re-parsing unchanged docs with an opt-in cache.
|
|
320
|
+
Set `cache_key` to a single safe segment (`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`); it names a slot
|
|
321
|
+
under your user cache home at `<cache_home>/doc-lattice/<cache_key>/load-cache.json`, where
|
|
322
|
+
`<cache_home>` is `$XDG_CACHE_HOME` (when absolute) or `~/.cache`. The cache lives outside every
|
|
323
|
+
checkout on purpose: because `.doc-lattice.yml` is committed, every clone and git worktree of the
|
|
324
|
+
project shares one warm cache with no per-checkout setup, which an in-repo cache could not do.
|
|
325
|
+
|
|
326
|
+
By default the cache re-reads and re-hashes each file's bytes every run, so its output is always
|
|
327
|
+
byte-identical to an uncached run under any cache state (cold, warm, stale, structurally corrupt, or
|
|
328
|
+
wrong version); only timing differs. A structurally corrupt cache (unreadable, non-JSON, wrong
|
|
329
|
+
version, or schema-invalid) is discarded wholesale and rebuilt; the cache is a trusted single-writer
|
|
330
|
+
file under your own cache home, so it is not hardened against hand-edited tampering that stays
|
|
331
|
+
schema-valid. Setting `cache_trust_stat: true` adds a faster tier for read-only commands that trusts
|
|
332
|
+
a file whose size and modification time are unchanged, accepting that the file is not opened at all:
|
|
333
|
+
a rewrite that preserves both its size and its nanosecond mtime is served stale, and a file made
|
|
334
|
+
unreadable (for example a permissions change, which does not alter size or mtime) is served from
|
|
335
|
+
cache instead of erroring, each until the file is touched. `reconcile` ignores `cache_trust_stat`
|
|
336
|
+
and always verifies content, so it can never
|
|
337
|
+
write frontmatter from stale data. Two projects sharing a `cache_key` stay correct (a content-hash
|
|
338
|
+
hit implies identical bytes); the only cost is overwrite churn, so prefer distinct keys. Delete the
|
|
339
|
+
cache directory to reset it; a tool-version bump discards it automatically.
|
|
340
|
+
|
|
341
|
+
## Adopting doc-lattice in your docs repo
|
|
342
|
+
|
|
343
|
+
Bootstrap config and the drift and authority-ladder gates for a repo whose docs you want to
|
|
344
|
+
track:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
uvx --python 3.13 --from doc-lattice==1.0.0 doc-lattice init
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
This writes `.doc-lattice.yml` (only if absent) and prints pre-commit hooks and a GitHub
|
|
351
|
+
Actions workflow that run `doc-lattice check` (drift) and `doc-lattice lint` (authority
|
|
352
|
+
ladder) as your gates. Paste each where the output says. Pass `--docs-root` (repeatable) or
|
|
353
|
+
`--linear-team` to bake those values into the generated config.
|
|
354
|
+
|
|
355
|
+
To test an unreleased commit, replace the PyPI requirement with a Git source such as
|
|
356
|
+
`--from git+https://github.com/Guardantix/doc-lattice@<commit>`; released configurations should
|
|
357
|
+
keep the exact PyPI version pin.
|
|
358
|
+
|
|
359
|
+
## Linear integration
|
|
360
|
+
|
|
361
|
+
`doc-lattice linear` is the only network-touching command. It builds a trigger map from the
|
|
362
|
+
loaded lattice, then fetches live ticket status over the Linear GraphQL API to report tickets
|
|
363
|
+
that shipped against a spec that has since drifted. It reads `LINEAR_API_KEY` from the
|
|
364
|
+
environment (export it before running; the error points you to `impact` for the offline view),
|
|
365
|
+
and the client is https-only, redirect-refusing, size-capped, and SSRF-hardened. A transient
|
|
366
|
+
HTTP 429 or 5xx is retried up to three times with a short backoff (honoring `Retry-After` when
|
|
367
|
+
present, capped) before failing, so a passing rate limit does not fail a CI run. Set the team
|
|
368
|
+
the query targets with `linear_team` in `.doc-lattice.yml`, or pass `--linear-team` to `init`.
|
|
369
|
+
Every other command runs fully offline.
|
|
370
|
+
|
|
371
|
+
## Exit codes
|
|
372
|
+
|
|
373
|
+
| Code | Meaning |
|
|
374
|
+
|------|---------|
|
|
375
|
+
| `0` | Success; no drift or violations. |
|
|
376
|
+
| `1` | The lattice is coherent but a gate failed: drift (`check`), an authority inversion (`lint`), or (with `--exit-code`) a DANGER/BLOCKED `linear` finding. |
|
|
377
|
+
| `2` | Tool error: the index is incoherent (e.g. a duplicate id), config is invalid, or a path escapes the project root. |
|
|
378
|
+
|
|
379
|
+
## Troubleshooting
|
|
380
|
+
|
|
381
|
+
**`LINEAR_API_KEY is not set`.** Only the `linear` command needs a key. Export a Linear API key
|
|
382
|
+
(`export LINEAR_API_KEY=lin_api_...`) before running `linear`, or run `impact` instead: `impact` is
|
|
383
|
+
the fully offline view of the same downstream reach and needs no key.
|
|
384
|
+
|
|
385
|
+
**Linear returns HTTP 429 or 5xx.** These are transient. The client already retries a bounded
|
|
386
|
+
number of times with a short backoff (honoring `Retry-After` when present, capped), so a passing
|
|
387
|
+
rate limit does not fail the run on its own. If it still fails after those retries, the error tells
|
|
388
|
+
you to wait and re-run; `impact` stays available offline in the meantime.
|
|
389
|
+
|
|
390
|
+
**A `linear` finding is BLOCKED `not-found`.** A ticket the Linear filter does not return is treated
|
|
391
|
+
as absence, not an error: it grades as a BLOCKED `not-found` finding rather than crashing the
|
|
392
|
+
command. Confirm the ticket id exists and that `linear_team` targets the right team.
|
|
393
|
+
|
|
394
|
+
**`duplicate id ...` exits 2.** A duplicate id makes the index incoherent, so loading the lattice
|
|
395
|
+
fails with exit 2 (a tool error, distinct from the exit 1 that `check` and `lint` use for drift).
|
|
396
|
+
The message names both registration sites so you can find the clash: either two files share an `id`,
|
|
397
|
+
or a heading's `{#marker}` collides with another heading's computed slug in the same file.
|
|
398
|
+
|
|
399
|
+
## Documentation
|
|
400
|
+
|
|
401
|
+
| Document | Purpose |
|
|
402
|
+
|----------|---------|
|
|
403
|
+
| [ARCHITECTURE.md](ARCHITECTURE.md) | System design and the decision log |
|
|
404
|
+
| [CLAUDE.md](CLAUDE.md) | Architecture map and tooling-enforced invariants |
|
|
405
|
+
| [roadmap.md](roadmap.md) | Shipped slices and what is deferred |
|
|
406
|
+
| [CHANGELOG.md](CHANGELOG.md) | Release history |
|
|
407
|
+
| [RELEASING.md](RELEASING.md) | Release checklist and version-tag procedure |
|
|
408
|
+
| [build-log.md](build-log.md) | Development timeline |
|
|
409
|
+
| [docs/superpowers/specs/](docs/superpowers/specs/) | The binding design specs (source of truth) |
|
|
410
|
+
|
|
411
|
+
## Project structure
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
doc-lattice/
|
|
415
|
+
├── src/doc_lattice/ # the engine: a pure graph/report core behind a thin impure shell
|
|
416
|
+
├── tests/ # test suite (mirrors sources; property-based hashing invariants)
|
|
417
|
+
├── scripts/ # CI guards (typing boundary, version sync)
|
|
418
|
+
├── docs/superpowers/ # design specs and plans
|
|
419
|
+
└── pyproject.toml # project configuration
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The engine is a pure pipeline (`config -> discovery -> frontmatter parse -> build_lattice`
|
|
423
|
+
feeding `{ check, impact, reconcile, graph, lint, linear }`) where all graph and report logic
|
|
424
|
+
is filesystem-free. Only `config`, `discovery`, `orchestrate`, and `cli` touch the disk, and
|
|
425
|
+
only `linear_client` touches the network. See [ARCHITECTURE.md](ARCHITECTURE.md) for the
|
|
426
|
+
decisions behind that split.
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
MIT. See [LICENSE](LICENSE).
|