doc-marshal 0.0.1__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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Root
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,50 @@
1
+ Metadata-Version: 2.5
2
+ Name: doc-marshal
3
+ Version: 0.0.1
4
+ Summary: A machine-checked documentation tree for repositories whose primary reader is a coding agent.
5
+ Project-URL: Homepage, https://github.com/rootdrew27/doc-marshal
6
+ Project-URL: Source, https://github.com/rootdrew27/doc-marshal
7
+ Author: Andrew Root
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,claude,docs-as-code,documentation,linter
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Documentation
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+
19
+ # doc-marshal
20
+
21
+ A machine-checked documentation tree for repositories whose primary reader is a coding agent.
22
+
23
+ > **Status: pre-alpha, unbuilt.** The design is settled and written down in
24
+ > [SPEC.md](SPEC.md). This release exists to reserve the name.
25
+
26
+ Documentation rots because nothing connects a document to the thing that would falsify it.
27
+ `doc-marshal` makes that connection mechanical: every living note declares, in frontmatter, the
28
+ code or sources it describes, so "which docs did this change invalidate?" is a question with an
29
+ answer a script can give.
30
+
31
+ It ships an opinionated eight-type ontology -- but the engine is the product, and an ontology you
32
+ declare yourself is held to exactly the same standard.
33
+
34
+ ## What it will do
35
+
36
+ ```bash
37
+ doc-marshal check --all # validate every note against the ontology
38
+ doc-marshal index # regenerate the one generated index
39
+ doc-marshal affected # notes whose anchors name code this change touched
40
+ doc-marshal new decision ... # scaffold a note the validator accepts
41
+ doc-marshal info # the effective ruleset, for a human or an agent
42
+ doc-marshal init # wire it into a repository
43
+ ```
44
+
45
+ Enforcement runs at four points -- as an agent writes a note, at `git commit` via pre-commit, and
46
+ twice in CI -- so an error surfaces at the earliest point that can see it.
47
+
48
+ ## License
49
+
50
+ MIT.
@@ -0,0 +1,32 @@
1
+ # doc-marshal
2
+
3
+ A machine-checked documentation tree for repositories whose primary reader is a coding agent.
4
+
5
+ > **Status: pre-alpha, unbuilt.** The design is settled and written down in
6
+ > [SPEC.md](SPEC.md). This release exists to reserve the name.
7
+
8
+ Documentation rots because nothing connects a document to the thing that would falsify it.
9
+ `doc-marshal` makes that connection mechanical: every living note declares, in frontmatter, the
10
+ code or sources it describes, so "which docs did this change invalidate?" is a question with an
11
+ answer a script can give.
12
+
13
+ It ships an opinionated eight-type ontology -- but the engine is the product, and an ontology you
14
+ declare yourself is held to exactly the same standard.
15
+
16
+ ## What it will do
17
+
18
+ ```bash
19
+ doc-marshal check --all # validate every note against the ontology
20
+ doc-marshal index # regenerate the one generated index
21
+ doc-marshal affected # notes whose anchors name code this change touched
22
+ doc-marshal new decision ... # scaffold a note the validator accepts
23
+ doc-marshal info # the effective ruleset, for a human or an agent
24
+ doc-marshal init # wire it into a repository
25
+ ```
26
+
27
+ Enforcement runs at four points -- as an agent writes a note, at `git commit` via pre-commit, and
28
+ twice in CI -- so an error surfaces at the earliest point that can see it.
29
+
30
+ ## License
31
+
32
+ MIT.
@@ -0,0 +1,534 @@
1
+ # doc-marshal -- design spec
2
+
3
+ **Status:** agreed, unbuilt. Written 2026-09-02 from a design session held in the MakeRent repo.
4
+
5
+ **Provenance:** the tooling this project extracts lives at
6
+ `.claude/skills/update-docs/` in MakeRent, at commit `d664cb1`. That kit is the working
7
+ prototype -- every rule below has been exercised against a real 30-note documentation tree.
8
+ This document records what the extraction decided, including the parts that reverse the
9
+ prototype.
10
+
11
+ This is a *spec* in the sense the ontology uses the word: it describes work before it is built,
12
+ and its Validation section carries items that are not yet closed. It is not a reference. When
13
+ the code exists and disagrees with this file, the code is right and this file is stale.
14
+
15
+ ---
16
+
17
+ ## 1. What this is
18
+
19
+ A documentation system for repositories whose primary reader is a coding agent.
20
+
21
+ It has three parts:
22
+
23
+ 1. **An engine** -- a validator, index builder and drift detector for a tree of typed markdown
24
+ notes. Every rule it enforces is read off a registry rather than hardcoded per check.
25
+ 2. **A preset** -- `standard`, an eight-type ontology with an opinionated argument for why each
26
+ type exists and how to route between them.
27
+ 3. **Integrations** -- a Claude Code plugin, a pre-commit hook, and a CLI that CI calls directly.
28
+
29
+ **Two names, deliberately.** The tool is `doc-marshal` -- the package, the CLI, the module
30
+ (`doc_marshal`), the config file and the repository. The directory it governs stays `agent-docs/`,
31
+ because a directory name is not constrained by a package index and `agent-docs/` says what it
32
+ holds. The tool is named for what it does; the directory for what is in it.
33
+
34
+ The distinguishing idea is the **anchor**: every living note declares, in frontmatter, what
35
+ outside itself would falsify it. That makes "which docs did this diff invalidate?" a question
36
+ with a mechanical answer, which is the difference between documentation that rots silently and
37
+ documentation that reports its own staleness.
38
+
39
+ ## 2. Positioning -- engine, not convention
40
+
41
+ The product is the engine. The eight-type ontology ships as a preset and is a convenience.
42
+
43
+ This was decided deliberately against the alternative, which was to sell the convention itself
44
+ (the way Conventional Commits or Keep a Changelog are sold) and treat the validator as the thing
45
+ that makes it stick. Engine-as-product buys users the freedom to bring their own ontology and be
46
+ held to it just as strictly. The cost, accepted: the shipped ontology is one option among many
47
+ rather than the point, and the engine competes on capability with general docs linters.
48
+
49
+ The consequence that does the most work downstream: **anchoring must be an engine capability, not
50
+ a fact about two hardcoded field names.** See §3.3.
51
+
52
+ ## 3. The ontology
53
+
54
+ ### 3.1 The standard preset
55
+
56
+ Eight types. `serves` is the reader each one is for; the required anchor follows from authorship
57
+ (see §3.3).
58
+
59
+ | Type | Serves | Required anchor |
60
+ | --- | --- | --- |
61
+ | `reference` | someone looking up a fact this repo decides | `code_refs` |
62
+ | `background` | someone looking up a fact this repo observes | `source` |
63
+ | `runbook` | someone executing under pressure | `code_refs` |
64
+ | `explanation` | someone asking why it works this way | `code_refs` |
65
+ | `decision` | someone about to reopen a settled choice | none |
66
+ | `spec` | someone building or validating unbuilt work | none |
67
+ | `history` | someone about to repeat a dead end | none |
68
+ | `context` | someone choosing what to call a thing | none |
69
+
70
+ Four types require no anchor, each for a stated reason. `decision` and `history` are append-only
71
+ and anchored by their own content. `spec` describes work that may not exist yet, so requiring a
72
+ resolvable path would make `status: proposed` unwritable. `context` is falsified by the words the
73
+ repo uses, not by a path.
74
+
75
+ ### 3.2 What a type declares
76
+
77
+ A type is data. Every facet below is enforced by the validator and applied by the scaffolder, and
78
+ no check hardcodes a type name.
79
+
80
+ | Facet | Meaning |
81
+ | --- | --- |
82
+ | `serves`, `voice`, `mutability` | prose, rendered by `info` |
83
+ | anchor requirements | which declared anchor fields this type must carry |
84
+ | `statuses`, `default_status` | allowed `status` values, empty means the type has none |
85
+ | `folder` | the one folder under the docs root this type lives in |
86
+ | `numbered` | filename carries a unique `NNNN-` prefix |
87
+ | `fixed_name` | the one filename this type may take, exempt from the naming pattern |
88
+ | `root_required` | one instance must exist at the docs root |
89
+ | `additive` | a nested instance may not redefine a key an ancestor defines |
90
+ | `append_only` | never edited after acceptance, so its wording cannot be corrected |
91
+ | `requires_related` | the note ends with a `## Related` section |
92
+ | `supersession` | field names and status recording that this note was replaced |
93
+ | `skeleton` | what the scaffolder writes |
94
+ | `structure` | the body shape other checks parse -- see below |
95
+
96
+ **`structure`** exists for a type whose body is *data* rather than prose. It declares the exact
97
+ `##` sections in order, the table's columns, which column is the key, which columns other notes
98
+ are scanned against, and caps on rows, cell length and total file size. A type with a `structure`
99
+ is validated for shape as an error rather than a warning, because a renamed column does not
100
+ degrade the checks built on it -- it silently turns them off, and a check that has quietly stopped
101
+ running is worse than one that never existed.
102
+
103
+ `context` is the only preset type with a `structure` today. Its `Avoid` column is read by every
104
+ other note's vocabulary check.
105
+
106
+ ### 3.3 Anchors
107
+
108
+ Anchor **fields** are declarable, not fixed. Each declares what it holds and how its entries
109
+ resolve.
110
+
111
+ | `resolves` | Meaning | On the drift spine |
112
+ | --- | --- | --- |
113
+ | `repo-path` | a path from the repo root, must exist | yes |
114
+ | `docs-path` | a path that must resolve inside the docs root | no |
115
+ | `url` | an `http`/`https` URL, validated for shape | no |
116
+ | `opaque` | validated for presence only | no |
117
+
118
+ The preset declares two: `code_refs` (`repo-path`) and `source` (`docs-path` or `url`).
119
+
120
+ **The drift spine** is the set of anchor fields with `resolves = "repo-path"`. `doc-marshal
121
+ affected` matches those, and only those, against a git diff. This generalises the prototype, where
122
+ `affected_docs.py` read the literal string `code_refs` and `check_source` hardcoded the rule that
123
+ `source` must resolve inside the docs root. Both were convention judgments about two specific
124
+ fields baked into engine code; they become instances of a general rule.
125
+
126
+ **The engine does not require any ontology to have a `repo-path` anchor.** A user may declare
127
+ every field `opaque` and float free of the spine. Decided deliberately: freedom belongs to the
128
+ user, and an ontology with no drift detection is a choice they are entitled to make loudly in one
129
+ config file.
130
+
131
+ Anchor requirements per type are **minimums, not permitted sets**. Any declared field is legal on
132
+ any type and is validated whenever present.
133
+
134
+ ## 4. Configuration
135
+
136
+ Everything in this section is designed now and **built in 0.2**. See §14.
137
+
138
+ ### 4.1 File
139
+
140
+ `doc-marshal.toml` at the repo root, with `[tool.doc-marshal]` in `pyproject.toml` as a fallback
141
+ when the dedicated file is absent. The dedicated file is primary because the target audience
142
+ includes repositories with no Python in them at all.
143
+
144
+ TOML is read with `tomllib`, which is standard library from 3.11 -- the floor this package sets.
145
+
146
+ ### 4.2 Composition
147
+
148
+ ```toml
149
+ extends = "standard" # the default when omitted; `extends = []` starts from nothing
150
+ ```
151
+
152
+ Presets are named and shipped in the package. More may be added later (`minimal`) without a new
153
+ config mechanism -- that is the same `extends` key with more data behind it.
154
+
155
+ Rejected: merge-by-default, which makes the eight types unremovable furniture; and
156
+ replace-by-default, which makes adding one type mean retyping eight skeletons.
157
+
158
+ ### 4.3 Merge semantics
159
+
160
+ **Per-type shallow merge over the extended preset.** Writing `[types.reference] serves = "..."`
161
+ yields the preset's `reference` with one field replaced -- not a fresh type whose other facets
162
+ revert to defaults. The alternative would mean that adding a `folder` to `decision` silently
163
+ strips its numbering, supersession and skeleton.
164
+
165
+ **Merge is shallow per table, and nested tables merge the same way.** `[types.context.structure]
166
+ max_rows = 30` overrides one number without restating `columns`, `sections` or the other caps.
167
+
168
+ ### 4.4 Disabling
169
+
170
+ Disabling is a **value in the type's own table**, not a parallel list:
171
+
172
+ ```toml
173
+ [types.history]
174
+ enabled = false
175
+ ```
176
+
177
+ This follows the eslint (`"off"`), stylelint (`null`) and pyright (`"none"`) model rather than
178
+ ruff's `select`/`ignore` lists. The research behind the choice: ruff needs lists because it has
179
+ thousands of prefix-namespaced rule codes, and it has since **deprecated `extend-ignore`** because
180
+ the replace-versus-extend distinction collapsed in practice. Doc types are a small named entity
181
+ space, where a keyed map is already the natural structure.
182
+
183
+ Concretely, `enabled = false` beats a `disable = [...]` list on four counts: one place to look for
184
+ whether a type is live; a typo'd `[types.histry]` is a validatable unknown-type error where
185
+ `disable = ["histry"]` is silently a no-op; re-enabling downstream in an `extends` chain is
186
+ `enabled = true` rather than list subtraction; and it needs no new mechanism, being one more facet
187
+ under the merge rule that already exists.
188
+
189
+ ### 4.5 Weakening a built-in
190
+
191
+ Permitted. `[types.reference] code_refs = false` is legal.
192
+
193
+ "Always enforced" is a property of the **engine**, not of the preset: there is no severity
194
+ configuration, no warn-only mode, and no inline suppression. Whatever registry results is enforced
195
+ completely. Freezing the preset's internals would defend a much weaker thing while making the
196
+ eight types furniture nobody can move.
197
+
198
+ ### 4.6 The rules table
199
+
200
+ ```toml
201
+ [rules]
202
+ em_dash = false
203
+ filename_pattern = "^[a-z0-9-]+$"
204
+ summary_max = 300
205
+ forbidden_names = []
206
+ exclude = ["agent-docs/legacy/**"]
207
+ ```
208
+
209
+ Rule-level configuration exists, expressed as **values**, with disabling as a legal value -- the
210
+ same shape as §4.4, so there is one idea to learn rather than two.
211
+
212
+ `exclude` is the incremental-adoption mechanism: it quarantines *files* visibly rather than
213
+ weakening a *rule* everywhere. Partial adoption already half-works, because `check` accepts a file
214
+ list and pre-commit passes only staged files; `exclude` extends that to `--all` in CI.
215
+
216
+ ### 4.7 What is not configurable, ever
217
+
218
+ **No inline suppression.** No `<!-- doc-marshal-disable -->` in a note, no per-line ignores. This
219
+ is the line, and it is the only prohibition the design defends absolutely. A hundred scattered
220
+ suppressions are unauditable; everything in a config file shows up in review and can be printed
221
+ back as the effective ruleset by `doc-marshal info`.
222
+
223
+ **Tier 1 invariants**, regardless of configuration: frontmatter parses; `type` names a live type;
224
+ declared anchors are present and resolve according to their kind; links resolve, including heading
225
+ anchors; a type's declared facets hold; the index is generated rather than written.
226
+
227
+ ## 5. Prose lives in the package
228
+
229
+ The convention's prose -- the rules, the argument for each type, the routing guidance -- ships
230
+ **inside the package** and is obtained by calling the CLI. It is never copied into a user's
231
+ repository.
232
+
233
+ This dissolves rather than solves several problems at once: no emitted copy means no staleness
234
+ check, no ownership boundary between the tool's file and the user's edits, and no question about
235
+ whether a conventions file inside the docs root is itself a note that must pass validation.
236
+ Output is filtered to *enabled* types, so it is more accurate than any stored file, and it always
237
+ matches the installed version.
238
+
239
+ It also deletes an enforcement point. The prototype's `build_types.py` exists solely to keep
240
+ stored tables in agreement with the registry, and is checked in CI and in the commit hook.
241
+ Rendering on demand makes that entire staleness class impossible.
242
+
243
+ **Markdown is primary**, with `--format json` available for third parties building on the engine.
244
+
245
+ Two consequences:
246
+
247
+ - **User-declared types supply their own prose.** `serves`/`voice`/`mutability` inline; a
248
+ `description_file` key for anything longer, because multi-paragraph markdown inside a TOML
249
+ string is miserable to author and to diff.
250
+ - **Humans need it on the web.** A reviewer on a pull request cannot run the CLI, and the
251
+ conventions are the sales pitch. The canonical human rendering lives in the repository README
252
+ and the project's docs site. `init` writes one small pointer file into the target repo -- a
253
+ pointer, not a copy, so it cannot drift.
254
+
255
+ ## 6. CLI surface
256
+
257
+ One command, `doc-marshal`, replacing the prototype's five script paths. That single name is what
258
+ makes the prose portable: reference files, hooks, CI steps and agent-memory files stop naming
259
+ installation paths.
260
+
261
+ | Command | Purpose |
262
+ | --- | --- |
263
+ | `check [paths...]` / `check --all` | validate the named notes, or sweep the tree |
264
+ | `index` | regenerate `INDEX.md`; `--check` reports staleness without writing |
265
+ | `affected` | notes whose `repo-path` anchors name code a change touched; `--range`, `--paths`, `--format github` |
266
+ | `new <type> <path>` | scaffold a note the validator will accept |
267
+ | `info` | the compact effective registry -- enabled types, one line each, with anchors |
268
+ | `info <type>` | one type in full: argument, skeleton, facets, statuses |
269
+ | `info --conventions` | the preset preamble -- the rules that are not per-type |
270
+ | `info --process` | the update-docs process, staged |
271
+ | `init [--claude-code]` | write the integration files into a target repo |
272
+ | `doctor` | report the resolved engine version and flag a plugin/repo mismatch |
273
+ | `session-context` | what a fresh session is given -- see §7 |
274
+
275
+ `build_types` has no successor: its rendering moves inside `info`.
276
+
277
+ ## 7. Session injection
278
+
279
+ A `SessionStart` hook injects three blocks:
280
+
281
+ 1. **The index preview** -- folder names with note counts, and nothing else, ending with a pointer
282
+ to `doc-marshal index` for the full list. **Uniform reduction at every size, including the top
283
+ level.**
284
+ 2. **The docs root's `CONTEXT.md`, verbatim.** The terms and the aliases they rule out are the
285
+ content; a summary of a vocabulary is a second vocabulary. Only the root note is injected -- a
286
+ nested one governs its subtree and is read on arriving there.
287
+ 3. **The compact `info` block** -- the enabled types and their anchors, roughly eight lines.
288
+
289
+ The reasoning for (1): `INDEX.md` in the prototype is injected in full and uncapped. It measured
290
+ **7592 characters at 30 notes** and grows linearly with the tree forever, while the `context` type
291
+ caps *itself* at 6000 characters with the explicit argument that it "is emitted into every session."
292
+ The argument that justifies the smaller cap applies with more force to the file that had none.
293
+
294
+ Rejected: capping by byte count, which makes the injected content depend on how verbose other
295
+ people's summaries are, so adding one long summary could silently truncate a different note out of
296
+ the index. Rejected: a note-count threshold with degradation, which introduces two renderings and
297
+ a cliff. Rejected: exempting top-level notes, which reintroduces the inconsistency that the
298
+ uniform rule removes.
299
+
300
+ The cost, accepted: a session that needs to route to a document spends one tool call. That is paid
301
+ only by sessions that touch documentation, where the previous design charged every session for a
302
+ full index including the majority that never open a doc.
303
+
304
+ ## 8. Package layout
305
+
306
+ ```
307
+ doc-marshal/
308
+ pyproject.toml zero runtime deps; console_script: doc-marshal
309
+ LICENSE MIT
310
+ README.md the standard preset's prose -- the sales pitch
311
+ SPEC.md this file
312
+ src/doc_marshal/
313
+ __main__.py python -m doc_marshal
314
+ cli.py subcommand dispatch
315
+ ontology.py DocType, Structure, Supersession; the standard preset
316
+ settings.py the constants of Tier 3, behind one object (see §13)
317
+ config.py TOML loader, merged over a preset [0.2]
318
+ paths.py docs-root discovery, path classification, frontmatter
319
+ check.py index.py affected.py new.py info.py init.py doctor.py
320
+ session.py what a fresh session is given
321
+ prose/
322
+ conventions.md rendered by `info --conventions`
323
+ doc-types.md rendered by `info` and `info <type>`
324
+ process.md rendered by `info --process`
325
+ plugin/
326
+ .claude-plugin/plugin.json
327
+ skills/update-docs/SKILL.md ~20 lines, deferring to `info --process`
328
+ hooks/hooks.json PostToolUse validation, SessionStart injection
329
+ vendor/doc_marshal/ built at release; makes the plugin work uninstalled
330
+ .pre-commit-hooks.yaml
331
+ tests/
332
+ ```
333
+
334
+ `DocType` is the **single internal representation**. The preset constructs it in Python -- keeping
335
+ the dataclass docstrings, type checking, and cross-references like `Structure(max_cell=SUMMARY_MAX)`
336
+ that TOML would flatten into a duplicated literal. The config loader is an alternate constructor
337
+ for the same objects.
338
+
339
+ **The round-trip test is the forcing function**: serialize the built-in registry to TOML, load it
340
+ back, assert equality. If the schema cannot express the shipped preset, the schema is too weak,
341
+ and that is discovered on day one rather than in a user's bug report. The same serializer backs
342
+ `info --dump-toml`, which is how a user sees a worked example without reading Python.
343
+
344
+ ## 9. Dependency policy
345
+
346
+ Runtime dependencies are permitted, and must pass three tests:
347
+
348
+ 1. **Pure Python.** No compiled extensions.
349
+ 2. **It must not replace a strictness boundary.**
350
+ 3. **It earns a decision record.**
351
+
352
+ **Nothing passes today.** The obvious candidate was replacing the hand-rolled frontmatter parser
353
+ with PyYAML, and it fails test 2: the prototype's `parse_frontmatter` deliberately reads a
354
+ *subset* -- scalars and dash lists -- and raises on anything richer, so a block the convention does
355
+ not sanction fails loudly rather than validating as empty. PyYAML accepts nested maps, flow style,
356
+ anchors, and the Norway problem where a bare `no` becomes `False`. The strictness is the
357
+ convention, enforced at parse time. The same argument rules out a real markdown parser: the table
358
+ and heading readers are strict subset readers, and `check_structure` depends on that.
359
+
360
+ `tomli-w` is taken as a **development dependency** for the round-trip test.
361
+
362
+ **Why pure-Python is load-bearing:** it keeps `PYTHONPATH=<plugin>/vendor python3 -m doc_marshal`
363
+ working, which means installing the plugin is the entire installation -- no pip, no uv, no venv,
364
+ and the plugin works in a repository whose maintainers have never heard of this tool. A single C
365
+ extension ends that.
366
+
367
+ ## 10. The Claude Code plugin
368
+
369
+ **Resolution order:** a repo-pinned `doc-marshal` first, the plugin's vendored copy as fallback. A
370
+ repo that pins gets the agent and CI validating against the same version; a repo that does not
371
+ still works. `doctor` reports which was resolved and flags a mismatch.
372
+
373
+ **SKILL.md is thin** -- roughly twenty lines: a description good enough for skill matching, then
374
+ an instruction to run `doc-marshal info --process` and follow it. The prototype's 274 lines of
375
+ process prose move into the package, where they are versioned with the engine and shared with
376
+ every other agent.
377
+
378
+ **The plugin's real value is the two hooks**, which no other harness provides: PostToolUse
379
+ validation of each note as it is written, and SessionStart injection. The extra Bash round-trip
380
+ before the agent knows the process is the price of the process matching the installed engine.
381
+
382
+ ## 11. Agent compatibility
383
+
384
+ Claude Code is the priority; the design stays vendor-neutral.
385
+
386
+ - `doc-marshal init` writes **`AGENTS.md`** by default -- the neutral surface, which Claude Code
387
+ also reads.
388
+ - `doc-marshal init --claude-code` writes **`CLAUDE.md`** instead, and additionally writes the
389
+ `.claude/settings.json` permission entries for `Bash(doc-marshal:*)` so the agent is not prompted
390
+ on every validator call.
391
+ - The flag generalises later to `--agent claude-code|codex|cursor`.
392
+
393
+ Either way the file is a pointer to `doc-marshal info --process`, so a Codex or Cursor user gets
394
+ the same process by the same route with no plugin at all. Non-Claude agents get no write-time
395
+ validation; pre-commit catches their errors, later.
396
+
397
+ ## 12. Enforcement points
398
+
399
+ | When | What runs | Effect |
400
+ | --- | --- | --- |
401
+ | every `Write`/`Edit` to a note | `check <that file>` via the plugin's PostToolUse hook | reports into the session; never blocks |
402
+ | every `git commit` | `check` on staged notes, `index`, via the pre-commit framework | errors block; regenerated files fail the hook for re-adding |
403
+ | every pull request | `check --all`, `index --check` | errors fail the build; a stale index warns |
404
+ | every pull request | `affected --format github` | annotates anchored notes; never fails |
405
+
406
+ **The pre-commit framework replaces the prototype's 218-line native hook**, which regenerated
407
+ `INDEX.md` and staged it into the same commit. The framework will not stage; a hook that modifies
408
+ files fails and you re-add. Accepted, because that is exactly how `black`, `ruff-format` and
409
+ `prettier` behave, so a public user meets a failure mode they have seen a hundred times rather than
410
+ a bespoke one -- and silently adding files to someone's commit is the more surprising of the two
411
+ behaviours. The invariant that matters, that no *pushed* branch carries a stale index, is held by
412
+ CI regardless. `git config core.hooksPath` is retired: no contributor ever runs it.
413
+
414
+ Neither pull-request job takes a `paths:` filter. Half of what they check is whether anchors still
415
+ resolve, and those break in the change that renames or deletes the code -- which by definition
416
+ touches no documentation.
417
+
418
+ CI calls `uvx doc-marshal check --all` directly. **No composite GitHub Action in 0.1**: it is sugar
419
+ over a three-line step, and a repository with no users does not need a marketplace listing.
420
+
421
+ ## 13. Stability contract
422
+
423
+ SemVer, plus pinning -- and pinning is free at every enforcement point: pre-commit by `rev:`, CI
424
+ by `uvx doc-marshal==0.5.*`.
425
+
426
+ **Minor releases may add checks.** The README says so plainly. A user who pinned does not see a
427
+ new check until they choose to bump, and upgrade-requires-fixes is a normal process when the
428
+ upgrade was a choice.
429
+
430
+ Rejected: a warnings-in-minors, errors-in-majors rollout ceremony. Pinning already solves the
431
+ problem it was designed for, and the ceremony would delay a genuinely important check by a major
432
+ release.
433
+
434
+ **The one real hazard** is the plugin's vendored copy, which updates on plugin install and is not
435
+ pinned by the repo -- so an agent could validate at 0.6 while CI runs 0.5. The §10 resolution
436
+ order exists for this, and `doctor` reports the mismatch.
437
+
438
+ **Tier 3 constants** -- the filename pattern, `SUMMARY_MAX`, the em-dash rule, the index and assets
439
+ directory names, the forbidden names, the excluded directories -- are **not configurable in 0.1**.
440
+ They are routed through one settings object anyway, so exposing them in 0.2 is a schema addition
441
+ rather than a refactor through six modules.
442
+
443
+ ## 14. Release plan
444
+
445
+ ### 0.1 -- the extraction
446
+
447
+ No TOML config. `standard` is the only ontology and it is hardcoded. Ships:
448
+
449
+ - the engine, registry-driven, with every check the prototype has
450
+ - the CLI of §6
451
+ - the thin plugin with both hooks and a vendored copy
452
+ - `.pre-commit-hooks.yaml`
453
+ - a README carrying the preset's prose
454
+ - MIT license, PyPI release
455
+
456
+ Fresh repository, **no history transfer**. A subtree split would capture only one of four source
457
+ directories -- the hooks, the commit hook and the workflows live elsewhere and are being dropped or
458
+ rewritten -- and the files that came across would be restructured into `src/`, giving renames on
459
+ top of a partial history. The initial commit records extraction from MakeRent `d664cb1`; the
460
+ reasoning stays readable in MakeRent's history, and most of it is in the docstrings regardless.
461
+
462
+ **Not dogfooded initially.** When it is, the split is: the package's prose documents the
463
+ convention, and the repository's own `agent-docs/` tree documents the implementation.
464
+
465
+ ### Then -- migrate MakeRent
466
+
467
+ The real integration test. MakeRent needs zero configurability, and running it exercises the
468
+ things paper cannot check: whether vendored-versus-pinned resolution is comprehensible, whether
469
+ `info`-instead-of-files works for an agent mid-task, and whether a thin SKILL.md still gets matched
470
+ and followed.
471
+
472
+ ### 0.2 -- configuration
473
+
474
+ The loader of §4, gated by the round-trip test. Brings with it `[rules]`, `exclude`, Tier 3, and
475
+ the `[types.context.structure]` overrides for `max_rows` and `max_chars`.
476
+
477
+ ## 15. Decision log
478
+
479
+ Each row is a decision taken in the design session, with the alternative it beat.
480
+
481
+ | # | Decision | Instead of |
482
+ | --- | --- | --- |
483
+ | 1 | The engine is the product; the ontology is a preset | selling the convention, with the validator as enforcement |
484
+ | 2 | Anchor fields are declarable, with a `resolves` kind | two hardcoded field names with hardcoded resolution asymmetry |
485
+ | 3 | `extends`, with the preset built in Python and TOML as an alternate constructor | shipping the preset as TOML for a forcing function -- a round-trip test buys that more cheaply |
486
+ | 4a | Per-type shallow merge, nested tables included | replace semantics, which silently strips unmentioned facets |
487
+ | 4b | `enabled = false` inside the type's table | a parallel `disable = [...]` list, per the ruff/eslint research |
488
+ | 4c | Weakening a built-in is permitted | freezing the preset, which makes its types immovable furniture |
489
+ | 5 | `[rules]` values with disabling as a value; `exclude` globs | no rule configuration at all -- the position collapsed, since `em_dash = false` is disabling a rule |
490
+ | 5b | **No inline suppression.** The one absolute prohibition | per-line ignores, which are unauditable at scale |
491
+ | 6 | Prose lives in the package, fetched by CLI | emitting it into the repo, with a staleness check and an ownership boundary |
492
+ | 7 | Markdown-primary output; compact `info` injected at SessionStart | JSON-primary, which would force arguments into fields |
493
+ | 8 | Dependency policy of three tests; nothing qualifies yet | zero-deps as dogma, or taking PyYAML because deps are allowed |
494
+ | 9 | `doc-marshal` for repo, package, module, CLI and config; the docs root stays `agent-docs/` | `agent-docs` for everything -- PyPI rejects it as too similar to the existing `agentdocs`, so the tool is named for what it does and the directory for what it holds |
495
+ | 10 | Vendor-neutral `AGENTS.md`, `--claude-code` for `CLAUDE.md` plus permissions | Claude-only, abandoning most of the public audience |
496
+ | 11 | pre-commit framework | the native hook, whose auto-staging is not worth the per-clone install step |
497
+ | 12 | SemVer and pinning; minors may add checks | a warnings-then-errors rollout ceremony |
498
+ | 13 | 0.1 is the extraction; config is 0.2 | building the configurable engine first |
499
+ | 14 | Commit-then-extract, fresh repo | `git subtree split`, which captures one of four source directories |
500
+ | 15 | Index preview reduced to folder names and counts | full injection (uncapped), a byte cap, or a note-count threshold with degradation |
501
+ | 16 | Uniform reduction, top level included | exempting top-level notes, reintroducing two renderings |
502
+ | 17 | No dogfooding initially; MIT | dogfooding from `git init` |
503
+
504
+ ## 16. Carried in from the prototype review
505
+
506
+ Findings from reviewing MakeRent `d664cb1`, to be handled during extraction.
507
+
508
+ - **`init` must scaffold `CONTEXT.md`.** The `context` type is `root_required`, so `check --all`
509
+ errors without it, and 0.1 has no config escape.
510
+ - **`max_rows = 20` and `max_chars = 6000` are hard errors in 0.1.** The README states plainly
511
+ that the vocabulary is deliberately small. 0.2 makes them overridable per §4.3.
512
+ - **`session_context.py`'s `REGENERATE`** is an f-string with no placeholders, and hardcodes the
513
+ skill path. Both disappear when it becomes `doc-marshal index`.
514
+ - **`check_structure` re-reads the file from disk** to measure size, though the caller already read
515
+ it. Thread the text through; whole-file measurement is correct, since the note is emitted with
516
+ its frontmatter.
517
+ - **`check_vocabulary`'s `\b{alias}\b`** misbehaves for an alias with a leading or trailing
518
+ non-word character (`.env`, `C++`) -- and a vocabulary is exactly where those appear.
519
+
520
+ ## 17. Validation
521
+
522
+ - [ ] **V1** -- `doc-marshal check --all` reproduces the prototype's output on MakeRent's tree,
523
+ note for note.
524
+ - [ ] **V2** -- the round-trip test passes: the `standard` preset serializes to TOML, loads back,
525
+ and compares equal. *(0.2)*
526
+ - [ ] **V3** -- the plugin validates a note in a repository with no `doc-marshal` installed, via
527
+ the vendored copy alone.
528
+ - [ ] **V4** -- `doctor` reports a deliberate version mismatch between a repo pin and the plugin's
529
+ vendored copy.
530
+ - [ ] **V5** -- MakeRent runs a full `/update-docs` cycle against the extracted tool, with the thin
531
+ SKILL.md, and the agent completes the process without the prose it used to carry.
532
+ - [ ] **V6** -- a fresh session's injected block is under 1000 characters on a 300-note tree.
533
+ - [ ] **V7** -- `pre-commit run --all-files` blocks a commit carrying an invalid note, and the
534
+ index regeneration fails for re-adding rather than silently staging.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "doc-marshal"
3
+ version = "0.0.1"
4
+ description = "A machine-checked documentation tree for repositories whose primary reader is a coding agent."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Andrew Root" }]
10
+ keywords = ["documentation", "linter", "agents", "claude", "docs-as-code"]
11
+ classifiers = [
12
+ "Development Status :: 2 - Pre-Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Topic :: Documentation",
16
+ "Topic :: Software Development :: Quality Assurance",
17
+ ]
18
+ # Deliberately empty, and not by dogma -- see SPEC.md section 9 for the three tests a runtime
19
+ # dependency has to pass, and why nothing passes them yet.
20
+ dependencies = []
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/rootdrew27/doc-marshal"
24
+ Source = "https://github.com/rootdrew27/doc-marshal"
25
+
26
+ [project.scripts]
27
+ doc-marshal = "doc_marshal.cli:main"
28
+
29
+ [dependency-groups]
30
+ dev = ["tomli-w>=1.0", "pytest>=8"]
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/doc_marshal"]
@@ -0,0 +1,7 @@
1
+ """A machine-checked documentation tree for repositories read by coding agents.
2
+
3
+ The design this package implements is recorded in SPEC.md at the repository root. Nothing here is
4
+ built yet.
5
+ """
6
+
7
+ __version__ = "0.0.1"
@@ -0,0 +1,7 @@
1
+ """`python -m doc_marshal`, so the package runs from a checkout or a vendored copy with no install."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,28 @@
1
+ """Command dispatch for `doc-marshal`.
2
+
3
+ One entry point rather than a directory of scripts, because every reference to this tool -- in
4
+ hooks, CI steps, agent-memory files and the convention's own prose -- names a verb instead of an
5
+ installation path. See SPEC.md section 6 for the surface this will grow.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+ from . import __version__
13
+
14
+
15
+ def main() -> int:
16
+ if "--version" in sys.argv[1:]:
17
+ print(f"doc-marshal {__version__}")
18
+ return 0
19
+ print(
20
+ f"doc-marshal {__version__} -- name reserved, not yet implemented.\n"
21
+ "The design is in SPEC.md: https://github.com/rootdrew27/doc-marshal",
22
+ file=sys.stderr,
23
+ )
24
+ return 1
25
+
26
+
27
+ if __name__ == "__main__":
28
+ sys.exit(main())