@nurix/etna 0.20.35 → 0.20.36
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.
- package/data/catalog/rules/changelog.md +1 -1
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +4 -4
- package/data/harnesses/code/agents/brainstorm.md +2 -2
- package/data/harnesses/code/agents/lld-mode.md +2 -2
- package/data/harnesses/code/agents/maintenance.md +17 -17
- package/data/harnesses/code/codex/agents/brainstorm.toml +6 -2
- package/data/harnesses/code/codex/agents/lld-mode.toml +2 -2
- package/data/harnesses/code/codex/agents/maintenance.toml +18 -17
- package/data/harnesses/code/docs/README.md +9 -17
- package/data/harnesses/code/docs/history/CHANGELOG.md +19 -18
- package/data/harnesses/code/docs/structure.md +60 -45
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/compliance.mjs +8 -2
- package/data/harnesses/code/hooks/lib/sweep.mjs +3 -2
- package/data/harnesses/code/rules/changelog.md +1 -1
- package/data/harnesses/code/skills/brainstorm/SKILL.md +4 -3
- package/data/harnesses/code/skills/brainstorm/references/harvest.md +1 -2
- package/data/harnesses/code/skills/documentation/SKILL.md +91 -142
- package/data/harnesses/code/skills/documentation/references/examples.md +35 -62
- package/data/harnesses/code/skills/documentation/references/frontmatter.md +7 -7
- package/data/harnesses/code/skills/documentation/references/source-comments.md +19 -0
- package/data/harnesses/code/skills/documentation/references/style-guide.md +21 -7
- package/data/harnesses/code/skills/documentation/references/untemplated.md +7 -6
- package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +178 -38
- package/data/harnesses/code/skills/documentation/templates/Readme.md +68 -41
- package/data/harnesses/code/skills/documentation/templates/acceptance-criteria.md +10 -18
- package/data/harnesses/code/skills/documentation/templates/api-reference.md +7 -2
- package/data/harnesses/code/skills/documentation/templates/architecture-decision.md +5 -5
- package/data/harnesses/code/skills/documentation/templates/backlog.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +9 -16
- package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/changelog-entry.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/changelog-index.md +21 -29
- package/data/harnesses/code/skills/documentation/templates/docs-readme.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/doctrine.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/domain-model.md +9 -4
- package/data/harnesses/code/skills/documentation/templates/environment-registry.md +6 -1
- package/data/harnesses/code/skills/documentation/templates/feature.md +4 -4
- package/data/harnesses/code/skills/documentation/templates/glossary.md +6 -1
- package/data/harnesses/code/skills/documentation/templates/handover.md +16 -16
- package/data/harnesses/code/skills/documentation/templates/implementation-map.md +9 -4
- package/data/harnesses/code/skills/documentation/templates/journal.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/memory-main.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/non-goals.md +7 -2
- package/data/harnesses/code/skills/documentation/templates/operations-procedure.md +6 -1
- package/data/harnesses/code/skills/documentation/templates/package.md +4 -8
- package/data/harnesses/code/skills/documentation/templates/plan.md +5 -11
- package/data/harnesses/code/skills/documentation/templates/protected-files.md +6 -1
- package/data/harnesses/code/skills/documentation/templates/root-readme.md +4 -4
- package/data/harnesses/code/skills/documentation/templates/security-overview.md +8 -3
- package/data/harnesses/code/skills/documentation/templates/setup.md +6 -6
- package/data/harnesses/code/skills/documentation/templates/system-overview.md +11 -6
- package/data/harnesses/code/skills/documentation/templates/system.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/testing-strategy.md +10 -5
- package/data/harnesses/code/skills/documentation/templates/ux-behavior.md +13 -2
- package/data/harnesses/code/skills/testing/SKILL.md +1 -1
- package/data/harnesses/delivery/CLAUDE.md +1 -1
- package/data/harnesses/delivery/agents/brainstorm.md +2 -2
- package/data/harnesses/delivery/agents/lld-mode.md +2 -2
- package/data/harnesses/delivery/agents/maintenance.md +17 -17
- package/data/harnesses/delivery/codex/agents/brainstorm.toml +6 -2
- package/data/harnesses/delivery/codex/agents/lld-mode.toml +2 -2
- package/data/harnesses/delivery/codex/agents/maintenance.toml +18 -17
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/rules/changelog.md +1 -1
- package/data/harnesses/delivery/skills/brainstorm/SKILL.md +4 -3
- package/data/harnesses/delivery/skills/brainstorm/references/harvest.md +1 -2
- package/data/harnesses/delivery/skills/documentation/SKILL.md +91 -142
- package/data/harnesses/delivery/skills/documentation/references/examples.md +35 -62
- package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +7 -7
- package/data/harnesses/delivery/skills/documentation/references/source-comments.md +19 -0
- package/data/harnesses/delivery/skills/documentation/references/style-guide.md +21 -7
- package/data/harnesses/delivery/skills/documentation/references/untemplated.md +7 -6
- package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +178 -38
- package/data/harnesses/delivery/skills/documentation/templates/Readme.md +68 -41
- package/data/harnesses/delivery/skills/documentation/templates/acceptance-criteria.md +10 -18
- package/data/harnesses/delivery/skills/documentation/templates/api-reference.md +7 -2
- package/data/harnesses/delivery/skills/documentation/templates/architecture-decision.md +5 -5
- package/data/harnesses/delivery/skills/documentation/templates/backlog.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +9 -16
- package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/changelog-entry.md +1 -1
- package/data/harnesses/delivery/skills/documentation/templates/changelog-index.md +21 -29
- package/data/harnesses/delivery/skills/documentation/templates/docs-readme.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/doctrine.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/domain-model.md +9 -4
- package/data/harnesses/delivery/skills/documentation/templates/environment-registry.md +6 -1
- package/data/harnesses/delivery/skills/documentation/templates/feature.md +4 -4
- package/data/harnesses/delivery/skills/documentation/templates/glossary.md +6 -1
- package/data/harnesses/delivery/skills/documentation/templates/handover.md +16 -16
- package/data/harnesses/delivery/skills/documentation/templates/implementation-map.md +9 -4
- package/data/harnesses/delivery/skills/documentation/templates/journal.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/memory-main.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/non-goals.md +7 -2
- package/data/harnesses/delivery/skills/documentation/templates/operations-procedure.md +6 -1
- package/data/harnesses/delivery/skills/documentation/templates/package.md +4 -8
- package/data/harnesses/delivery/skills/documentation/templates/plan.md +5 -11
- package/data/harnesses/delivery/skills/documentation/templates/protected-files.md +6 -1
- package/data/harnesses/delivery/skills/documentation/templates/root-readme.md +4 -4
- package/data/harnesses/delivery/skills/documentation/templates/security-overview.md +8 -3
- package/data/harnesses/delivery/skills/documentation/templates/setup.md +6 -6
- package/data/harnesses/delivery/skills/documentation/templates/system-overview.md +11 -6
- package/data/harnesses/delivery/skills/documentation/templates/system.md +1 -1
- package/data/harnesses/delivery/skills/documentation/templates/testing-strategy.md +10 -5
- package/data/harnesses/delivery/skills/documentation/templates/ux-behavior.md +13 -2
- package/data/harnesses/etna/CLAUDE.md +2 -2
- package/data/harnesses/etna/agents/brainstorm.md +2 -2
- package/data/harnesses/etna/agents/lld-mode.md +2 -2
- package/data/harnesses/etna/agents/maintenance.md +17 -17
- package/data/harnesses/etna/codex/agents/brainstorm.toml +6 -2
- package/data/harnesses/etna/codex/agents/lld-mode.toml +2 -2
- package/data/harnesses/etna/codex/agents/maintenance.toml +18 -17
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/rules/changelog.md +1 -1
- package/data/harnesses/etna/skills/brainstorm/SKILL.md +4 -3
- package/data/harnesses/etna/skills/brainstorm/references/harvest.md +1 -2
- package/data/harnesses/etna/skills/documentation/SKILL.md +91 -142
- package/data/harnesses/etna/skills/documentation/references/examples.md +35 -62
- package/data/harnesses/etna/skills/documentation/references/frontmatter.md +7 -7
- package/data/harnesses/etna/skills/documentation/references/source-comments.md +19 -0
- package/data/harnesses/etna/skills/documentation/references/style-guide.md +21 -7
- package/data/harnesses/etna/skills/documentation/references/untemplated.md +7 -6
- package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +178 -38
- package/data/harnesses/etna/skills/documentation/templates/Readme.md +68 -41
- package/data/harnesses/etna/skills/documentation/templates/acceptance-criteria.md +10 -18
- package/data/harnesses/etna/skills/documentation/templates/api-reference.md +7 -2
- package/data/harnesses/etna/skills/documentation/templates/architecture-decision.md +5 -5
- package/data/harnesses/etna/skills/documentation/templates/backlog.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +9 -16
- package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/changelog-entry.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/changelog-index.md +21 -29
- package/data/harnesses/etna/skills/documentation/templates/docs-readme.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/doctrine.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/domain-model.md +9 -4
- package/data/harnesses/etna/skills/documentation/templates/environment-registry.md +6 -1
- package/data/harnesses/etna/skills/documentation/templates/feature.md +4 -4
- package/data/harnesses/etna/skills/documentation/templates/glossary.md +6 -1
- package/data/harnesses/etna/skills/documentation/templates/handover.md +16 -16
- package/data/harnesses/etna/skills/documentation/templates/implementation-map.md +9 -4
- package/data/harnesses/etna/skills/documentation/templates/journal.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/memory-main.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/non-goals.md +7 -2
- package/data/harnesses/etna/skills/documentation/templates/operations-procedure.md +6 -1
- package/data/harnesses/etna/skills/documentation/templates/package.md +4 -8
- package/data/harnesses/etna/skills/documentation/templates/plan.md +5 -11
- package/data/harnesses/etna/skills/documentation/templates/protected-files.md +6 -1
- package/data/harnesses/etna/skills/documentation/templates/root-readme.md +4 -4
- package/data/harnesses/etna/skills/documentation/templates/security-overview.md +8 -3
- package/data/harnesses/etna/skills/documentation/templates/setup.md +6 -6
- package/data/harnesses/etna/skills/documentation/templates/system-overview.md +11 -6
- package/data/harnesses/etna/skills/documentation/templates/system.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/testing-strategy.md +10 -5
- package/data/harnesses/etna/skills/documentation/templates/ux-behavior.md +13 -2
- package/package.json +2 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Source comments — what stays in the file and what moves to docs
|
|
2
|
+
|
|
3
|
+
Load when: writing or sweeping source-file comments, or cutting a comment whose claim belongs in a document. Not needed to write a document.
|
|
4
|
+
|
|
5
|
+
**In `docs/`, never in the file's comments** — and this skill is the half of that rule that makes it safe. A source file carries a repo-relative path header of two or three present-tense sentences and one third-person sentence above each non-obvious function; the shape is the harness's `CLAUDE.md` § File Structure, which is canonical and is not restated here. That header's `docs/…` path is also the file's edge into the knowledge graph: the header pointer names the document that speaks for the file, which is how ownership is computed ([`frontmatter.md`](frontmatter.md)). What lands **here** is everything that shape displaces: design rationale, the alternatives weighed, the measurement behind a constant, the reason an obvious-looking change is wrong.
|
|
6
|
+
|
|
7
|
+
**Cutting a comment is a move, not a deletion.** A claim taken out of a source file is owed one of two things before the cut is done, and the choice is per claim, not per file:
|
|
8
|
+
|
|
9
|
+
| The claim is | What it earns |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Worth keeping, and its doc does not state it | A section in the **owning feature doc** (the ownership index resolves which), carrying the claim and the argument for why the codebase is worse without it — never the comment's wording, which was written for a reader holding the code |
|
|
12
|
+
| Already stated in its doc | Nothing — but you **open that doc and find the sentence** before saying so. An assumed duplicate is how a claim is lost |
|
|
13
|
+
| History, a ticket or decision-record reference, a dated note, a divider, or narration of code that no longer exists | Nothing, and no record. A record lives in `docs/history/`; these are not claims about the present |
|
|
14
|
+
|
|
15
|
+
**Count what you cut, and prove the cut was only comments.** A comment sweep states comment lines before and after per file, so a drop nobody accounted for is visible as a number rather than as a feeling. [`scripts/comment-strip.mjs`](../scripts/comment-strip.mjs) turns that from a claim into a check: it strips comments from both revisions — honouring strings, template and character literals, regex literals, raw strings and JSX comment braces — and diffs what is left, so a sweep that quietly changed executable code cannot pass as a cleanup. `comment-strip.mjs --rev <before> --to <after> <paths…>` exits 0 for "no executable code changed" and 1 naming the file and the first diverging line. Run it over any pass that touches more than a handful of files; a sweep without it is an assertion.
|
|
16
|
+
|
|
17
|
+
**The mechanical half of the shape is checked, not remembered.** [`scripts/source-lint.mjs`](../scripts/source-lint.mjs) `--changed` reports a header that names another path or none, a change whose added lines are over a quarter comments, and the four shapes the convention deletes outright — narration, ALL-CAPS emphasis, an over-long header, a comment block that has become a document. Block-severity findings must be fixed; the escape hatch is a `source-lint-disable: <id>` comment with the reason in a second comment beside it, and the only finding that legitimately takes one is a booby trap. The harness detector raises a bookkeeping row for the same condition, so an unfixed breach outlives the session that wrote it.
|
|
18
|
+
|
|
19
|
+
**Never convert a booby trap into a one-liner.** A comment whose subject is a danger on an edge that looks safe — a pinned dependency's limitation, a measurement that justifies a narrow allowlist, a two-clock interaction — is load-bearing where it sits, because the reader who needs it is mid-edit and will not go looking. Compress it if it rambles; do not relocate it. The tell is that the comment names a version, a measurement, or a condition under which it expires.
|
|
@@ -11,7 +11,7 @@ A document is read by a person who wants to understand something or to act on it
|
|
|
11
11
|
|
|
12
12
|
### Principles
|
|
13
13
|
|
|
14
|
-
1. **
|
|
14
|
+
1. **Three natures:** every document is a truth document (what is, now, rewritten in place), a record (what happened, dated and frozen) or a queue (work not done yet, rows appended and deleted when resolved). A truth document never narrates history, a record is never edited to match the present, and a queue holds only open rows, because a reader must be able to trust each without the others.
|
|
15
15
|
2. **The outside-in shape:** an explaining document opens on the question it answers and its boundary, then the model with its numbered principles, then the parts and their relations, then each part's decisions, and stops at its genre's deepest layer. Each layer is complete on its own, so a reader who stops at any layer holds a picture that is coarse but true, never a teaser the next section completes.
|
|
16
16
|
3. **The stability test:** a sentence that would have to change when a function is renamed, a file moves, a route, column, error code or cookie attribute changes, or a constant is retuned without the design changing belongs to the code, which the code graph computes on demand (`docs/architecture/systems/knowledge-layer.md`), and not to the document. A copy typed into a document is at best a duplicate of the code and at worst a stale one.
|
|
17
17
|
4. **The authority split:** the code is the authority on *what*; the document is the authority on *why*. A disagreement on *what* means the document is stale and the code wins; a disagreement on *why* or on an invariant is a defect to raise, because one side is wrong on purpose.
|
|
@@ -32,9 +32,10 @@ Every genre takes the same shape (principle 2) and the same labels (principle 5)
|
|
|
32
32
|
| Feature (`docs/features/`) | Truth | The behaviour a person meets, in full: each state the surface can be in, each edge case, what a refusal tells them, the wording where the wording is promised. This genre runs deepest toward its end, in Promises, because a promise is checked by a person or a test and the code cannot state it as intent | Columns, routes, guards, functions, files |
|
|
33
33
|
| Reference (API reference, environment registry) | Truth | The contract itself: signatures, shapes, variables | Nothing. The detail is the point |
|
|
34
34
|
| Operations procedure, migration runbook | Truth, record | The exact commands, in order, with their expected output | Nothing. The detail is the point |
|
|
35
|
-
| Record (plan, changelog entry,
|
|
35
|
+
| Record (plan, changelog entry, runbook) | Record | Its template's fields, as of the moment it describes | Nothing. Its prose follows § Voice and § Labels; the dates and the journey are its own |
|
|
36
|
+
| Queue (backlog, journal, bookkeeping, memory) | Queue | One row per open item, in its template's row grammar | Nothing. A row may carry its own date; a resolved row is deleted |
|
|
36
37
|
|
|
37
|
-
- **Owning documents:** feature, package and system are the three genres that speak for code: source files name them in their headers, and the ownership scan resolves paths to them (`frontmatter.md`). They take principles 5 and 6 in full, their sections are fixed (§ Owning documents — the body and the footer), and `doc-lint` reads them.
|
|
38
|
+
- **Owning documents:** feature, package and system are the three genres that speak for code: source files name them in their headers, and the ownership scan resolves paths to them (`frontmatter.md`). They take principles 5 and 6 in full, their sections are fixed (§ Owning documents — the body and the footer), and `doc-lint` reads them. A UX document also speaks for a surface's components where none of the three does; its sections are its own genre's (`templates/ux-behavior.md`).
|
|
38
39
|
- **Doctrine:** the same shape and labels as an owning document, speaking for no code and describing no mechanism. Its model layer may argue in paragraphs; below the model it is bullets like every other explaining document.
|
|
39
40
|
- **Reference documents and procedures:** the one place a signature, a shape, a variable table, a command list or a file path is the content. They keep the detail the owning genres hand to the graph (§ Reference documents).
|
|
40
41
|
- **Indexes:** a folder's `Readme.md` describes directories, never files — no link, filename or record ID per file; each file's own `description:` frontmatter is its account. The root README follows `templates/root-readme.md`. Both take § Labels.
|
|
@@ -51,6 +52,16 @@ Every truth document a person reads to understand something is written from the
|
|
|
51
52
|
4. **Each part in turn:** for each part, the decisions it rests on and why. A part's section opens with why the part exists, never with how it is built.
|
|
52
53
|
5. **The genre's deepest layer:** the row for the genre in § The genres, and nothing deeper.
|
|
53
54
|
|
|
55
|
+
An owning or doctrine document's fixed sections carry these layers; nothing is added beside them to hold a layer:
|
|
56
|
+
|
|
57
|
+
| Layer | Feature | System | Package | Doctrine |
|
|
58
|
+
| --- | --- | --- | --- | --- |
|
|
59
|
+
| The question and its boundary | Goal line, Scope | Opening paragraph, Scope | Identity and goal lines, Scope | Opening paragraph |
|
|
60
|
+
| The model | Scope's Covers bullet | Flow's opening | Usage's opening | The model |
|
|
61
|
+
| The parts | Flows | Flow | Ownership, Usage | The parts |
|
|
62
|
+
| Each part's decisions | Decisions | Decisions | Decisions | The parts in depth |
|
|
63
|
+
| The deepest layer | Promises | Promises | Promises | — |
|
|
64
|
+
|
|
54
65
|
### The stability test
|
|
55
66
|
|
|
56
67
|
The test (principle 3) decides what a sentence may say. These survive it:
|
|
@@ -62,7 +73,9 @@ The test (principle 3) decides what a sentence may say. These survive it:
|
|
|
62
73
|
- **Cross-component contracts:** what no single file shows, such as two clients that must classify the same answer identically.
|
|
63
74
|
- **Decided-but-unbuilt intent:** what the code cannot reveal because it does not yet do it, wrapped in `<!-- UNBUILT:START — {what, and the evidence it is not built} -->` … `<!-- UNBUILT:END -->` in plain words. A design decided *against* carries no marker.
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
Unbuilt work has one home per kind. Decided design the code does not do yet is an `UNBUILT` block where the design is stated; a shortfall in what is built (a promise the code does not keep, a case it mishandles) is one bullet under Gaps; the work that would close either is a backlog row, and the document never tracks that work's progress.
|
|
77
|
+
|
|
78
|
+
The authority split (principle 4) is stated once, where agents enter the docs (the root instruction file or `docs/README.md`), so a reader who finds the code and the document disagreeing knows which way to resolve it. A doctrine document restates it in its model where its readers would otherwise not know; a feature, package or system document never does, because its opening is fixed to its goal.
|
|
66
79
|
|
|
67
80
|
### Diagrams and length by layer
|
|
68
81
|
|
|
@@ -144,11 +157,12 @@ A feature adds its pages, because an address is what a person points at; a packa
|
|
|
144
157
|
|
|
145
158
|
## The Two Natures — tense and history
|
|
146
159
|
|
|
147
|
-
Every document is a truth document or a
|
|
160
|
+
Every document is a truth document, a record or a queue (principle 1); `SKILL.md` § The three natures gives the split and the genre registry (`templates/Readme.md`) gives each genre's nature. The tense rules follow directly.
|
|
148
161
|
|
|
149
|
-
- **Truth documents:**
|
|
162
|
+
- **Truth documents:** every genre the registry marks `truth` states what the system *is* and how it works *today*, as if it had always been so. The journey (dates, migrations, progress) lives only in records.
|
|
150
163
|
- **The one bridge:** an Architecture Decision's **Decision Changelog** tail carries that decision's dated revision history while its body stays current-truth-only; head is truth, tail is provenance. No other truth document carries a dated line, apart from the frontmatter's tooling keys and a dated path inside a link or code span, which cites a record rather than claiming a date. The `maintenance` agent's Health Log is a record of its own (`docs/history/health-log.md`), not a tail.
|
|
151
164
|
- **Records:** never edited to match the present. A frozen plan stays wrong-in-hindsight; a changelog entry describes what shipped as it shipped. A scope change gets a new dated record that links back.
|
|
165
|
+
- **Queues:** a row is appended when the work appears and deleted when it is done, drained or promoted, never marked done in place. A row may carry its date; nothing else in a queue is edited.
|
|
152
166
|
|
|
153
167
|
| ❌ Historical narration (a record's) | ✅ Present-tense fact (the truth document's) |
|
|
154
168
|
| --- | --- |
|
|
@@ -239,7 +253,7 @@ flowchart LR
|
|
|
239
253
|
- **Retrieval keys:** search and agents retrieve a section out of context, where a bare `### Failures` reads identically across every document. A generic `###` heading, and every heading in an untemplated document, is qualified by its topic (`### Sign-in failures`, `### Merge rules — data sync`) so it stands alone when retrieved. A short document whose H1 disambiguates can keep bare headings.
|
|
240
254
|
- **Sentence case:** headings are sentence case; a product, a proper name or a named concept (`The Two Natures`) keeps its own casing.
|
|
241
255
|
- **No hard-wrapped prose:** a paragraph is one line in the source (`.prettierrc.json` keeps `proseWrap: never`); the renderer wraps it.
|
|
242
|
-
- **No empty stubs:** a section with nothing real to say is omitted; an explicit "None" is written only where the absence itself is information (`SKILL.md` §
|
|
256
|
+
- **No empty stubs:** a section with nothing real to say is omitted; an explicit "None" is written only where the absence itself is information (`SKILL.md` § Truth documents).
|
|
243
257
|
- **No file rosters:** a file list belongs in no document. The ownership index and the code graph answer "which file" for every genre, and a folder index describes directories, never files.
|
|
244
258
|
- **The reference-document layout:** a document that is not an owning genre and whose content is the contract itself (an API reference, an environment registry, a procedure's appendix) opens on its goal, as a `**Goal:**` line or one sentence, and proceeds straight to its contract or procedure sections:
|
|
245
259
|
|
|
@@ -6,10 +6,11 @@ A template is a pre-made set of shape decisions. With no template, make the same
|
|
|
6
6
|
|
|
7
7
|
## 1. Decide the nature — truth or record
|
|
8
8
|
|
|
9
|
-
Every document is exactly one (`SKILL.md` § The
|
|
9
|
+
Every document is exactly one (`SKILL.md` § The three natures). Ask: *when reality changes, is this file rewritten, frozen, or does it lose a row?*
|
|
10
10
|
|
|
11
|
-
- Rewritten in place → **truth document**. Topic-named (`kebab-case.md`), lives outside `docs/history/`, present tense throughout — the iron rule in `SKILL.md` §
|
|
12
|
-
-
|
|
11
|
+
- Rewritten in place → **truth document**. Topic-named (`kebab-case.md`), lives outside `docs/history/`, present tense throughout — the iron rule in `SKILL.md` § The three natures.
|
|
12
|
+
- A list of open work whose rows are added and deleted → **queue**. A fixed name, one row per item; extend an existing queue before inventing one.
|
|
13
|
+
- Frozen → **record**. Date-named (`YYYY-MM-DD-slug.md`), lives in `docs/history/` — inside an existing subfolder where it fits (an investigation or research writeup → `history/research/{topic}/`); a genuinely new category of record needs a `docs/structure.md` folder-rule row in the same change, never an invented folder.
|
|
13
14
|
|
|
14
15
|
When a draft wants to be both — the current state *and* the story of getting there — split it: the state becomes a truth document, the story becomes a record that links to it. Never mix the natures in one body.
|
|
15
16
|
|
|
@@ -17,7 +18,7 @@ When a draft wants to be both — the current state *and* the story of getting t
|
|
|
17
18
|
|
|
18
19
|
- `docs/structure.md` is the placement authority — match the document's topic to its folder map before inventing a location.
|
|
19
20
|
- **One canonical home per topic.** If a document on the topic already exists, extend it; a second document on the same topic is drift waiting to happen. Link, don't copy.
|
|
20
|
-
- A new topic *folder* gets a `Readme.md` landing page that
|
|
21
|
+
- A new topic *folder* gets a `Readme.md` landing page that describes what the folder holds and how its files are named, never a row per file (`type: folder-index`).
|
|
21
22
|
|
|
22
23
|
## 3. Shape it
|
|
23
24
|
|
|
@@ -35,6 +36,6 @@ When a draft wants to be both — the current state *and* the story of getting t
|
|
|
35
36
|
|
|
36
37
|
## 5. With a human in the loop — the co-authoring workflow
|
|
37
38
|
|
|
38
|
-
**Stage 1 — context gathering.**
|
|
39
|
+
**Stage 1 — context gathering.** Read what already exists on the topic, then draft the structure from §1–3 with sensible defaults for audience, purpose and format. Ask only the questions whose answer the repository cannot give and a default would get wrong (usually one to three), each with your recommended answer, in one message.
|
|
39
40
|
|
|
40
|
-
**Stage 2 —
|
|
41
|
+
**Stage 2 — draft.** Show the structure with the questions, then draft the whole document on the answers or the defaults. The user indicates changes — learn their style from their edits; don't silently rewrite them.
|
|
@@ -1,49 +1,147 @@
|
|
|
1
|
-
// skills/documentation/scripts/doc-lint.mjs - mechanical checks over markdown docs (tense narration, dates, stubs,
|
|
1
|
+
// skills/documentation/scripts/doc-lint.mjs - mechanical checks over markdown docs (tense and history narration, dates, redirect stubs, frontmatter, index shape, owning-document body)
|
|
2
2
|
// Usage: node doc-lint.mjs <file.md ...> | node doc-lint.mjs --changed (git-changed + untracked .md)
|
|
3
|
-
// Exit 1 when any block-severity finding survives. Escape hatch: <!-- doc-lint-disable: id[, id] -->
|
|
3
|
+
// Exit 1 when any block-severity finding survives. Escape hatch: <!-- doc-lint-disable: id[, id] --> alone on its
|
|
4
|
+
// own line (file-wide, with a reason comment beside it) or at the end of the offending line (that line only).
|
|
5
|
+
// Genre facts (nature, type, location, required keys and sections) come from ../templates/Readme.md § Registry.
|
|
4
6
|
|
|
5
7
|
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
6
8
|
import { execSync } from 'node:child_process';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const MONTH = '(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)';
|
|
12
|
+
const DATE = new RegExp(`\\b20\\d{2}[-/.]\\d{1,2}[-/.]\\d{1,2}\\b|\\b\\d{1,2}/\\d{1,2}/20\\d{2}\\b|\\b${MONTH}\\.? (\\d{1,2},? )?20\\d{2}\\b|\\b\\d{1,2} ${MONTH}\\.? 20\\d{2}\\b`);
|
|
7
13
|
|
|
8
14
|
const RULES = [
|
|
9
15
|
{ id: 'stale-tripwire', severity: 'block', scope: 'truth', pattern: /treat (any )?[\w`./-]+ ?(references? )?as stale|superseded (by|the)|\bused to (be|target|live|hold)\b/i, message: 'stale-reference tripwire / supersession talk — fix the references, state the present fact' },
|
|
10
|
-
{ id: 'history-narration', severity: 'block', scope: 'truth', pattern: /\
|
|
16
|
+
{ id: 'history-narration', severity: 'block', scope: 'truth', pattern: /\b(was|were|has been|have been) (renamed|extracted|folded|migrated|replaced|superseded)\b|\bmoved (to|from|into) [\w`./-]+ \(?20\d{2}/i, message: 'history narration — the journey belongs in a dated record' },
|
|
17
|
+
// A ticked box in a committed checklist or procedure records one run's state inside a document about the present.
|
|
18
|
+
{ id: 'ticked-checkbox', severity: 'warn', scope: 'truth', only: /(^|\/)docs\/(tests|operations|acceptance)\//, pattern: /^\s*[-*] \[[xX]\]/, message: 'ticked box in a truth document — a run\'s result belongs to the changelog entry of the unit that ran it; commit the box unticked' },
|
|
11
19
|
{ id: 'progress-marker', severity: 'block', scope: 'truth', pattern: /\bphases?\s+\d[^.\n]{0,40}\b(complete|done)\b|\b(build|implementation) (is )?(\d+%|partially) (complete|done)\b/i, message: 'progress marker — progress lives in the plan/backlog record' },
|
|
12
20
|
{ id: 'temporal-hedge', severity: 'warn', scope: 'truth', pattern: /\bnot yet\b|\bfor now\b|\bcurrently\b|\bstill (unsupported|missing|pending|unimplemented|in progress)\b/i, message: 'temporal hedge — state the fact without "currently / not yet / for now"' },
|
|
13
|
-
// "previously-fetched" is a hyphenated adjective naming a thing, not a predecessor design — don't fire on it.
|
|
14
|
-
|
|
21
|
+
// "previously-fetched" is a hyphenated adjective naming a thing, not a predecessor design — don't fire on it. "No longer"
|
|
22
|
+
// and "was removed" also describe a state a person meets ("a removed member no longer sees the panel"), so they warn.
|
|
23
|
+
{ id: 'predecessor-frame', severity: 'warn', scope: 'truth', pattern: /\bpreviously\b(?!-)|\bthe old \w+\b|\blegacy \w+ (was|is)\b|\bno longer\b|\bwas removed\b|\bv\d+(\.\d+)*\s*(→|->)\s*v?\d+/i, message: 'predecessor framing — describe the one current design' },
|
|
15
24
|
// docs/tests/ behavioral checklists use `- [ ]` as test-step notation (step → **Assert**) and docs/operations/
|
|
16
25
|
// procedures use it for operator verification steps — neither is progress tracking. Every other truth doc still bans checkboxes.
|
|
17
|
-
{ id: 'status-glyph', severity: 'warn', scope: 'truth', exempt: /(^|\/)docs\/(tests|operations)\//, pattern: /(^|\s)(✅|⏳|- \[[
|
|
26
|
+
{ id: 'status-glyph', severity: 'warn', scope: 'truth', exempt: /(^|\/)docs\/(tests|operations|acceptance)\//, pattern: /(^|\s)(✅|⏳|- \[[ xX]\](?!\())/u, message: 'status glyph / checklist in a truth doc — tracking belongs in a record' },
|
|
18
27
|
// Truth docs are supposed to cite records, and record filenames are dated — so a date inside a link
|
|
19
|
-
// target
|
|
20
|
-
{ id: 'date-in-truth-doc', severity: 'block', scope: 'truth', strip: /\[[^\]]*\]\([^)]*\)
|
|
28
|
+
// target is a path, not narration. Code spans are stripped before every rule.
|
|
29
|
+
{ id: 'date-in-truth-doc', severity: 'block', scope: 'truth', strip: /\[[^\]]*\]\([^)]*\)|^(last-reviewed|last-updated|rev|harness-version):.*$/gm, pattern: DATE, message: 'date in a truth doc — cite the record by name and link; the closed exceptions are the frontmatter tooling keys, an AD Decision Changelog tail and a dated path inside a link or code span' },
|
|
21
30
|
];
|
|
22
31
|
|
|
23
|
-
//
|
|
24
|
-
const BRIDGE_HEADINGS = /^#{1,4}\s+
|
|
25
|
-
// Instruction files, the three harness-installed mirrors
|
|
26
|
-
//
|
|
27
|
-
|
|
32
|
+
// The one section inside a truth doc where dated history is sanctioned: an AD's Decision Changelog.
|
|
33
|
+
const BRIDGE_HEADINGS = /^#{1,4}\s+decision changelog\b/i;
|
|
34
|
+
// Instruction files, the three harness-installed mirrors, generated trees and the hook-written bookkeeping queue
|
|
35
|
+
// (hooks/lib/state.mjs, a fixed header and a dated row grammar) are out of scope; the authoring roots are anchored
|
|
36
|
+
// to the repo root.
|
|
37
|
+
const SKIP = /^(\.claude|\.agents|\.codex|skills|rules|agents|harness)\/|(^|\/)(node_modules|packages\/[^/]+\/data)\/|(^|\/)(CLAUDE|AGENTS)\.md$|\.bkp\.|(^|\/)docs\/operations\/bookkeeping\.md$/;
|
|
28
38
|
// `operations/issues/YYYY-MM-DD-{slug}.md` is a sanctioned record genre (`type: issue`, the
|
|
29
39
|
// `issue-bridge` skill's local pack) living outside the record folders, so it is named here too —
|
|
30
40
|
// graded as a truth document it fails the date rule for carrying the date its filename already does.
|
|
31
41
|
const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\/|(^|\/)docs\/operations\/release-notes\/(?!readme\.md$)[^/]+\.md$/i;
|
|
32
42
|
|
|
33
|
-
const RECORD_TYPES = /^(changelog-entry|plan|migration|migration-runbook|research|brainstorm|journal|record|ledger|issue|triage|project-memory|agent-memory)$/;
|
|
43
|
+
const RECORD_TYPES = /^(changelog-entry|plan|migration|migration-runbook|research|brainstorm|journal|record|ledger|issue|triage|handover|project-memory|agent-memory)$/;
|
|
34
44
|
const TRUTH_LOCATIONS = /(^|\/)docs\/(features|architecture|tests|acceptance)\//;
|
|
45
|
+
// Types other skills stamp on documents this registry does not template (product-dna's ledger, generic records).
|
|
46
|
+
const EXTERNAL_TYPES = /^(ledger|record|migration|migration-runbook|changelog-entry|plan|journal)$/;
|
|
47
|
+
|
|
48
|
+
// The genre registry (templates/Readme.md § Registry), one entry per row, or null when the skill's templates are
|
|
49
|
+
// not installed beside this script — every registry check then skips, and scope falls back to the path rules.
|
|
50
|
+
function loadRegistry() {
|
|
51
|
+
let text;
|
|
52
|
+
try { text = readFileSync(new URL('../templates/Readme.md', import.meta.url), 'utf8'); } catch { return null; }
|
|
53
|
+
const at = text.search(/^## Registry\s*$/m);
|
|
54
|
+
if (at === -1) return null;
|
|
55
|
+
const ticks = (cell) => [...cell.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
|
|
56
|
+
const rows = [];
|
|
57
|
+
for (const line of text.slice(at).split('\n').slice(1)) {
|
|
58
|
+
if (/^## /.test(line)) break;
|
|
59
|
+
if (!line.startsWith('|') || /^\|\s*(Genre|-{3})/.test(line)) continue;
|
|
60
|
+
const cells = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
61
|
+
if (cells.length !== 7) continue;
|
|
62
|
+
const [genre, , nature, type, location, keys, sections] = cells;
|
|
63
|
+
const where = ticks(location)[0] || null;
|
|
64
|
+
rows.push({
|
|
65
|
+
genre,
|
|
66
|
+
nature,
|
|
67
|
+
type: ticks(type)[0] || null,
|
|
68
|
+
location: where,
|
|
69
|
+
match: where ? locationPattern(where) : null,
|
|
70
|
+
// Literal characters outside placeholders: the most specific location wins a tie (`{surface}-ux.md` over `{feature}.md`).
|
|
71
|
+
specificity: where ? where.replace(/\{[^}]+\}|YYYY-MM-DD|NNN|X\.Y\.Z/g, '').length : 0,
|
|
72
|
+
keys: ticks(keys),
|
|
73
|
+
sections: sections === '—' ? [] : sections.split('·').map((x) => x.trim()).filter(Boolean),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return rows.length ? rows : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A registry location as a path RegExp: `{name}` is one segment, the date and number placeholders their digits, a trailing slash any file beneath.
|
|
80
|
+
function locationPattern(location) {
|
|
81
|
+
const body = location
|
|
82
|
+
.replace(/[.+?^$()[\]\\|]/g, '\\$&')
|
|
83
|
+
.replace(/\\\{[^}]+\}|\{[^}]+\}/g, '[^/]+')
|
|
84
|
+
.replace(/YYYY-MM-DD/g, '\\d{4}-\\d{2}-\\d{2}')
|
|
85
|
+
.replace(/NNN/g, '\\d{3}')
|
|
86
|
+
.replace(/X\\\.Y\\\.Z/g, '[\\d.]+');
|
|
87
|
+
// A location with no folder (the root README) sits at the repo root only, never at any depth.
|
|
88
|
+
return new RegExp(`${location.includes('/') ? '(^|/)' : '^(\\./)?'}${body}${location.endsWith('/') ? '' : '$'}`, 'i');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const REGISTRY = loadRegistry();
|
|
92
|
+
|
|
93
|
+
// The registry row a document belongs to: its declared `type:` first (the location breaking a tie between genres
|
|
94
|
+
// sharing a type), else the most specific location it sits at; null when neither names one.
|
|
95
|
+
function genreOf(path, text) {
|
|
96
|
+
if (!REGISTRY) return null;
|
|
97
|
+
const declared = text === undefined ? null : declaredType(text);
|
|
98
|
+
// A folder's Readme is an index whatever folder it sits in, and only an index location can claim one.
|
|
99
|
+
const isIndex = /(^|\/)readme\.md$/i.test(path);
|
|
100
|
+
const at = REGISTRY.filter((r) => r.match && r.match.test(path) && /(^|\/)readme\.md$/i.test(r.location) === isIndex)
|
|
101
|
+
.sort((a, b) => b.specificity - a.specificity);
|
|
102
|
+
if (declared) {
|
|
103
|
+
const typed = REGISTRY.filter((r) => r.type === declared);
|
|
104
|
+
return typed.find((r) => at.includes(r)) || typed[0] || null;
|
|
105
|
+
}
|
|
106
|
+
return at[0] || null;
|
|
107
|
+
}
|
|
35
108
|
|
|
36
109
|
// The genre a document declares for itself, from its own frontmatter `type:` key, or null.
|
|
37
110
|
function declaredType(text) {
|
|
38
111
|
if (!text.startsWith('---')) return null;
|
|
39
112
|
const end = text.indexOf('\n---', 3);
|
|
40
|
-
const m = /^type:\s*(\S+)\s
|
|
41
|
-
return m ? m[1] : null;
|
|
113
|
+
const m = /^type:\s*(\S+)\s*(#.*)?$/m.exec(end === -1 ? text : text.slice(0, end));
|
|
114
|
+
return m ? m[1].replace(/^["']|["']$/g, '') : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Top-level frontmatter keys and their raw values (a block list joined one item per line), or null with no block.
|
|
118
|
+
function frontmatter(text) {
|
|
119
|
+
if (!/^---\r?\n/.test(text)) return null;
|
|
120
|
+
const end = text.indexOf('\n---', 3);
|
|
121
|
+
if (end === -1) return null;
|
|
122
|
+
const keys = new Map();
|
|
123
|
+
let last = null;
|
|
124
|
+
for (const line of text.slice(text.indexOf('\n') + 1, end).split('\n')) {
|
|
125
|
+
const key = /^([A-Za-z][\w-]*):[ \t]*(.*)$/.exec(line);
|
|
126
|
+
if (key) { last = key[1]; keys.set(last, key[2].replace(/\s+#.*$/, '').trim()); continue; }
|
|
127
|
+
const item = /^\s+-\s+(.*)$/.exec(line);
|
|
128
|
+
if (item && last) keys.set(last, [keys.get(last), item[1].replace(/\s+#.*$/, '').trim()].filter(Boolean).join('\n'));
|
|
129
|
+
}
|
|
130
|
+
return keys;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A frontmatter value as a list: a flow list, a block list, or one scalar; quotes dropped, empties removed.
|
|
134
|
+
function listValue(raw) {
|
|
135
|
+
if (!raw) return [];
|
|
136
|
+
const items = raw.startsWith('[') ? raw.replace(/^\[|\]$/g, '').split(',') : raw.split('\n');
|
|
137
|
+
return items.map((v) => v.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
42
138
|
}
|
|
43
139
|
|
|
44
|
-
// Classify a document: record or
|
|
140
|
+
// Classify a document: truth, record, queue or out of scope — the registry's nature first, then the path rules.
|
|
45
141
|
function scopeOf(path, text) {
|
|
46
142
|
if (SKIP.test(path)) return 'skip';
|
|
143
|
+
const genre = genreOf(path, text);
|
|
144
|
+
if (genre) return genre.nature === 'truth' ? 'truth' : genre.nature === 'queue' ? 'queue' : 'record';
|
|
47
145
|
if (RECORD.test(path)) return 'record';
|
|
48
146
|
const declared = text === undefined ? null : declaredType(text);
|
|
49
147
|
return declared && RECORD_TYPES.test(declared) ? 'record' : 'truth';
|
|
@@ -80,14 +178,27 @@ function proseLines(text) {
|
|
|
80
178
|
return out;
|
|
81
179
|
}
|
|
82
180
|
|
|
83
|
-
// Collect file-wide disabled rule ids
|
|
181
|
+
// Collect file-wide disabled rule ids: a <!-- doc-lint-disable: a, b --> comment alone on its own line. One that shares
|
|
182
|
+
// a line with prose disables that line only, and is honoured where each rule reads its lines.
|
|
84
183
|
function fileDisables(text) {
|
|
85
184
|
const ids = new Set();
|
|
86
|
-
for (const m of text.matchAll(
|
|
185
|
+
for (const m of text.matchAll(/^\s*<!--\s*doc-lint-disable:\s*([\w-,\s]+?)\s*-->\s*$/gm))
|
|
87
186
|
m[1].split(',').forEach((id) => ids.add(id.trim()));
|
|
88
187
|
return ids;
|
|
89
188
|
}
|
|
90
189
|
|
|
190
|
+
// File-wide disables with no reason beside them: no comment on the next non-blank line, the way SKILL.md asks.
|
|
191
|
+
function unexplainedDisables(text) {
|
|
192
|
+
const lines = text.split('\n');
|
|
193
|
+
const out = [];
|
|
194
|
+
lines.forEach((line, i) => {
|
|
195
|
+
if (!/^\s*<!--\s*doc-lint-disable:[^>]*-->\s*$/.test(line)) return;
|
|
196
|
+
const next = lines.slice(i + 1).find((l) => l.trim());
|
|
197
|
+
if (!next || !/^\s*<!--/.test(next) || /doc-lint-disable:/.test(next)) out.push({ n: i + 1, line });
|
|
198
|
+
});
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
91
202
|
function lintFile(path, findings) {
|
|
92
203
|
if (SKIP.test(path) || !existsSync(path)) return;
|
|
93
204
|
const text = readFileSync(path, 'utf8');
|
|
@@ -99,11 +210,17 @@ function lintFile(path, findings) {
|
|
|
99
210
|
if (scope === 'truth' && nonEmpty <= 10 && /\b(moved to|relocated to|see [\w`./-]+ instead)\b/i.test(text) && !disabled.has('redirect-doc'))
|
|
100
211
|
findings.push({ path, n: 1, severity: 'block', id: 'redirect-doc', message: 'redirect stub — delete the file and repoint every inbound link' });
|
|
101
212
|
|
|
213
|
+
for (const { n, line } of unexplainedDisables(text))
|
|
214
|
+
if (!disabled.has('disable-without-reason')) findings.push({ path, n, severity: 'warn', id: 'disable-without-reason', message: 'file-wide doc-lint-disable with no reason comment on the next line — say why the rule does not apply here', excerpt: line.trim().slice(0, 90) });
|
|
215
|
+
|
|
102
216
|
for (const rule of RULES) {
|
|
103
217
|
if (rule.scope !== scope || disabled.has(rule.id)) continue;
|
|
104
218
|
if (rule.exempt && rule.exempt.test(path)) continue;
|
|
219
|
+
if (rule.only && !rule.only.test(path)) continue;
|
|
105
220
|
for (const { n, line } of lines) {
|
|
106
|
-
|
|
221
|
+
// A code span quotes a name or a phrase rather than saying it, so no tense rule reads inside one.
|
|
222
|
+
const bare = line.replace(/`[^`]*`/g, '');
|
|
223
|
+
const subject = rule.strip ? bare.replace(rule.strip, '') : bare;
|
|
107
224
|
if (!rule.pattern.test(subject)) continue;
|
|
108
225
|
if (line.includes(`doc-lint-disable: ${rule.id}`) || new RegExp(`doc-lint-disable:[^>]*\\b${rule.id}\\b`).test(line)) continue;
|
|
109
226
|
findings.push({ path, n, severity: rule.severity, id: rule.id, message: rule.message, excerpt: line.trim().slice(0, 90) });
|
|
@@ -120,14 +237,16 @@ function lintFile(path, findings) {
|
|
|
120
237
|
// A folder index (Readme.md/README.md) is a browse page, not a feature doc — it owns no code.
|
|
121
238
|
const isFolderIndex = /(^|\/)(readme)\.md$/i.test(path);
|
|
122
239
|
|
|
123
|
-
// An index names folders, never files (SKILL.md §
|
|
240
|
+
// An index names folders, never files (SKILL.md § Indexes and descriptions). A link from a folder index or
|
|
124
241
|
// the authority map to an individual document is a row that must be edited on every add, rename
|
|
125
242
|
// and retirement; the only files an index may link are the mandated scaffolds and a sibling
|
|
126
243
|
// folder's own Readme.
|
|
127
244
|
const isAuthorityMap = /(^|\/)docs\/structure\.md$/.test(path);
|
|
128
245
|
const isRootReadme = /^(\.\/)?README\.md$/i.test(path.replace(process.cwd() + '/', ''));
|
|
129
|
-
|
|
130
|
-
|
|
246
|
+
// Only the docs tree's indexes and the repo-root README are graded as indexes: a package's README links its own CONTRIBUTING.md.
|
|
247
|
+
const inDocs = /(^|\/)docs\//.test(path);
|
|
248
|
+
if (scope === 'truth' && ((isFolderIndex && (inDocs || isRootReadme)) || isAuthorityMap) && !disabled.has('index-lists-files')) {
|
|
249
|
+
const SCAFFOLD = /(^|\/)(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|contributing|license|security|code_of_conduct)\.md$|(^|\/)operations\/setup\.md$/i;
|
|
131
250
|
for (const { n, line } of lines) {
|
|
132
251
|
if (line.includes('doc-lint-disable: index-lists-files')) continue;
|
|
133
252
|
for (const m of line.matchAll(/\]\(([^)\s#]+\.md)(#[^)]*)?\)/g)) {
|
|
@@ -142,18 +261,16 @@ function lintFile(path, findings) {
|
|
|
142
261
|
}
|
|
143
262
|
}
|
|
144
263
|
// A folder index describes directories; a drawn tree is a file roster in another costume
|
|
145
|
-
// (SKILL.md § Indexes
|
|
264
|
+
// (SKILL.md § Indexes and descriptions). structure.md is the authority map and keeps its tree.
|
|
146
265
|
if (scope === 'truth' && isFolderIndex && (isRootReadme || /(^|\/)docs\//.test(path)) && !disabled.has('index-file-tree')) {
|
|
147
266
|
// Raw lines, not proseLines: a tree is drawn inside a code fence, which proseLines drops. The repo-root README routes by a Map table; its tree is docs/structure.md's too (templates/root-readme.md).
|
|
148
267
|
const rawLines = text.split('\n').map((line, i) => ({ n: i + 1, line }));
|
|
149
268
|
const tree = rawLines.find(({ line }) => /^\s*[│├└]/.test(line));
|
|
150
269
|
if (tree) findings.push({ path, n: tree.n, severity: 'block', id: 'index-file-tree', message: 'a tree drawn in a folder index — describe each directory in a sentence; the tree is docs/structure.md\'s', excerpt: tree.line.trim().slice(0, 90) });
|
|
151
270
|
}
|
|
152
|
-
// A file or record named without a link is the same per-file row as a link (SKILL.md §
|
|
271
|
+
// A file or record named without a link is the same per-file row as a link (SKILL.md § Indexes and descriptions).
|
|
153
272
|
// docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
|
|
154
273
|
const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
|
|
155
|
-
// The bookkeeping queue is written by the harness hooks from a fixed header (hooks/lib/state.mjs); it carries no frontmatter.
|
|
156
|
-
const isHookQueue = /(^|\/)docs\/operations\/bookkeeping\.md$/.test(path);
|
|
157
274
|
if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
|
|
158
275
|
const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
|
|
159
276
|
for (const { n, line } of lines) {
|
|
@@ -164,7 +281,7 @@ function lintFile(path, findings) {
|
|
|
164
281
|
}
|
|
165
282
|
}
|
|
166
283
|
// Every truth document and folder index under docs/ describes itself in at most 300 characters (references/frontmatter.md § description:).
|
|
167
|
-
if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !
|
|
284
|
+
if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !disabled.has('description-missing')) {
|
|
168
285
|
const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
|
|
169
286
|
const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
|
|
170
287
|
const m = /^description:[ \t]*(.*)$/m.exec(fmText);
|
|
@@ -177,7 +294,7 @@ function lintFile(path, findings) {
|
|
|
177
294
|
for (const l of rest) { if (/^\s+\S/.test(l)) block.push(l.trim()); else break; }
|
|
178
295
|
value = block.join(' ');
|
|
179
296
|
}
|
|
180
|
-
value = value.replace(/^(["'])(.*)\1
|
|
297
|
+
value = /^["']/.test(value) ? value.replace(/^(["'])(.*)\1\s*(#.*)?$/, '$2') : value.replace(/\s+#.*$/, '');
|
|
181
298
|
if (!value) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: '`description:` is empty — at most 300 characters saying what this document settles and where its boundary sits' });
|
|
182
299
|
else if (value.length > 300 && !disabled.has('description-length')) findings.push({ path, n: 1, severity: 'block', id: 'description-length', message: `\`description:\` is ${value.length} characters — at most 300` });
|
|
183
300
|
}
|
|
@@ -190,24 +307,47 @@ function lintFile(path, findings) {
|
|
|
190
307
|
const chrome = raw.findIndex((l) => /shields\.io|<img\b|<div\b|<p align|^!\[/.test(l));
|
|
191
308
|
if (!disabled.has('root-readme-chrome') && chrome !== -1) findings.push({ path, n: chrome + 1, severity: 'block', id: 'root-readme-chrome', message: 'badge, image or HTML layout in the root README — agents cannot read it and the team does not need it', excerpt: raw[chrome].trim().slice(0, 90) });
|
|
192
309
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
310
|
+
// The genre's own contract, from the registry: its required keys, its required sections, and a `type:` it knows.
|
|
311
|
+
const genre = genreOf(path, text);
|
|
312
|
+
const fmKeys = frontmatter(text) || new Map();
|
|
313
|
+
const declared = declaredType(text);
|
|
314
|
+
if (REGISTRY && declared && !REGISTRY.some((r) => r.type === declared) && !EXTERNAL_TYPES.test(declared) && !disabled.has('type-unknown'))
|
|
315
|
+
findings.push({ path, n: 1, severity: 'warn', id: 'type-unknown', message: `\`type: ${declared}\` names no genre in the registry (templates/Readme.md) — use its row's value` });
|
|
316
|
+
// An owning genre's keys are what the ownership scan and the graph read, so their absence blocks; elsewhere it warns.
|
|
317
|
+
const owningGenre = genre && ['feature', 'package', 'system'].includes(genre.type);
|
|
318
|
+
if (scope === 'truth' && genre && !isFolderIndex && !disabled.has('required-key') && !(genre.type === 'feature' && disabled.has('feature-frontmatter'))) {
|
|
319
|
+
for (const key of genre.keys) {
|
|
320
|
+
// `description` carries its own rule above, and an owning document's `objects` its own below.
|
|
321
|
+
if (key === 'description' || (key === 'objects' && owningGenre) || fmKeys.has(key)) continue;
|
|
322
|
+
findings.push({ path, n: 1, severity: owningGenre ? 'block' : 'warn', id: 'required-key', message: `${genre.genre.toLowerCase()} document missing \`${key}:\` frontmatter (templates/Readme.md § Registry)` });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (scope === 'truth' && genre && genre.sections.length && !isFolderIndex && fmKeys.get('status') !== 'deprecated' && !disabled.has('section-missing')) {
|
|
326
|
+
const headings = new Set(lines.map(({ line }) => /^##\s+(.+?)\s*$/.exec(line)).filter(Boolean).map((m) => m[1].toLowerCase()));
|
|
327
|
+
for (const section of genre.sections)
|
|
328
|
+
if (!headings.has(section.toLowerCase())) findings.push({ path, n: 1, severity: 'warn', id: 'section-missing', message: `${genre.genre.toLowerCase()} document has no \`## ${section}\` section (templates/Readme.md § Registry)` });
|
|
329
|
+
}
|
|
330
|
+
// A `verifies:` edge is the one doc → doc edge a person authors; a target that does not exist is a typo or a deletion.
|
|
331
|
+
for (const target of listValue(fmKeys.get('verifies'))) {
|
|
332
|
+
if (disabled.has('dangling-edge') || existsSync(target) || existsSync(join(dirname(path), target))) continue;
|
|
333
|
+
findings.push({ path, n: 1, severity: 'block', id: 'dangling-edge', message: `\`verifies: ${target}\` names no document — repo-relative path of the document whose promises this proves` });
|
|
197
334
|
}
|
|
198
335
|
|
|
199
336
|
// An owning document (feature, package, system) is intent and promises; the sections that narrate
|
|
200
337
|
// mechanism — a file roster, a walk through the code, a schema or route table — are the graph's
|
|
201
|
-
// to compute and are retired from the genre (SKILL.md §
|
|
202
|
-
const isOwningDoc = scope === 'truth' && !isFolderIndex && /(^|\/)docs\/(features
|
|
338
|
+
// to compute and are retired from the genre (SKILL.md § Truth documents).
|
|
339
|
+
const isOwningDoc = scope === 'truth' && !isFolderIndex && /(^|\/)docs\/(features\/(?:[^/]+\/)*|architecture\/(packages|systems)\/)[^/]+\.md$/.test(path);
|
|
340
|
+
if (isOwningDoc && /(^|\/)docs\/features\/[^/]+\/.+\.md$/.test(path) && !disabled.has('nested-feature'))
|
|
341
|
+
findings.push({ path, n: 1, severity: 'warn', id: 'nested-feature', message: 'feature document in a subfolder — `docs/features/` is flat; a subject too large for one file is two files' });
|
|
203
342
|
if (isOwningDoc && !disabled.has('mechanism-section')) {
|
|
204
|
-
|
|
343
|
+
// Whole headings only: "## Data retention" is a promise, "## Data" the retired table.
|
|
344
|
+
const RETIRED = /^#{2,4}\s+(Files|Key files|Current Behavior|Key Functions|Data|Data model|Data flow|API|API surface|Refusals?|Architecture|State|State management|Redux|Affected Modules|Conventions to Follow|Ownership Conflicts|Tests|The shape|Layout|Build and test|Commands|Routes|Schema|Wire)\s*:?\s*$/i;
|
|
205
345
|
for (const { n, line } of lines) {
|
|
206
346
|
if (!RETIRED.test(line) || line.includes('doc-lint-disable: mechanism-section')) continue;
|
|
207
347
|
findings.push({ path, n, severity: 'block', id: 'mechanism-section', message: 'mechanism section in an owning document — columns, routes, guards, functions and files are the graph\'s to compute; keep the decision or the promise, point by name, cut the rest', excerpt: line.trim().slice(0, 90) });
|
|
208
348
|
}
|
|
209
|
-
|
|
210
|
-
|
|
349
|
+
if (!disabled.has('objects-missing') && !listValue(fmKeys.get('objects')).length)
|
|
350
|
+
findings.push({ path, n: 1, severity: 'block', id: 'objects-missing', message: 'owning document names no `objects:` — the product objects it designs are its edge into the graph and its discovery terms' });
|
|
211
351
|
}
|
|
212
352
|
|
|
213
353
|
// The body of an owning document is scanned by people; its "For the agent" footer holds the pointers
|