constant-docs 0.7.0__tar.gz → 0.8.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 (33) hide show
  1. {constant_docs-0.7.0 → constant_docs-0.8.0}/PKG-INFO +64 -55
  2. {constant_docs-0.7.0 → constant_docs-0.8.0}/README.md +63 -54
  3. {constant_docs-0.7.0 → constant_docs-0.8.0}/pyproject.toml +1 -1
  4. {constant_docs-0.7.0 → constant_docs-0.8.0}/pyproject.toml.orig +1 -1
  5. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/api.py +86 -11
  6. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/cli.py +33 -12
  7. {constant_docs-0.7.0 → constant_docs-0.8.0}/LICENSE +0 -0
  8. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/__init__.py +0 -0
  9. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/__main__.py +0 -0
  10. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/auto.py +0 -0
  11. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/checks.py +0 -0
  12. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/completeness.py +0 -0
  13. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/config.py +0 -0
  14. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/coverage.py +0 -0
  15. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/decisions.py +0 -0
  16. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/document.py +0 -0
  17. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/fingerprint.py +0 -0
  18. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/globs.py +0 -0
  19. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/guides/quickstart.md +0 -0
  20. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/guides/readme.md +0 -0
  21. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/house-style.md +0 -0
  22. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/index.py +0 -0
  23. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/kinds.py +0 -0
  24. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/paths.py +0 -0
  25. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/architecture.md +0 -0
  26. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/cli-reference.md +0 -0
  27. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/config-reference.md +0 -0
  28. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/errors.md +0 -0
  29. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/log.md +0 -0
  30. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/module.md +0 -0
  31. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/prompts/spec.md +0 -0
  32. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/state.py +0 -0
  33. {constant_docs-0.7.0 → constant_docs-0.8.0}/src/constant_docs/vcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: constant-docs
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Self-maintaining documentation for agentic codebases
5
5
  Keywords: documentation,docs,staleness,drift,ci,codegen,agents,llm
6
6
  Author: Ashborn Systems
@@ -51,11 +51,11 @@ constant_docs:
51
51
  - src/constant_docs/api.py
52
52
  - src/constant_docs/cli.py
53
53
  - src/constant_docs/kinds.py
54
- source_hash: sha256:99017f4b3440779659af6e13a51d12da0e37146fd449a934a42751f6f6f8d50b
54
+ source_hash: sha256:831cdb38a60c83171b114148aff40e507c1feb9c94600e9404de748e293f9b21
55
55
  hash_method: sha256-over-sorted-path-and-content
56
56
  hash_covers: source_files
57
- timestamp: '2026-09-05T13:18:40Z'
58
- generator: constant-docs/0.7.0
57
+ timestamp: '2026-09-05T16:46:40Z'
58
+ generator: constant-docs/0.8.0
59
59
  generator_spec: https://github.com/ashborn-systems/constant-docs/blob/main/SPEC.md
60
60
  -->
61
61
 
@@ -94,8 +94,9 @@ Your coding agent writes every word, because it already holds the source and a
94
94
  model connection. constant-docs names the documents that moved and takes the
95
95
  new text back.
96
96
 
97
- That one command is also the whole CI integration. It exits 1 on drift, so a
98
- change that alters behaviour and leaves its document behind does not merge:
97
+ That one command is also the whole continuous-integration check. It exits 1 on
98
+ drift, so a change that alters behaviour and leaves its document behind does
99
+ not merge:
99
100
 
100
101
  ```yaml
101
102
  - run: constant-docs verify
@@ -168,12 +169,12 @@ marks say. `verify` hashes everything and exits 1 on drift. Keep `verify` as
168
169
  the gate.
169
170
 
170
171
  Claude Code gets a plugin. Install it and two hooks run those commands for you:
171
- `PostToolUse` runs `mark`, `Stop` runs `settle --hook`. Edit a file, stop the
172
- turn, and its document is up to date. Any harness that can run a command on
173
- write and at turn end does the same job.
172
+ `PostToolUse` runs `mark` after each edit, and `Stop` runs `settle --hook` when
173
+ the turn ends. Edit a file, stop the turn, and its document is up to date. Any
174
+ harness that can run a command on write and at turn end does the same job.
174
175
 
175
- Regeneration happens at quiescence, never mid-edit, so the generator and the
176
- coding loop never write the same file at once. The dirty set lives in
176
+ Regeneration happens at the end of a turn, never mid-edit, so the generator and
177
+ the coding loop never write the same file at once. The dirty set lives in
177
178
  `.constant-docs/dirty.json`, which is gitignored, so a crashed session is
178
179
  picked up on the next run.
179
180
 
@@ -181,9 +182,9 @@ The [harness guide](guides/harness-integration.md) walks through each route.
181
182
 
182
183
  ## Kinds
183
184
 
184
- A document is not always a module explainer. Its kind decides which sections it
185
- must carry, which prompt its generator is handed, and whether a regeneration
186
- replaces the body or appends to it.
185
+ Every document has a kind. The kind decides which sections its body must carry,
186
+ which prompt its generator is handed, and whether a regeneration replaces the
187
+ body or appends an entry.
187
188
 
188
189
  | Kind | For | Mode |
189
190
  |---|---|---|
@@ -191,7 +192,7 @@ replaces the body or appends to it.
191
192
  | `spec` | What a subsystem promises, and why | replace |
192
193
  | `log` | A changelog or build log | **append** |
193
194
  | `architecture` | The module map, with a diagram | replace |
194
- | `errors` | Every message the tool can emit | replace |
195
+ | `errors` | Every message the covered code can raise | replace |
195
196
  | `config-reference` | Every configuration key | replace |
196
197
  | `cli-reference` | Every command and flag | replace |
197
198
 
@@ -212,8 +213,9 @@ An append never rewrites or reorders what is already there. Pass a whole body
212
213
  to a log, or a single entry to a module document, and the tool refuses it. A
213
214
  configuration that has never heard of kinds behaves exactly as it did.
214
215
 
215
- One house style governs every generated document. It lives in a single file
216
- that all seven built-in prompts receive, so the rules cannot drift apart.
216
+ One house style governs every document written from a built-in prompt. It lives
217
+ in a single file that all seven built-in prompts receive, so the rules cannot
218
+ drift apart.
217
219
 
218
220
  ## Finding what is undocumented
219
221
 
@@ -223,17 +225,18 @@ constant-docs coverage # source no module covers, by directory
223
225
  constant-docs completeness # kinds of document the contents warrant
224
226
  ```
225
227
 
226
- `verify` can only check what has been declared, so a repository could otherwise
227
- drift to a large fraction undocumented while every check passed. `coverage`
228
- does one thing: a set difference. Declare the directories that should carry no
229
- document in the configuration, with a reason each. An exclusion nobody
230
- justified is one nobody decided.
228
+ `verify` can only check what has been declared, so a repository can drift a
229
+ long way undocumented while every check passes. `coverage` does one thing: a
230
+ set difference. It reports the files no declared glob matches and no exclusion
231
+ names. Declare the directories that should carry no document in the
232
+ configuration, with a reason each. An exclusion nobody justified is one nobody
233
+ decided.
231
234
 
232
235
  Coverage answers whether a file has a document. It cannot answer whether the
233
- set of documents is complete: a document that was never written cannot drift,
234
- and a module document covers its files, so a repository with a document per
235
- module and no specification passes everything. `completeness` proposes the
236
- kinds your contents warrant, each with the fact behind it:
236
+ set of documents is complete. A document that was never written cannot drift,
237
+ and a repository with a document per module and no specification passes every
238
+ coverage check. `completeness` proposes the kinds your contents warrant, each
239
+ with the fact behind it:
237
240
 
238
241
  ```
239
242
  Warranted and not written (2):
@@ -247,11 +250,11 @@ Warranted and not written (2):
247
250
  It proposes from evidence in the repository, such as a console script or an
248
251
  exception hierarchy. The existence of a kind is not itself evidence. A library
249
252
  with no command line is not offered a CLI reference, because a document nobody
250
- needs still has to be kept true and still refuses a commit when it drifts.
253
+ needs still has to be kept true and still fails the build when it drifts.
251
254
  Nothing warrants a changelog: every repository could keep one, so the signal
252
255
  fires everywhere and says nothing.
253
256
 
254
- Write the document, or decline the kind under `unwarranted:` with a reason and
257
+ Write the document, or decline the kind under `unwarranted:` with a reason, and
255
258
  it stops being proposed.
256
259
 
257
260
  `verify --coverage` and `verify --completeness` fold the two into the gate,
@@ -261,13 +264,14 @@ document, and `prune` deletes it.
261
264
  ## Files it will not touch
262
265
 
263
266
  Pointing the tool at a `docs/` folder the repository already has is safe. Every
264
- document this tool writes carries a block naming the tool, and, when you
265
- declare a `project` id, the project that wrote it. A markdown file without that
266
- block belongs to somebody else, and three commands act on the difference:
267
+ document this tool writes carries a block naming the tool and, when you declare
268
+ a `project` id, the project that wrote it. A markdown file without that block
269
+ belongs to somebody else, and three commands act on the difference:
267
270
 
268
271
  - `apply` refuses to write over one. It names the file and changes no byte
269
272
  - `prune` refuses to delete one, and names every file it left alone
270
- - `verify` lists them under a heading of their own, and they fail nothing
273
+ - `verify` lists them under a heading of their own, and none of them fails the
274
+ check
271
275
 
272
276
  Take one over when you want it maintained:
273
277
 
@@ -288,7 +292,7 @@ rewrite it. Every body your agent generates still carries those headings, and
288
292
  `apply` checks before it writes a byte.
289
293
 
290
294
  A glob that matches no file gets the same restraint. `verify` names the module
291
- and exits 1, `apply` refuses to record a digest over nothing, and the document
295
+ and exits 1, `apply` refuses to record a hash over nothing, and the document
292
296
  stays where it is. A mistyped glob costs you a failing check and nothing else.
293
297
 
294
298
  ## Documents outside the repository
@@ -309,12 +313,14 @@ real directory on this machine: where to open the file.
309
313
 
310
314
  The variable must resolve when the configuration loads. Unset, empty, relative,
311
315
  or naming a directory that is not there, each refuses the configuration and
312
- says which. A store landing inside `.ssh`, `Secret/`, a cache or any other
313
- directory on the deny list is refused too. The tool declines to read those
314
- directories, so it declines to write documents into them. Plain `${NAME}` is
315
- the whole syntax. A default value would let a machine without the vault write
316
- documents into the repository and report success. The Stop hook blocks on the
317
- same failure, so a missing variable cannot switch the gate off quietly.
316
+ says which. A store landing inside `.ssh`, `Secret/`, a cache, or any other
317
+ directory on the deny list is refused too: the tool declines to read those
318
+ directories, so it declines to write documents into them.
319
+
320
+ Plain `${NAME}` is the whole syntax. A default value would let a machine
321
+ without the vault write documents into the repository and report success. The
322
+ Stop hook blocks on the same failure, so a missing variable cannot switch the
323
+ gate off quietly.
318
324
 
319
325
  Deletion changes as well. A repository's documents ride the branch, and `git
320
326
  checkout` brings one back. A store stands still while the checkout moves, so
@@ -334,12 +340,14 @@ auto:
334
340
  budget: 5
335
341
  ```
336
342
 
337
- The tool still calls no model. constant-docs splits your command with `shlex`
338
- and runs it directly, so an agent CLI, a script and a Makefile target all work.
339
- No credential goes in it. The command's success is checked: `auto` re-runs
340
- `verify` afterwards. It exits 0 when everything is clean, 1 when something is
341
- still stale, and 2 when the command could not run or exited non-zero. Three
342
- outcomes, because a scheduler that confuses the last two retries the wrong one.
343
+ constant-docs itself calls no model. It splits your command with `shlex` and
344
+ runs it directly, so an agent CLI, a script, and a Makefile target all work. No
345
+ credential goes in it. The command's success is checked: `auto` re-runs
346
+ `verify` afterwards.
347
+
348
+ It exits 0 when everything is clean, 1 when something is still stale, and 2
349
+ when the command could not run or exited non-zero. Three outcomes, because a
350
+ scheduler that confuses the last two retries the wrong one.
343
351
 
344
352
  ## Three things it checks that a hash cannot
345
353
 
@@ -354,9 +362,10 @@ document, with a date.
354
362
  in those modules' files by name, so it goes stale when their boundary moves.
355
363
 
356
364
  **An error catalogue agrees with the source, both ways.** Every message the
357
- code raises appears in the catalogue, and every entry still exists in the code.
358
- Neither needs parsing beyond a string scan, so it costs nothing, and it is the
359
- one document here the tool can actually guarantee. The rest are only fresh.
365
+ code raises appears in the catalogue, and every entry listed is still raised in
366
+ the code. The check needs no parsing beyond a string scan, so it costs nothing,
367
+ and it is the one document here the tool can guarantee. The rest are only
368
+ fresh.
360
369
 
361
370
  ## What a document looks like
362
371
 
@@ -381,7 +390,7 @@ constant_docs:
381
390
  hash_method: sha256-over-sorted-path-and-content
382
391
  hash_covers: source_files
383
392
  timestamp: '2026-08-19T09:14:00Z'
384
- generator: constant-docs/0.7.0
393
+ generator: constant-docs/0.8.0
385
394
  generator_spec: https://github.com/ashborn-systems/constant-docs/blob/main/SPEC.md
386
395
  ---
387
396
  ## Purpose
@@ -404,7 +413,7 @@ report records that it happened.
404
413
 
405
414
  The frontmatter stands alone. An agent that has never run this tool can see
406
415
  what the document describes, where its source lives, and how to recompute the
407
- digest. `project` answers the question asked before every overwrite and every
416
+ hash. `project` answers the question asked before every overwrite and every
408
417
  deletion: whose document is this. The tool keeps everything it owns under one
409
418
  `constant_docs` key, an Open Knowledge Format producer extension. `type`,
410
419
  `title`, `tags` and the two dates stay yours, and unknown keys survive a write.
@@ -424,11 +433,11 @@ prune() # deletes orphans; under a store, lists them
424
433
 
425
434
  ## What it does not do
426
435
 
427
- A module names a glob, and the tool digests the bytes of the files that glob
428
- matches. Nothing in that step knows a language, which is what makes yours work:
429
- no model client, no network call, no AST, no import graph. The cost is that any
430
- byte moves the hash. Fixing a typo in a comment reports the module stale, and
431
- your agent rewrites a document the edit never touched.
436
+ A module names a glob, and the tool hashes the bytes of the files that glob
437
+ matches. Nothing in that step knows a language, which is what makes it work on
438
+ yours: no model client, no network call, no syntax tree, no import graph. The
439
+ cost is that any byte moves the hash. Fixing a typo in a comment reports the
440
+ module stale, and your agent rewrites a document the edit never touched.
432
441
 
433
442
  It watches files and nothing else. A document quoting a queue depth or a
434
443
  production URL stays fresh for ever, because nobody edited anything. Hash what
@@ -22,11 +22,11 @@ constant_docs:
22
22
  - src/constant_docs/api.py
23
23
  - src/constant_docs/cli.py
24
24
  - src/constant_docs/kinds.py
25
- source_hash: sha256:99017f4b3440779659af6e13a51d12da0e37146fd449a934a42751f6f6f8d50b
25
+ source_hash: sha256:831cdb38a60c83171b114148aff40e507c1feb9c94600e9404de748e293f9b21
26
26
  hash_method: sha256-over-sorted-path-and-content
27
27
  hash_covers: source_files
28
- timestamp: '2026-09-05T13:18:40Z'
29
- generator: constant-docs/0.7.0
28
+ timestamp: '2026-09-05T16:46:40Z'
29
+ generator: constant-docs/0.8.0
30
30
  generator_spec: https://github.com/ashborn-systems/constant-docs/blob/main/SPEC.md
31
31
  -->
32
32
 
@@ -65,8 +65,9 @@ Your coding agent writes every word, because it already holds the source and a
65
65
  model connection. constant-docs names the documents that moved and takes the
66
66
  new text back.
67
67
 
68
- That one command is also the whole CI integration. It exits 1 on drift, so a
69
- change that alters behaviour and leaves its document behind does not merge:
68
+ That one command is also the whole continuous-integration check. It exits 1 on
69
+ drift, so a change that alters behaviour and leaves its document behind does
70
+ not merge:
70
71
 
71
72
  ```yaml
72
73
  - run: constant-docs verify
@@ -139,12 +140,12 @@ marks say. `verify` hashes everything and exits 1 on drift. Keep `verify` as
139
140
  the gate.
140
141
 
141
142
  Claude Code gets a plugin. Install it and two hooks run those commands for you:
142
- `PostToolUse` runs `mark`, `Stop` runs `settle --hook`. Edit a file, stop the
143
- turn, and its document is up to date. Any harness that can run a command on
144
- write and at turn end does the same job.
143
+ `PostToolUse` runs `mark` after each edit, and `Stop` runs `settle --hook` when
144
+ the turn ends. Edit a file, stop the turn, and its document is up to date. Any
145
+ harness that can run a command on write and at turn end does the same job.
145
146
 
146
- Regeneration happens at quiescence, never mid-edit, so the generator and the
147
- coding loop never write the same file at once. The dirty set lives in
147
+ Regeneration happens at the end of a turn, never mid-edit, so the generator and
148
+ the coding loop never write the same file at once. The dirty set lives in
148
149
  `.constant-docs/dirty.json`, which is gitignored, so a crashed session is
149
150
  picked up on the next run.
150
151
 
@@ -152,9 +153,9 @@ The [harness guide](guides/harness-integration.md) walks through each route.
152
153
 
153
154
  ## Kinds
154
155
 
155
- A document is not always a module explainer. Its kind decides which sections it
156
- must carry, which prompt its generator is handed, and whether a regeneration
157
- replaces the body or appends to it.
156
+ Every document has a kind. The kind decides which sections its body must carry,
157
+ which prompt its generator is handed, and whether a regeneration replaces the
158
+ body or appends an entry.
158
159
 
159
160
  | Kind | For | Mode |
160
161
  |---|---|---|
@@ -162,7 +163,7 @@ replaces the body or appends to it.
162
163
  | `spec` | What a subsystem promises, and why | replace |
163
164
  | `log` | A changelog or build log | **append** |
164
165
  | `architecture` | The module map, with a diagram | replace |
165
- | `errors` | Every message the tool can emit | replace |
166
+ | `errors` | Every message the covered code can raise | replace |
166
167
  | `config-reference` | Every configuration key | replace |
167
168
  | `cli-reference` | Every command and flag | replace |
168
169
 
@@ -183,8 +184,9 @@ An append never rewrites or reorders what is already there. Pass a whole body
183
184
  to a log, or a single entry to a module document, and the tool refuses it. A
184
185
  configuration that has never heard of kinds behaves exactly as it did.
185
186
 
186
- One house style governs every generated document. It lives in a single file
187
- that all seven built-in prompts receive, so the rules cannot drift apart.
187
+ One house style governs every document written from a built-in prompt. It lives
188
+ in a single file that all seven built-in prompts receive, so the rules cannot
189
+ drift apart.
188
190
 
189
191
  ## Finding what is undocumented
190
192
 
@@ -194,17 +196,18 @@ constant-docs coverage # source no module covers, by directory
194
196
  constant-docs completeness # kinds of document the contents warrant
195
197
  ```
196
198
 
197
- `verify` can only check what has been declared, so a repository could otherwise
198
- drift to a large fraction undocumented while every check passed. `coverage`
199
- does one thing: a set difference. Declare the directories that should carry no
200
- document in the configuration, with a reason each. An exclusion nobody
201
- justified is one nobody decided.
199
+ `verify` can only check what has been declared, so a repository can drift a
200
+ long way undocumented while every check passes. `coverage` does one thing: a
201
+ set difference. It reports the files no declared glob matches and no exclusion
202
+ names. Declare the directories that should carry no document in the
203
+ configuration, with a reason each. An exclusion nobody justified is one nobody
204
+ decided.
202
205
 
203
206
  Coverage answers whether a file has a document. It cannot answer whether the
204
- set of documents is complete: a document that was never written cannot drift,
205
- and a module document covers its files, so a repository with a document per
206
- module and no specification passes everything. `completeness` proposes the
207
- kinds your contents warrant, each with the fact behind it:
207
+ set of documents is complete. A document that was never written cannot drift,
208
+ and a repository with a document per module and no specification passes every
209
+ coverage check. `completeness` proposes the kinds your contents warrant, each
210
+ with the fact behind it:
208
211
 
209
212
  ```
210
213
  Warranted and not written (2):
@@ -218,11 +221,11 @@ Warranted and not written (2):
218
221
  It proposes from evidence in the repository, such as a console script or an
219
222
  exception hierarchy. The existence of a kind is not itself evidence. A library
220
223
  with no command line is not offered a CLI reference, because a document nobody
221
- needs still has to be kept true and still refuses a commit when it drifts.
224
+ needs still has to be kept true and still fails the build when it drifts.
222
225
  Nothing warrants a changelog: every repository could keep one, so the signal
223
226
  fires everywhere and says nothing.
224
227
 
225
- Write the document, or decline the kind under `unwarranted:` with a reason and
228
+ Write the document, or decline the kind under `unwarranted:` with a reason, and
226
229
  it stops being proposed.
227
230
 
228
231
  `verify --coverage` and `verify --completeness` fold the two into the gate,
@@ -232,13 +235,14 @@ document, and `prune` deletes it.
232
235
  ## Files it will not touch
233
236
 
234
237
  Pointing the tool at a `docs/` folder the repository already has is safe. Every
235
- document this tool writes carries a block naming the tool, and, when you
236
- declare a `project` id, the project that wrote it. A markdown file without that
237
- block belongs to somebody else, and three commands act on the difference:
238
+ document this tool writes carries a block naming the tool and, when you declare
239
+ a `project` id, the project that wrote it. A markdown file without that block
240
+ belongs to somebody else, and three commands act on the difference:
238
241
 
239
242
  - `apply` refuses to write over one. It names the file and changes no byte
240
243
  - `prune` refuses to delete one, and names every file it left alone
241
- - `verify` lists them under a heading of their own, and they fail nothing
244
+ - `verify` lists them under a heading of their own, and none of them fails the
245
+ check
242
246
 
243
247
  Take one over when you want it maintained:
244
248
 
@@ -259,7 +263,7 @@ rewrite it. Every body your agent generates still carries those headings, and
259
263
  `apply` checks before it writes a byte.
260
264
 
261
265
  A glob that matches no file gets the same restraint. `verify` names the module
262
- and exits 1, `apply` refuses to record a digest over nothing, and the document
266
+ and exits 1, `apply` refuses to record a hash over nothing, and the document
263
267
  stays where it is. A mistyped glob costs you a failing check and nothing else.
264
268
 
265
269
  ## Documents outside the repository
@@ -280,12 +284,14 @@ real directory on this machine: where to open the file.
280
284
 
281
285
  The variable must resolve when the configuration loads. Unset, empty, relative,
282
286
  or naming a directory that is not there, each refuses the configuration and
283
- says which. A store landing inside `.ssh`, `Secret/`, a cache or any other
284
- directory on the deny list is refused too. The tool declines to read those
285
- directories, so it declines to write documents into them. Plain `${NAME}` is
286
- the whole syntax. A default value would let a machine without the vault write
287
- documents into the repository and report success. The Stop hook blocks on the
288
- same failure, so a missing variable cannot switch the gate off quietly.
287
+ says which. A store landing inside `.ssh`, `Secret/`, a cache, or any other
288
+ directory on the deny list is refused too: the tool declines to read those
289
+ directories, so it declines to write documents into them.
290
+
291
+ Plain `${NAME}` is the whole syntax. A default value would let a machine
292
+ without the vault write documents into the repository and report success. The
293
+ Stop hook blocks on the same failure, so a missing variable cannot switch the
294
+ gate off quietly.
289
295
 
290
296
  Deletion changes as well. A repository's documents ride the branch, and `git
291
297
  checkout` brings one back. A store stands still while the checkout moves, so
@@ -305,12 +311,14 @@ auto:
305
311
  budget: 5
306
312
  ```
307
313
 
308
- The tool still calls no model. constant-docs splits your command with `shlex`
309
- and runs it directly, so an agent CLI, a script and a Makefile target all work.
310
- No credential goes in it. The command's success is checked: `auto` re-runs
311
- `verify` afterwards. It exits 0 when everything is clean, 1 when something is
312
- still stale, and 2 when the command could not run or exited non-zero. Three
313
- outcomes, because a scheduler that confuses the last two retries the wrong one.
314
+ constant-docs itself calls no model. It splits your command with `shlex` and
315
+ runs it directly, so an agent CLI, a script, and a Makefile target all work. No
316
+ credential goes in it. The command's success is checked: `auto` re-runs
317
+ `verify` afterwards.
318
+
319
+ It exits 0 when everything is clean, 1 when something is still stale, and 2
320
+ when the command could not run or exited non-zero. Three outcomes, because a
321
+ scheduler that confuses the last two retries the wrong one.
314
322
 
315
323
  ## Three things it checks that a hash cannot
316
324
 
@@ -325,9 +333,10 @@ document, with a date.
325
333
  in those modules' files by name, so it goes stale when their boundary moves.
326
334
 
327
335
  **An error catalogue agrees with the source, both ways.** Every message the
328
- code raises appears in the catalogue, and every entry still exists in the code.
329
- Neither needs parsing beyond a string scan, so it costs nothing, and it is the
330
- one document here the tool can actually guarantee. The rest are only fresh.
336
+ code raises appears in the catalogue, and every entry listed is still raised in
337
+ the code. The check needs no parsing beyond a string scan, so it costs nothing,
338
+ and it is the one document here the tool can guarantee. The rest are only
339
+ fresh.
331
340
 
332
341
  ## What a document looks like
333
342
 
@@ -352,7 +361,7 @@ constant_docs:
352
361
  hash_method: sha256-over-sorted-path-and-content
353
362
  hash_covers: source_files
354
363
  timestamp: '2026-08-19T09:14:00Z'
355
- generator: constant-docs/0.7.0
364
+ generator: constant-docs/0.8.0
356
365
  generator_spec: https://github.com/ashborn-systems/constant-docs/blob/main/SPEC.md
357
366
  ---
358
367
  ## Purpose
@@ -375,7 +384,7 @@ report records that it happened.
375
384
 
376
385
  The frontmatter stands alone. An agent that has never run this tool can see
377
386
  what the document describes, where its source lives, and how to recompute the
378
- digest. `project` answers the question asked before every overwrite and every
387
+ hash. `project` answers the question asked before every overwrite and every
379
388
  deletion: whose document is this. The tool keeps everything it owns under one
380
389
  `constant_docs` key, an Open Knowledge Format producer extension. `type`,
381
390
  `title`, `tags` and the two dates stay yours, and unknown keys survive a write.
@@ -395,11 +404,11 @@ prune() # deletes orphans; under a store, lists them
395
404
 
396
405
  ## What it does not do
397
406
 
398
- A module names a glob, and the tool digests the bytes of the files that glob
399
- matches. Nothing in that step knows a language, which is what makes yours work:
400
- no model client, no network call, no AST, no import graph. The cost is that any
401
- byte moves the hash. Fixing a typo in a comment reports the module stale, and
402
- your agent rewrites a document the edit never touched.
407
+ A module names a glob, and the tool hashes the bytes of the files that glob
408
+ matches. Nothing in that step knows a language, which is what makes it work on
409
+ yours: no model client, no network call, no syntax tree, no import graph. The
410
+ cost is that any byte moves the hash. Fixing a typo in a comment reports the
411
+ module stale, and your agent rewrites a document the edit never touched.
403
412
 
404
413
  It watches files and nothing else. A document quoting a queue depth or a
405
414
  production URL stays fresh for ever, because nobody edited anything. Hash what
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "constant-docs"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Self-maintaining documentation for agentic codebases"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "constant-docs"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Self-maintaining documentation for agentic codebases"
5
5
  readme = "README.md"
6
6
  # No contact address. PyPI does not require one, and it is published verbatim
@@ -89,7 +89,10 @@ class ModulePlan:
89
89
  previous_body: str | None
90
90
  previous_description: str | None
91
91
  required_headings: list[str]
92
- reason: Literal["changed", "missing"]
92
+ # "changed" and "missing" are answers about this repository. "regen" is an
93
+ # answer about the tool: the source has not moved and the document is
94
+ # listed anyway, because a newer prompt writes it differently.
95
+ reason: Literal["changed", "missing", "regen"]
93
96
  # A kind decides the shape of the write. `mode` is the one a caller must
94
97
  # branch on: "replace" takes a whole body through `apply`, "append" takes
95
98
  # a single entry through `append`, and passing one to the other is refused
@@ -403,6 +406,7 @@ def plan(
403
406
  config_path: str | Path | None = None,
404
407
  changed_paths: list[str] | None = None,
405
408
  modules: list[str] | None = None,
409
+ regen: bool = False,
406
410
  ) -> Plan:
407
411
  """Inspect the repository and return the current state.
408
412
 
@@ -412,9 +416,47 @@ def plan(
412
416
  *changed_paths* of ``None`` means consider every module. *modules* names
413
417
  module keys directly, which is what `settle` has and saves it inventing a
414
418
  path that resolves back to them.
419
+
420
+ *regen* lists every module as work whether or not its source moved, which
421
+ is the one question a content hash cannot answer: the document is not
422
+ behind its code, it is behind the conventions it was written against. A
423
+ release that changes the prompts leaves every document passing `verify`
424
+ and none of them written the new way.
425
+
426
+ The previous body is still supplied, and the prompt carries an
427
+ instruction to carry its content across. A regeneration that starts from
428
+ an empty page keeps every heading and loses the reasons underneath them,
429
+ which no check here can see.
415
430
  """
416
431
  resolved = _resolve_config(config_path)
417
- return _plan(load_config(resolved), _repo_root(resolved), changed_paths, modules)
432
+ return _plan(
433
+ load_config(resolved), _repo_root(resolved), changed_paths, modules, regen=regen
434
+ )
435
+
436
+
437
+ # Appended to every prompt a `--regen` plan carries. Without it, a rewrite
438
+ # against an unchanged source loses content: regenerating this repository's
439
+ # own `config` document dropped 22 of its 38 correctness pillars, and the SPEC
440
+ # came back at 10,065 words against 24,502. The same run with this paragraph
441
+ # in front of the generator returned 102% and 98% of the originals.
442
+ #
443
+ # It travels in the plan rather than in the skill, so a harness that reads
444
+ # `settle --json` and follows the `prompt` field gets it without knowing what
445
+ # `--regen` means.
446
+ _REGEN_NOTE = """
447
+
448
+ ## This is a regeneration, not a rewrite
449
+
450
+ Nothing in the source moved. This document is being written again because the
451
+ conventions above have changed since it was last written.
452
+
453
+ Carry every rule, every failure mode and every reason the previous body
454
+ records into the new one. Change the wording, the order and the structure as
455
+ the conventions now require. Merge two entries that say the same thing. Drop
456
+ only something the source no longer supports.
457
+
458
+ A shorter document is the failure mode here. If you cannot express a rule in
459
+ the new form, keep the old sentence rather than losing the rule."""
418
460
 
419
461
 
420
462
  def _plan(
@@ -423,6 +465,7 @@ def _plan(
423
465
  changed_paths: list[str] | None = None,
424
466
  modules: list[str] | None = None,
425
467
  cache: DocumentCache | None = None,
468
+ regen: bool = False,
426
469
  ) -> Plan:
427
470
  """The body of :func:`plan`, over a configuration already loaded.
428
471
 
@@ -431,6 +474,12 @@ def _plan(
431
474
  the configuration three times and walk the repository three times with it.
432
475
  """
433
476
  cache = cache or DocumentCache()
477
+ # *regen* does not appear here. It decides what counts as work, not what
478
+ # is looked at, and the default scope is already every module. A branch
479
+ # widening the scope for it would only fire when a caller passed a
480
+ # narrowing argument *and* asked for a regeneration, and there the
481
+ # narrowing argument is the more specific instruction — the same reason
482
+ # `auto` keeps its budget through one.
434
483
  if modules is not None:
435
484
  modules_in_scope = {m for m in modules if m in cfg.module_files}
436
485
  else:
@@ -461,7 +510,7 @@ def _plan(
461
510
  doc_abs = store_path(cfg, repo_root, doc_file)
462
511
  previous_body: str | None = None
463
512
  previous_description: str | None = None
464
- reason: Literal["changed", "missing"] = "missing"
513
+ reason: Literal["changed", "missing", "regen"] = "missing"
465
514
 
466
515
  if doc_abs.exists():
467
516
  try:
@@ -472,9 +521,18 @@ def _plan(
472
521
  previous_body = existing.body
473
522
  previous_description = existing.frontmatter.get("description")
474
523
  if stored_hash == current_hash:
475
- fresh.append(mod_key)
476
- continue
477
- reason = "changed"
524
+ # A log is never regenerated. Its entries are a record of
525
+ # what happened, and the only write it takes is one more
526
+ # entry on the end. Listed under *regen* with nothing to
527
+ # report, a harness writes an entry describing no change.
528
+ # A log whose source did move is still work, and reaches
529
+ # the list by the ordinary route below.
530
+ if not regen or kind.mode == "append":
531
+ fresh.append(mod_key)
532
+ continue
533
+ reason = "regen"
534
+ else:
535
+ reason = "changed"
478
536
  except (OSError, DocumentError):
479
537
  # Malformed document → treat as missing
480
538
  reason = "missing"
@@ -490,7 +548,7 @@ def _plan(
490
548
  reason=reason,
491
549
  kind=kind.name,
492
550
  mode=kind.mode,
493
- prompt=_prompt_for(kind, repo_root),
551
+ prompt=_prompt_for(kind, repo_root) + (_REGEN_NOTE if regen else ""),
494
552
  )
495
553
  )
496
554
 
@@ -886,12 +944,20 @@ def apply(
886
944
 
887
945
  def append(
888
946
  module_key: str,
947
+ *,
889
948
  entry: str,
890
949
  title: str,
891
950
  config_path: str | Path | None = None,
892
951
  ) -> None:
893
952
  """Add one entry to the document for *module_key*.
894
953
 
954
+ *entry* and *title* are keyword-only, because they are two strings in the
955
+ opposite order to `constant-docs append <module> <title> <entry>` and
956
+ swapping them is silent: the whole entry becomes the heading and the
957
+ title becomes the paragraph under it. That happened to this repository's
958
+ own changelog. A caller that gets it wrong now fails on the call rather
959
+ than in the document.
960
+
895
961
  The entry goes under the kind's first section as an H3 whose text is
896
962
  Everything already in the document is left exactly as it is: an
897
963
  append that can modify history is a replace with extra steps.
@@ -1228,7 +1294,7 @@ def _unmarked_scope(
1228
1294
  return _Scope(modules=sorted(keys), evidence=evidence)
1229
1295
 
1230
1296
 
1231
- def settle(config_path: str | Path | None = None) -> Plan:
1297
+ def settle(config_path: str | Path | None = None, regen: bool = False) -> Plan:
1232
1298
  """Plan over what has changed, without clearing the dirty set.
1233
1299
 
1234
1300
  Called at quiescence — the end of a turn — so the generator and the coding
@@ -1252,10 +1318,17 @@ def settle(config_path: str | Path | None = None) -> Plan:
1252
1318
 
1253
1319
  1. A scope declared for this run in the environment, which is how `auto`
1254
1320
  holds its budget across whatever loop its command runs
1255
- 2. The dirty set, when a hook marked something. What a session touched is
1321
+ 2. Every module, when *regen* asks for one. It sits below the declared
1322
+ scope so `auto` keeps its cap, and above the rest because the question
1323
+ it asks is not "what moved" at all
1324
+ 3. The dirty set, when a hook marked something. What a session touched is
1256
1325
  the work that session owes, and planning over more of the repository
1257
1326
  would make `auto`'s cap advisory
1258
- 3. Otherwise, what git says moved
1327
+ 4. Otherwise, what git says moved
1328
+
1329
+ A *regen* run records no baseline. A baseline claims every module was
1330
+ fresh at this commit, and a run that listed every module as work has
1331
+ established nothing of the kind.
1259
1332
  """
1260
1333
  resolved = _resolve_config(config_path)
1261
1334
  repo_root = _repo_root(resolved)
@@ -1278,6 +1351,8 @@ def settle(config_path: str | Path | None = None) -> Plan:
1278
1351
  declared = declared_scope()
1279
1352
  if declared:
1280
1353
  scope = _Scope(modules=declared)
1354
+ elif regen:
1355
+ scope = _Scope(modules=None)
1281
1356
  elif dirty.modules:
1282
1357
  scope = _Scope(modules=list(dirty.modules))
1283
1358
  else:
@@ -1299,7 +1374,7 @@ def settle(config_path: str | Path | None = None) -> Plan:
1299
1374
  if scope.modules is None
1300
1375
  else [m for m in scope.modules if m in cfg.module_files]
1301
1376
  )
1302
- p = _plan(cfg, repo_root, changed_paths=None, modules=known)
1377
+ p = _plan(cfg, repo_root, changed_paths=None, modules=known, regen=regen)
1303
1378
 
1304
1379
  remaining = dirty.modules
1305
1380
  if p.fresh:
@@ -10,7 +10,7 @@ Usage::
10
10
  constant-docs prune [<config>]
11
11
  constant-docs index [<config>]
12
12
  constant-docs mark [<path>...]
13
- constant-docs settle [--json] [--hook]
13
+ constant-docs settle [--json] [--hook] [--regen]
14
14
  constant-docs completeness [--json] [<config>]
15
15
  constant-docs prompt [<kind>]
16
16
  """
@@ -303,10 +303,10 @@ def cmd_verify(
303
303
  sys.exit(report.exit_code)
304
304
 
305
305
 
306
- def cmd_plan(config_path: str | None, json_output: bool) -> None:
306
+ def cmd_plan(config_path: str | None, json_output: bool, regen: bool = False) -> None:
307
307
  """Inspect and report the current state."""
308
308
  try:
309
- p = plan(_resolve_config(config_path))
309
+ p = plan(_resolve_config(config_path), regen=regen)
310
310
  except ConfigError as e:
311
311
  _exit_config_error(str(e))
312
312
 
@@ -359,7 +359,12 @@ def cmd_append(
359
359
  ) -> None:
360
360
  """Add one entry to an append-mode document."""
361
361
  try:
362
- append(module_key, entry, title, config_path=_resolve_config(config_path))
362
+ append(
363
+ module_key,
364
+ entry=entry,
365
+ title=title,
366
+ config_path=_resolve_config(config_path),
367
+ )
363
368
  print(f"Appended to {module_key}.")
364
369
  except ConfigError as e:
365
370
  _exit_config_error(str(e))
@@ -907,11 +912,13 @@ def cmd_mark(paths: list[str]) -> int:
907
912
  return 0
908
913
 
909
914
 
910
- def cmd_settle(config_path: str | None, json_output: bool, hook: bool) -> int:
915
+ def cmd_settle(
916
+ config_path: str | None, json_output: bool, hook: bool, regen: bool = False
917
+ ) -> int:
911
918
  """Report the work the dirty set implies, without clearing it."""
912
919
  try:
913
920
  resolved = _resolve_config(config_path)
914
- p = settle(resolved)
921
+ p = settle(resolved, regen=regen)
915
922
  except ConfigError as e:
916
923
  # See cmd_mark: silence outside a configured repository. A Stop hook
917
924
  # that fails everywhere else is a Stop hook nobody keeps.
@@ -1142,7 +1149,10 @@ def main(argv: list[str] | None = None) -> int:
1142
1149
  wants_completeness,
1143
1150
  )
1144
1151
  elif subcommand == "plan":
1145
- cmd_plan(args[0] if args else None, json_output)
1152
+ regen = "--regen" in args
1153
+ if regen:
1154
+ args.remove("--regen")
1155
+ cmd_plan(args[0] if args else None, json_output, regen)
1146
1156
  elif subcommand == "apply":
1147
1157
  try:
1148
1158
  retire = _take_repeated(args, "--retire")
@@ -1214,7 +1224,10 @@ def main(argv: list[str] | None = None) -> int:
1214
1224
  hook = "--hook" in args
1215
1225
  if hook:
1216
1226
  args.remove("--hook")
1217
- return cmd_settle(args[0] if args else None, json_output, hook)
1227
+ regen = "--regen" in args
1228
+ if regen:
1229
+ args.remove("--regen")
1230
+ return cmd_settle(args[0] if args else None, json_output, hook, regen)
1218
1231
  elif subcommand == "append":
1219
1232
  if len(args) < 3:
1220
1233
  print(
@@ -1259,7 +1272,8 @@ def _print_help() -> None:
1259
1272
  "\n"
1260
1273
  "Commands:\n"
1261
1274
  " verify [--json] [<config>] Check for stale/missing/orphan documents\n"
1262
- " plan [--json] [<config>] List module states\n"
1275
+ " plan [--json] [--regen] [<config>]\n"
1276
+ " List module states\n"
1263
1277
  " apply <module> <body> [<config>] [--retire <id>]...\n"
1264
1278
  " Write a document body\n"
1265
1279
  " append <module> <title> <entry> [<config>] Add one log entry\n"
@@ -1273,7 +1287,8 @@ def _print_help() -> None:
1273
1287
  " prune [<config>] Delete orphaned documents\n"
1274
1288
  " index [<config>] Rebuild the root index from descriptions\n"
1275
1289
  " mark [<path>...] Add a path's modules to the dirty set\n"
1276
- " settle [--json] [--hook] Report what the dirty set implies\n"
1290
+ " settle [--json] [--hook] [--regen]\n"
1291
+ " Report what the dirty set implies\n"
1277
1292
  " prompt [<name>] Print a kind's conventions, or a guide\n"
1278
1293
  "\n"
1279
1294
  "Options:\n"
@@ -1333,7 +1348,13 @@ def _print_subcommand_help(cmd: str) -> None:
1333
1348
  " --completeness Also fail on a kind the contents warrant and\n"
1334
1349
  " nothing provides\n"
1335
1350
  ),
1336
- "plan": "Usage: constant-docs plan [--json] [<config>]\n\nList the state of every configured module.\n\nOptions:\n --json Output machine-readable JSON\n",
1351
+ "plan": (
1352
+ "Usage: constant-docs plan [--json] [--regen] [<config>]\n\n"
1353
+ "List the state of every configured module.\n\n"
1354
+ "Options:\n"
1355
+ " --json Output machine-readable JSON\n"
1356
+ " --regen List every module as work, whether or not its source moved.\n For after an upgrade that changed the conventions: the\n documents are not behind their code, they are behind the\n prompts they were written against, and no hash sees that.\n Each plan still carries the previous body, and the prompt\n it carries says to keep what that body records\n"
1357
+ ),
1337
1358
  "apply": (
1338
1359
  "Usage: constant-docs apply <module> <body> [<config>] "
1339
1360
  "[--retire <id>]...\n\n"
@@ -1378,7 +1399,7 @@ def _print_subcommand_help(cmd: str) -> None:
1378
1399
  "document to a renamed module.\n"
1379
1400
  ),
1380
1401
  "mark": "Usage: constant-docs mark [<path>...]\n\nAdd every module the given paths belong to, to `.constant-docs/dirty.json`.\nPass paths as arguments from any harness. With no arguments, reads a\nPostToolUse hook payload from stdin, the shape Claude Code sends.\nSilent and exit 0 when a path matches nothing, or outside a configured\nrepository.\n",
1381
- "settle": "Usage: constant-docs settle [--json] [--hook]\n\nReport the work the dirty set implies. The set is NOT cleared by being\nread; it clears on the apply or append that satisfies each module.\n\nOptions:\n --json Machine-readable plan, the same structure `plan --json` emits\n --hook Stop-hook form: exit 2 with instructions on stderr when work is\n outstanding, exit 0 when it is not\n",
1402
+ "settle": "Usage: constant-docs settle [--json] [--hook] [--regen]\n\nReport the work the dirty set implies. The set is NOT cleared by being\nread; it clears on the apply or append that satisfies each module.\n\nOptions:\n --json Machine-readable plan, the same structure `plan --json` emits\n --hook Stop-hook form: exit 2 with instructions on stderr when work is\n outstanding, exit 0 when it is not\n --regen List every module as work, whether or not its source moved.\n For after an upgrade that changed the conventions: the\n documents are not behind their code, they are behind the\n prompts they were written against, and no hash sees that.\n Each plan still carries the previous body, and the prompt\n it carries says to keep what that body records\n",
1382
1403
  "index": "Usage: constant-docs index [<config>]\n\nRebuild <docs_root>/index.md from every document's description.\nLeaves the file byte-identical when nothing changed.\n",
1383
1404
  "append": "Usage: constant-docs append <module> <title> <entry> [<config>]\n\nAdd one entry to an append-mode document. The title becomes the entry's\nH2 heading. Nothing already written is rewritten or reordered.\n",
1384
1405
  "prompt": (
File without changes