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.
Files changed (73) hide show
  1. doc_lattice-1.0.0/.gitignore +23 -0
  2. doc_lattice-1.0.0/LICENSE +21 -0
  3. doc_lattice-1.0.0/PKG-INFO +430 -0
  4. doc_lattice-1.0.0/README.md +411 -0
  5. doc_lattice-1.0.0/pyproject.toml +94 -0
  6. doc_lattice-1.0.0/src/doc_lattice/__init__.py +3 -0
  7. doc_lattice-1.0.0/src/doc_lattice/cache.py +406 -0
  8. doc_lattice-1.0.0/src/doc_lattice/check.py +91 -0
  9. doc_lattice-1.0.0/src/doc_lattice/cli.py +692 -0
  10. doc_lattice-1.0.0/src/doc_lattice/config.py +141 -0
  11. doc_lattice-1.0.0/src/doc_lattice/constants.py +54 -0
  12. doc_lattice-1.0.0/src/doc_lattice/datetime_utils.py +12 -0
  13. doc_lattice-1.0.0/src/doc_lattice/discovery.py +145 -0
  14. doc_lattice-1.0.0/src/doc_lattice/error_types.py +51 -0
  15. doc_lattice-1.0.0/src/doc_lattice/frontmatter_parser.py +73 -0
  16. doc_lattice-1.0.0/src/doc_lattice/hashing.py +54 -0
  17. doc_lattice-1.0.0/src/doc_lattice/impact.py +105 -0
  18. doc_lattice-1.0.0/src/doc_lattice/linear_client.py +171 -0
  19. doc_lattice-1.0.0/src/doc_lattice/linear_fetch.py +45 -0
  20. doc_lattice-1.0.0/src/doc_lattice/linear_parser.py +92 -0
  21. doc_lattice-1.0.0/src/doc_lattice/linear_query.py +161 -0
  22. doc_lattice-1.0.0/src/doc_lattice/linear_render.py +85 -0
  23. doc_lattice-1.0.0/src/doc_lattice/lint.py +129 -0
  24. doc_lattice-1.0.0/src/doc_lattice/loader.py +201 -0
  25. doc_lattice-1.0.0/src/doc_lattice/model.py +181 -0
  26. doc_lattice-1.0.0/src/doc_lattice/orchestrate.py +79 -0
  27. doc_lattice-1.0.0/src/doc_lattice/path_utils.py +19 -0
  28. doc_lattice-1.0.0/src/doc_lattice/reconcile.py +173 -0
  29. doc_lattice-1.0.0/src/doc_lattice/render.py +152 -0
  30. doc_lattice-1.0.0/src/doc_lattice/report_render.py +81 -0
  31. doc_lattice-1.0.0/src/doc_lattice/resolve.py +70 -0
  32. doc_lattice-1.0.0/src/doc_lattice/scaffold.py +133 -0
  33. doc_lattice-1.0.0/src/doc_lattice/sections.py +214 -0
  34. doc_lattice-1.0.0/src/doc_lattice/stale_shipped.py +134 -0
  35. doc_lattice-1.0.0/src/doc_lattice/text_utils.py +24 -0
  36. doc_lattice-1.0.0/src/doc_lattice/tickets.py +66 -0
  37. doc_lattice-1.0.0/src/doc_lattice/version_check.py +115 -0
  38. doc_lattice-1.0.0/tests/conftest.py +51 -0
  39. doc_lattice-1.0.0/tests/fixtures/release-smoke/.doc-lattice.yml +6 -0
  40. doc_lattice-1.0.0/tests/fixtures/release-smoke/docs/seed.md +8 -0
  41. doc_lattice-1.0.0/tests/test_cache.py +747 -0
  42. doc_lattice-1.0.0/tests/test_check.py +178 -0
  43. doc_lattice-1.0.0/tests/test_cli.py +1383 -0
  44. doc_lattice-1.0.0/tests/test_config.py +268 -0
  45. doc_lattice-1.0.0/tests/test_constants.py +97 -0
  46. doc_lattice-1.0.0/tests/test_conventions.py +136 -0
  47. doc_lattice-1.0.0/tests/test_datetime_utils.py +15 -0
  48. doc_lattice-1.0.0/tests/test_discovery.py +195 -0
  49. doc_lattice-1.0.0/tests/test_error_types.py +73 -0
  50. doc_lattice-1.0.0/tests/test_frontmatter_parser.py +169 -0
  51. doc_lattice-1.0.0/tests/test_hashing.py +73 -0
  52. doc_lattice-1.0.0/tests/test_impact.py +288 -0
  53. doc_lattice-1.0.0/tests/test_linear_client.py +329 -0
  54. doc_lattice-1.0.0/tests/test_linear_fetch.py +147 -0
  55. doc_lattice-1.0.0/tests/test_linear_parser.py +239 -0
  56. doc_lattice-1.0.0/tests/test_linear_query.py +172 -0
  57. doc_lattice-1.0.0/tests/test_linear_render.py +207 -0
  58. doc_lattice-1.0.0/tests/test_lint.py +271 -0
  59. doc_lattice-1.0.0/tests/test_loader.py +421 -0
  60. doc_lattice-1.0.0/tests/test_model.py +157 -0
  61. doc_lattice-1.0.0/tests/test_orchestrate.py +167 -0
  62. doc_lattice-1.0.0/tests/test_package_metadata.py +174 -0
  63. doc_lattice-1.0.0/tests/test_path_utils.py +60 -0
  64. doc_lattice-1.0.0/tests/test_reconcile.py +374 -0
  65. doc_lattice-1.0.0/tests/test_render.py +233 -0
  66. doc_lattice-1.0.0/tests/test_report_render.py +121 -0
  67. doc_lattice-1.0.0/tests/test_resolve.py +101 -0
  68. doc_lattice-1.0.0/tests/test_scaffold.py +127 -0
  69. doc_lattice-1.0.0/tests/test_sections.py +255 -0
  70. doc_lattice-1.0.0/tests/test_stale_shipped.py +320 -0
  71. doc_lattice-1.0.0/tests/test_text_utils.py +47 -0
  72. doc_lattice-1.0.0/tests/test_tickets.py +133 -0
  73. 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).