@nurix/etna 0.20.40 → 0.20.42
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/harness-index.json +1 -1
- package/data/harnesses/code/agents/doc-reviewer.md +31 -0
- package/data/harnesses/code/codex/agents/doc-reviewer.toml +28 -0
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/skills/documentation/SKILL.md +14 -11
- package/data/harnesses/code/skills/documentation/references/document-sets.md +95 -0
- package/data/harnesses/code/skills/documentation/references/style-guide.md +111 -23
- package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +172 -1
- package/data/harnesses/code/skills/documentation/templates/Readme.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/doctrine.md +39 -33
- package/data/harnesses/code/skills/documentation/templates/feature.md +2 -2
- package/data/harnesses/code/skills/documentation/templates/package.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/system.md +2 -2
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/SKILL.md +14 -11
- package/data/harnesses/delivery/skills/documentation/references/document-sets.md +95 -0
- package/data/harnesses/delivery/skills/documentation/references/style-guide.md +111 -23
- package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +172 -1
- package/data/harnesses/delivery/skills/documentation/templates/Readme.md +1 -1
- package/data/harnesses/delivery/skills/documentation/templates/doctrine.md +39 -33
- package/data/harnesses/delivery/skills/documentation/templates/feature.md +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/package.md +1 -1
- package/data/harnesses/delivery/skills/documentation/templates/system.md +2 -2
- package/data/harnesses/etna/agents/doc-reviewer.md +31 -0
- package/data/harnesses/etna/codex/agents/doc-reviewer.toml +28 -0
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/SKILL.md +14 -11
- package/data/harnesses/etna/skills/documentation/references/document-sets.md +95 -0
- package/data/harnesses/etna/skills/documentation/references/style-guide.md +111 -23
- package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +172 -1
- package/data/harnesses/etna/skills/documentation/templates/Readme.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/doctrine.md +39 -33
- package/data/harnesses/etna/skills/documentation/templates/feature.md +2 -2
- package/data/harnesses/etna/skills/documentation/templates/package.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/system.md +2 -2
- package/package.json +2 -2
|
@@ -7,18 +7,20 @@ This guide states how a document is written: every file under `docs/` and every
|
|
|
7
7
|
|
|
8
8
|
## The model
|
|
9
9
|
|
|
10
|
-
A document is read by a person who wants to understand something or to act on it, and it is scanned more often than it is read. Every rule below follows from
|
|
10
|
+
A document is read by a person who wants to understand something or to act on it, and it is scanned more often than it is read. Every rule below follows from ten principles; later sections cite them by number.
|
|
11
11
|
|
|
12
12
|
### Principles
|
|
13
13
|
|
|
14
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
|
-
2. **The outside-in shape:** an explaining document opens on
|
|
15
|
+
2. **The outside-in shape:** an explaining document opens on what it defines 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. Headings name the concept a section covers, never the layer it sits in.
|
|
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
|
-
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.
|
|
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. Doctrine is the one exception: it is a specification, so code that differs from it has a gap to plan and build (`templates/doctrine.md`).
|
|
18
18
|
5. **Noun-phrase labels, third-person sentences:** every heading and every bold bullet label names something and contains no finite verb, so it carries no tense, person or voice. The sentence after a label is third person, present tense, active, with the actor as its subject.
|
|
19
|
-
6. **A body scanned by people, a
|
|
19
|
+
6. **A body scanned by people, a closing for the pointers:** an owning document is bullets under noun-phrase headings, tables wherever the cases are parallel states, code named only where a person types or sees it or at a cross-cutting seam, and a closing that holds pointers, never an inventory: "For the agent" in an owning document, Terms and Related documents in a doctrine document.
|
|
20
20
|
7. **Self-contained, never padded:** the body reads whole without opening another file, restating the one sentence of context a borrowed concept needs and citing the authority for the rest. Length is a signal of a document that has let mechanism in, never a target.
|
|
21
21
|
8. **Depth per genre:** doctrine stops at decisions and reasons, a system at behaviour across packages, a package at its public contract, a feature runs deepest at its end in Promises, and a reference document or runbook keeps full detail because the detail is its content.
|
|
22
|
+
9. **Every line owns a decision:** a line earns its place by stating a rule, a reason, a boundary or a promise a reader would otherwise get wrong. A line restating a rule that has a home is a citation instead, and a line stating nothing is cut, because restatements drift apart and filler buries the rules a reader came for.
|
|
23
|
+
10. **Standard terms, one rule per sentence:** a sentence uses the term engineers use for the thing, states one rule, and stops. Its reason, where the rule needs one (§ Sentences), is the next sentence. A plainer synonym reads as vague, and a rule fused with its reason hides which half binds.
|
|
22
24
|
|
|
23
25
|
## The genres
|
|
24
26
|
|
|
@@ -29,14 +31,14 @@ Every genre takes the same shape (principle 2) and the same labels (principle 5)
|
|
|
29
31
|
| Doctrine (`docs/architecture/common/`) | Truth | The decisions behind each part and the reasons for them | Any mechanism. A doctrine document describes no code |
|
|
30
32
|
| System (`docs/architecture/systems/`) | Truth | The machine's behaviour across packages: the order it runs in, sequence and state diagrams at role level, the invariant at each place where parts meet, what re-drives a stall. No single file shows a cross-package flow, so this layer earns its place | Per-function steps, tables and columns, routes |
|
|
31
33
|
| Package (`docs/architecture/packages/`) | Truth | The public contract a consumer touches: commands and their flags, exports, what is installed and where. For a CLI the flags *are* the promise | Internal layout, the build's steps, which file holds what |
|
|
32
|
-
| Feature (`docs/features/`) | Truth | The behaviour a person meets, in full: each state the surface can be in, each edge case, what a
|
|
34
|
+
| Feature (`docs/features/`) | Truth | The behaviour a person meets, in full: each state the surface can be in, each edge case, what a rejection 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
35
|
| Reference (API reference, environment registry) | Truth | The contract itself: signatures, shapes, variables | Nothing. The detail is the point |
|
|
34
36
|
| Operations procedure, migration runbook | Truth, record | The exact commands, in order, with their expected output | Nothing. The detail is the point |
|
|
35
37
|
| 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
38
|
| 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 |
|
|
37
39
|
|
|
38
40
|
- **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`).
|
|
39
|
-
- **Doctrine:** the same shape and labels as an owning document, speaking for no code and describing no mechanism.
|
|
41
|
+
- **Doctrine:** the same shape and labels as an owning document, speaking for no code and describing no mechanism. Each section opens with a definition or the governing rule, never a lead-in, and continues in bullets like every other explaining document.
|
|
40
42
|
- **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).
|
|
41
43
|
- **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.
|
|
42
44
|
|
|
@@ -46,20 +48,22 @@ Every truth document a person reads to understand something is written from the
|
|
|
46
48
|
|
|
47
49
|
### The layers
|
|
48
50
|
|
|
49
|
-
1. **The
|
|
50
|
-
2. **The model:** what the thing is, in a page: the parties or parts, the one or two axes that organize them, and the invariants that hold everywhere, numbered
|
|
51
|
+
1. **The definition and its boundary:** the opening states what the document defines, then what is in scope and what is out of scope, each out-of-scope item pointing to the document that owns it. No scene-setting before it, and no addresses, file paths or configuration in it.
|
|
52
|
+
2. **The model:** what the thing is, in a page: the parties or parts, the one or two axes that organize them, and the invariants that hold everywhere, numbered so later sections cite them ("principle 3"), each with its reason where the rule needs one (§ Sentences).
|
|
51
53
|
3. **The parts:** one table or diagram placing every part in the model, then how the parts relate: what they share, where they meet, what keeps them apart.
|
|
52
|
-
4. **Each part in turn:** for each part, the decisions it rests on and why. A part's section opens with
|
|
54
|
+
4. **Each part in turn:** for each part, the decisions it rests on and why. A part's section opens with its definition or its governing rule, never with how it is built.
|
|
55
|
+
|
|
56
|
+
Each layer is carried by sections named for their concepts: "Callers", "Principles", "Credential families", "Capability links". A heading never names the layer itself: no "The model", "The parts", "In depth" or "Overview".
|
|
53
57
|
5. **The genre's deepest layer:** the row for the genre in § The genres, and nothing deeper.
|
|
54
58
|
|
|
55
59
|
An owning or doctrine document's fixed sections carry these layers; nothing is added beside them to hold a layer:
|
|
56
60
|
|
|
57
61
|
| Layer | Feature | System | Package | Doctrine |
|
|
58
62
|
| --- | --- | --- | --- | --- |
|
|
59
|
-
| The
|
|
60
|
-
| The model | Scope's
|
|
61
|
-
| The parts | Flows | Flow | Ownership, Usage | The parts |
|
|
62
|
-
| Each part's decisions | Decisions | Decisions | Decisions |
|
|
63
|
+
| The definition and its boundary | Goal line, Scope | Opening paragraph, Scope | Identity and goal lines, Scope | Opening sentence and scope bullets |
|
|
64
|
+
| The model | Scope's In scope bullet | Flow's opening | Usage's opening | The participant sections, then Principles |
|
|
65
|
+
| The parts | Flows | Flow | Ownership, Usage | The parts table and its relations |
|
|
66
|
+
| Each part's decisions | Decisions | Decisions | Decisions | One section per part |
|
|
63
67
|
| The deepest layer | Promises | Promises | Promises | — |
|
|
64
68
|
|
|
65
69
|
### The stability test
|
|
@@ -71,16 +75,16 @@ The test (principle 3) decides what a sentence may say. These survive it:
|
|
|
71
75
|
- **Decisions:** the choice made, and the alternative a reader would reach for.
|
|
72
76
|
- **Promises:** what a person or a test can check.
|
|
73
77
|
- **Cross-component contracts:** what no single file shows, such as two clients that must classify the same answer identically.
|
|
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.
|
|
78
|
+
- **Decided-but-unbuilt intent:** outside doctrine, 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.
|
|
75
79
|
|
|
76
|
-
Unbuilt work has one home per kind.
|
|
80
|
+
Unbuilt work has one home per kind. A doctrine document carries none of it, because it specifies every product and records no product's build state. Elsewhere, 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
81
|
|
|
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
|
|
82
|
+
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 states its exception in its opening, as the specification sentence (`templates/doctrine.md`), where its readers would otherwise apply the default; a feature, package or system document never restates the split, because its opening is fixed to its goal.
|
|
79
83
|
|
|
80
84
|
### Diagrams and length by layer
|
|
81
85
|
|
|
82
86
|
- **A diagram's layer:** at the model and parts layers a diagram names roles ("the browser", "the auth service"), never a URL, a header or a file. A diagram drawn from the code's names belongs to the code graph.
|
|
83
|
-
- **Length as a signal:** a document whose reference tail is longer than its body is an inventory with an introduction. A doctrine document past about
|
|
87
|
+
- **Length as a signal:** a document whose reference tail is longer than its body is an inventory with an introduction. A doctrine document past about two hundred and fifty lines has usually let mechanism or restatement in; the per-genre expectations are each template's.
|
|
84
88
|
|
|
85
89
|
## Labels
|
|
86
90
|
|
|
@@ -115,10 +119,63 @@ The sentence after a label, and every sentence of running prose, describes the p
|
|
|
115
119
|
- **Concision:** "Merges server data with local changes by version", never a sentence that restates its own subject twice. A function or a command is described by what it does ("Fetches data from the server", "Returns an array"), never by what it "is used to" do and never in the passive ("an array is returned").
|
|
116
120
|
- **Bold:** marks a bullet's label or a defined term's first appearance, never a noun mid-sentence.
|
|
117
121
|
- **Em-dash:** a budget of one per paragraph, never a joint between clauses.
|
|
118
|
-
- **Terms of art:** introduced in plain words the first time they appear, the term itself in parentheses where a reader will meet it elsewhere.
|
|
119
122
|
- **Inline code:** every identifier, filename, command, flag, environment variable and config key a document names is in backticks. Where a document may name code at all is § Owning documents — the body and the footer and § Reference documents.
|
|
120
123
|
- **Decoration:** ❌ / ✅ only inside banned/blessed lists and comparison tables, never as general decoration; no emoji in headings.
|
|
121
124
|
|
|
125
|
+
### Sentences
|
|
126
|
+
|
|
127
|
+
The reader is an engineer deciding what to build or checking what was built, so a sentence earns its place by carrying a fact, a rule, a boundary or a reason (principle 9). The test for any sentence: deleted, does the reader lose one of those? If not, it is cut.
|
|
128
|
+
|
|
129
|
+
- **One rule per sentence:** the rule is the sentence. Its reason, where it has one, is the next sentence, never joined to it by *so*, *so that*, *because* or *which means* (principle 10).
|
|
130
|
+
- **Reasons where a rule needs one:** a rule takes a reason when any of four holds, because a rule with no reason reads as a preference and leaves a disagreement nothing to argue with:
|
|
131
|
+
|
|
132
|
+
| Trigger | Example |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| The alternative is tempting: an engineer would reasonably do otherwise | retrying a failed signed token as a lookup; adopting an account by an unverified email |
|
|
135
|
+
| The rule costs something: friction, or a feature someone will ask for | no password-only administrators; a step-up prompt; no forced rotation |
|
|
136
|
+
| The value looks arbitrary: a number or a cutoff | a fifteen-minute window; a twenty-four-hour overlap |
|
|
137
|
+
| The failure is invisible: an attack or a drift no user or test would notice | a timing leak; an account takeover; data crossing organizations |
|
|
138
|
+
|
|
139
|
+
No reason is owed by a definition, by a rule a cited principle already justifies (the citation is the reason), or where the only reason restates the rule ("this keeps things secure").
|
|
140
|
+
- **The reason's size:** one sentence, after its rule, naming the concrete failure the rule prevents: "A cross-site page otherwise makes the browser send its cookie." Alternatives weighed and the argument behind a choice belong to an architecture decision (`decisions/`), which the document cites; the document carries the conclusion and its one line of why.
|
|
141
|
+
- **Length:** about twenty-five words at most, one idea each. A sentence listing three or more items introduces them with a colon, in parallel form, and is the one exception.
|
|
142
|
+
- **Statements, not modals:** a document states what happens ("the request is rejected"), never what must or should happen; its statements are requirements by being in it. "Never" marks a hard boundary, sparingly.
|
|
143
|
+
- **Declaratives only:** no questions, rhetorical or otherwise, and no *imagine*, *consider* or *suppose*.
|
|
144
|
+
- **Specific over general:** the concrete thing, never an abstract stand-in: role, credential, route, not power, proof, door.
|
|
145
|
+
- **No rhetoric:** no scene-setting opener, no generic truth used as framing, no metaphor, aphorism, personification, or phrasing inverted or balanced for effect, and no hedging (*generally*, *typically*, *in most cases*). State the rule and list its exceptions.
|
|
146
|
+
|
|
147
|
+
| ❌ Cut | Why | ✅ Keep |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| "Every product on the enterprise plane must know exactly who is calling before it does anything else." | A truth no reader doubts, naming nothing, written to introduce the next sentence | "This document defines how a product identifies the caller of every request: one principal per request, step-up verification for sensitive actions, bounds on agent runs, and the credentials a product verifies itself." |
|
|
150
|
+
| "A key that makes keys turns one leak into unlimited keys, so a person's API key cannot mint keys." | Reason and rule fused, the rule last | "A person's API key cannot list, mint or revoke API keys. A credential that can mint credentials turns one leak into an unbounded set." |
|
|
151
|
+
| "the one caller whose next act nobody wrote down in advance" | Personification standing in for a definition | "An agent run is a job in which a model selects the next step." |
|
|
152
|
+
| "Many proofs, one person" | A balanced slogan in place of the rule | "**One principal per person:** handlers see the same principal however the person authenticated." |
|
|
153
|
+
|
|
154
|
+
### Vocabulary
|
|
155
|
+
|
|
156
|
+
A document uses the term engineers use. A plainer synonym reads as vague and an elegant one as literary, and either one reads as a second concept (principle 10).
|
|
157
|
+
|
|
158
|
+
| Use | Not |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| verify, validate | prove, check, where verification is meant |
|
|
161
|
+
| reject, deny; rejection | refuse; refusal |
|
|
162
|
+
| resolve (a principal) | turn into, become |
|
|
163
|
+
| mint, issue (a credential) | create, make |
|
|
164
|
+
| route, endpoint | door, gate, page |
|
|
165
|
+
| permission, role | power, authority |
|
|
166
|
+
| origin, same-origin | the product's own site or pages |
|
|
167
|
+
| timestamp, recency | how recently, when it was last |
|
|
168
|
+
| constant-time comparison | a compare that leaks nothing |
|
|
169
|
+
| step-up verification | a fresh sign-in, proving it again |
|
|
170
|
+
| revoke | withdraw, kill (a credential, session or link) |
|
|
171
|
+
|
|
172
|
+
- **One term, defined once:** a term is defined once (in a doctrine document's Terms table, at its first appearance elsewhere) and used unchanged after; it is never varied for style.
|
|
173
|
+
- **"Doctrine" in prose:** the word is the genre's tooling name (`type: doctrine`, the template, the folder). A doctrine document calls itself "this document" or "this specification", and its neighbours "related documents".
|
|
174
|
+
|
|
175
|
+
### Reach and lint
|
|
176
|
+
|
|
177
|
+
§ Sentences and § Vocabulary bind doctrine and owning documents; records and queues take § Vocabulary and the person and tense rules. On a doctrine document `doc-lint` warns `modal-verb` (*must*, *should*, *shall* outside a quotation), `question` (a question mark in prose), `long-sentence` (past twenty-five words, a colon list exempt), `vague-term` (a word in the Not column above) and `doctrine-word` (the genre name in prose). A fused reason and rhetoric are judgment, and they are the cold read's (`document-sets.md` § Rubric).
|
|
178
|
+
|
|
122
179
|
## Owning documents — the body and the footer
|
|
123
180
|
|
|
124
181
|
A feature, package or system document is scanned more than read (principle 6). It takes the shape (§ Shape — the model first, the detail after) and the labels (§ Labels) above, and these rules.
|
|
@@ -137,11 +194,11 @@ Nothing else. A "Why it exists" section has no place; its content is Scope or De
|
|
|
137
194
|
|
|
138
195
|
- **The opening:** the goal is the first paragraph under the title, after the one identity line a package carries. No plan link and no routing line above it.
|
|
139
196
|
- **Bullets:** each section is bullets under a noun-phrase heading; each bullet is a bold noun-phrase label, a colon, then one or two plain sentences saying what happens, what is decided or what is promised.
|
|
140
|
-
- **Tables:** a table replaces the bullets wherever the cases are parallel states, such as each
|
|
141
|
-
- **Paragraphs:** right for the goal, for a system's opening definition, and for
|
|
197
|
+
- **Tables:** a table replaces the bullets wherever the cases are parallel states, such as each rejection and the exact message it shows. Its header cells are noun phrases.
|
|
198
|
+
- **Paragraphs:** right for the goal, for a system's opening definition, and for the one- or two-sentence definition that opens a section. Everywhere else, a paragraph is bullets that have not been cut yet.
|
|
142
199
|
- **Code in the body:** named only where a person types or sees it (a page, a command, a flag, an environment variable) or at a cross-cutting seam the graph would not lead a reader to from the feature's own files: a middleware, a gate, a hook, a scheduler tick, a shared store holding part of the feature's logic because that logic is global. Written once, by name, where the narrative leaves the feature. Everything else is a plain name: "the device registry", "the sign-off page", "the mirror". `doc-lint` warns `body-identifier` past three identifiers in a section.
|
|
143
200
|
- **Flows:** describe behaviour, never mechanism. A flow bullet says that a forgotten machine comes back on its next call, never which function re-mints the row. Behaviour in plain words is intent made legible; columns, routes, guards and signatures are the graph's.
|
|
144
|
-
- **Promises:** the deepest section of a feature document: every state, edge case and
|
|
201
|
+
- **Promises:** the deepest section of a feature document: every state, edge case and rejection, and the exact wording where the wording is promised, each checkable by a person or a test. A promise reads "a removed membership shows a panel, not a redirect", never which route answers it.
|
|
145
202
|
- **Gaps:** a gap the code does not close is one bullet under Gaps, or a backlog row. It is never a ledger row inside another section, and never a temporal hedge.
|
|
146
203
|
|
|
147
204
|
### The footer
|
|
@@ -186,14 +243,45 @@ Every document is a truth document, a record or a queue (principle 1); `SKILL.md
|
|
|
186
243
|
|
|
187
244
|
Worked example of the strip-and-relocate move: [`examples.md`](examples.md) Example 6.
|
|
188
245
|
|
|
246
|
+
## Net-net — what earns a line
|
|
247
|
+
|
|
248
|
+
A document fails its reader less often by being wrong than by burying its rules under what it repeats (principle 9). The failure arrives one reasonable sentence at a time: each restatement helps where it stands, and the set reads as noise.
|
|
249
|
+
|
|
250
|
+
### The line test
|
|
251
|
+
|
|
252
|
+
- **A decision of its own:** the line states a rule, its reason, a boundary or a promise, and a reader without it would act differently. Otherwise it is cut.
|
|
253
|
+
- **One home:** before a rule is stated, the document and its neighbours are searched for it, by a distinctive phrase. Found, it is cited ("(auth, principle 3)") with the one clause of context the sentence needs (principle 7), never restated in new words.
|
|
254
|
+
- **Once inside the document:** a part's section carries the decisions its principles do not state; a principle repeated as a bullet below it is a duplicate wearing a label.
|
|
255
|
+
- **One name per concept:** a concept keeps the one name the document set gave it (the term table in `document-sets.md`, or the `objects:` list for a single document); a synonym reads as a second concept.
|
|
256
|
+
|
|
257
|
+
### What fails it
|
|
258
|
+
|
|
259
|
+
| ❌ | Why it fails | ✅ |
|
|
260
|
+
| --- | --- | --- |
|
|
261
|
+
| "The lock exists so that an unvetted organization can look around without creating anything." | Announces a purpose the next bullet states | "**Locked:** signs in and reads; creates and runs nothing." |
|
|
262
|
+
| A parts table whose every row reads "see principle 4" | Places nothing | No table |
|
|
263
|
+
| A Relations bullet restating principle 2 in other words | A second statement that will drift from the first | Cut, or cite principle 2 |
|
|
264
|
+
| A doctrine document whose every rule had a home elsewhere: attribution, key custody, a budget rejection, outbound deadlines | Owns no decision of its own | Each rule moved to its home; the document deleted |
|
|
265
|
+
| A rule stated in two sibling documents in different words | A reader cannot tell which binds, and the two drift | One home, one citation |
|
|
266
|
+
| A table of states, then a bullet per state restating its row | Every rule is stated twice, a page apart | The table for what each state allows; bullets only for the reasons |
|
|
267
|
+
| "Security matters at every layer." · "This section describes how…" | A truism and a ceremony: no decision | Cut; the heading already names the section |
|
|
268
|
+
|
|
269
|
+
### Port hygiene
|
|
270
|
+
|
|
271
|
+
A document that changes scope (one product's design lifted into platform doctrine, or doctrine applied to one product) has every product name, setting value, code root, build state and product-specific example rewritten for the new scope or cut. Left in place, the residue reads as a rule binding every reader.
|
|
272
|
+
|
|
273
|
+
### Proof
|
|
274
|
+
|
|
275
|
+
On a doctrine folder, `doc-lint` catches the mechanical half: a restated sentence (`sibling-duplicate`), a concept two documents govern (`objects-shared`), a citation to a principle or heading that does not exist (`citation-unresolved`), a purpose opener (`purpose-opener`). The rest is judgment, and it takes one cold read (`document-sets.md` § Review).
|
|
276
|
+
|
|
189
277
|
## Self-contained documents and the link surface
|
|
190
278
|
|
|
191
279
|
Instruction files stay lean because an agent pays for them on every load (`instruction-style.md`); a document is read on demand, so it carries what its reader needs to understand the page (principle 7).
|
|
192
280
|
|
|
193
281
|
- **Completeness:** the one or two sentences of context a borrowed concept needs are restated where it is used, because a reader forced mid-paragraph into a second document loses the thread of the first.
|
|
194
282
|
- **Restatement and citation:** a changing detail (a version, a count, a path list) keeps one canonical home; the restatement carries what the page needs and points at the authority for the rest. Link-don't-copy governs where a fact is *owned*, not how readable the page that uses it must be.
|
|
195
|
-
- **Inline links:** the minimum that action requires. A link mid-prose is right only where the reader must go there to act on this page, which includes a Scope section's
|
|
196
|
-
- **The closing link surface:** every other sister document belongs in the document's one closing section: "For the agent" (its neighbours line) in an owning
|
|
283
|
+
- **Inline links:** the minimum that action requires. A link mid-prose is right only where the reader must go there to act on this page, which includes the out-of-scope pointers in a Scope section or a doctrine document's opening.
|
|
284
|
+
- **The closing link surface:** every other sister document belongs in the document's one closing section: "For the agent" (its neighbours line) in an owning document, "Related documents" in a doctrine document, "Related" elsewhere. Deep relative paths (`../../../../…`) are fragile, since a folder move breaks every one, so they are confined to that block; body mentions use the plain name ("the boundary skill", "`structure.md`").
|
|
197
285
|
|
|
198
286
|
## Diagrams
|
|
199
287
|
|
|
@@ -1,4 +1,4 @@
|
|
|
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)
|
|
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, doctrine sets)
|
|
2
2
|
// Usage: node doc-lint.mjs <file.md ...> | node doc-lint.mjs --changed (git-changed + untracked .md)
|
|
3
3
|
// Exit 1 when any block-severity finding survives. Escape hatch: <!-- doc-lint-disable: id[, id] --> alone on its
|
|
4
4
|
// own line (file-wide, with a reason comment beside it) or at the end of the offending line (that line only).
|
|
@@ -392,6 +392,13 @@ function lintFile(path, findings) {
|
|
|
392
392
|
if (!label || /^For the agent$/i.test(label)) continue;
|
|
393
393
|
if (CLAUSE.test(label.replace(/`[^`]*`/g, 'X'))) findings.push({ path, n, severity: 'warn', id: 'clause-label', message: 'a heading or bold label written as a clause — a label is a noun phrase with no verb, tense or person ("Decisions", not "What was decided"; "Organization member:", not "You already belong to an organization:")', excerpt: label.slice(0, 90) });
|
|
394
394
|
}
|
|
395
|
+
// "X exists so that…" restates the subject its heading already named; the purpose belongs in the rule it implies.
|
|
396
|
+
if (!disabled.has('purpose-opener')) {
|
|
397
|
+
for (const { n, line } of lines) {
|
|
398
|
+
const bare = line.replace(/`[^`]*`/g, 'X').replace(/^\s*[-*]\s+(\*\*[^*]+\*\*:?\s*)?/, '');
|
|
399
|
+
if (/^(the |an? )?[\w' -]{1,40}? exists (so that|to |because|in order)/i.test(bare)) findings.push({ path, n, severity: 'warn', id: 'purpose-opener', message: '"X exists so that…" — state the rule the purpose implies ("An unvetted organization signs in and reads, and creates nothing"), not that the thing has a purpose', excerpt: line.trim().slice(0, 90) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
395
402
|
// The sentences after a label describe the product in the third person; quoted interface copy keeps its own "you".
|
|
396
403
|
if (!disabled.has('second-person')) {
|
|
397
404
|
for (const { n, line } of lines) {
|
|
@@ -400,6 +407,170 @@ function lintFile(path, findings) {
|
|
|
400
407
|
}
|
|
401
408
|
}
|
|
402
409
|
}
|
|
410
|
+
|
|
411
|
+
if (scope === 'truth' && !isFolderIndex && (declared === 'doctrine' || DOCTRINE_LOCATION.test(path))) lintDoctrine(path, text, lines, disabled, findings);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// A doctrine folder is read as a set: its documents cite each other's principles by number and must not restate each
|
|
415
|
+
// other (templates/doctrine.md, references/document-sets.md), so these checks read the siblings beside the linted file.
|
|
416
|
+
const DOCTRINE_LOCATION = /(^|\/)docs\/architecture\/common\/[^/]+\.md$/;
|
|
417
|
+
// A doctrine document closes on Terms and Related documents; an older one on "For the agent". The body ends at the first.
|
|
418
|
+
const DOCTRINE_CLOSING = /^##\s+(For the agent|Terms|Related documents)\s*$/;
|
|
419
|
+
// The words a doctrine document replaces with the term engineers use (references/style-guide.md § Vocabulary).
|
|
420
|
+
const VAGUE_TERMS = [[/\bdoors?\b/i, 'route or endpoint'], [/\bgates?\b/i, 'route, check or permission check'], [/\brefus(e|es|ed|ing|als?)\b/i, 'reject, rejection'], [/\bpowers?\b/i, 'permission or role'], [/\bprov(e|ing) it again\b/i, 'step-up verification']];
|
|
421
|
+
// The specification sentence every doctrine document carries verbatim is the template's, not a duplicated rule.
|
|
422
|
+
const SPEC_SENTENCE = /specification of its concern|has a gap to plan and build|never settled silently in code/i;
|
|
423
|
+
const STOPWORDS = new Set('the and for that this with from are its their them they into onto than then when where which while who whom whose what each every any all one its not nor but was were has have had been being does did can may must will would should could only also more most other such same own per via out off over under about after before between both either neither those these there here because so as at by in of on or to an a is be it no'.split(' '));
|
|
424
|
+
const siblingCache = new Map();
|
|
425
|
+
|
|
426
|
+
// The doctrine documents in a folder, each with its title, objects, principle numbers, headings and sentences.
|
|
427
|
+
function doctrineSiblings(dir) {
|
|
428
|
+
if (siblingCache.has(dir)) return siblingCache.get(dir);
|
|
429
|
+
const docs = [];
|
|
430
|
+
for (const name of existsSync(dir) ? readdirSync(dir) : []) {
|
|
431
|
+
if (!name.endsWith('.md') || /^readme\.md$/i.test(name)) continue;
|
|
432
|
+
const path = join(dir, name);
|
|
433
|
+
const text = readFileSync(path, 'utf8');
|
|
434
|
+
const lines = proseLines(text);
|
|
435
|
+
const title = (lines.find(({ line }) => /^#\s+/.test(line))?.line || '').replace(/^#\s+/, '').trim().toLowerCase();
|
|
436
|
+
docs.push({ path, name, title, text, lines, objects: listValue((frontmatter(text) || new Map()).get('objects')).map((o) => o.toLowerCase()), principles: principleNumbers(lines), headings: new Set(lines.map(({ line }) => /^#{2,4}\s+(.+?)\s*$/.exec(line)?.[1].toLowerCase()).filter(Boolean)), sentences: sentencesOf(lines) });
|
|
437
|
+
}
|
|
438
|
+
siblingCache.set(dir, docs);
|
|
439
|
+
return docs;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The numbers of a document's principles: the numbered items under its Principles heading.
|
|
443
|
+
function principleNumbers(lines) {
|
|
444
|
+
const out = new Set();
|
|
445
|
+
let level = 0;
|
|
446
|
+
for (const { line } of lines) {
|
|
447
|
+
const heading = /^(#{2,4})\s+(.+?)\s*$/.exec(line);
|
|
448
|
+
if (heading) { level = /^principles$/i.test(heading[2]) ? heading[1].length : level && heading[1].length <= level ? 0 : level; continue; }
|
|
449
|
+
const item = level && /^(\d+)\.\s/.exec(line);
|
|
450
|
+
if (item) out.add(Number(item[1]));
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// The body's sentences long enough to state a rule, as content-word sets; headings, tables and the footer drop out.
|
|
456
|
+
function sentencesOf(lines) {
|
|
457
|
+
const out = [];
|
|
458
|
+
for (const { n, line } of lines) {
|
|
459
|
+
if (DOCTRINE_CLOSING.test(line)) break;
|
|
460
|
+
if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
|
|
461
|
+
const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
|
|
462
|
+
for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
|
|
463
|
+
if (SPEC_SENTENCE.test(sentence)) continue;
|
|
464
|
+
const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
|
|
465
|
+
if (words.size >= 9) out.push({ n, text: sentence.trim(), words });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const slugOf = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
472
|
+
|
|
473
|
+
function lintDoctrine(path, text, lines, disabled, findings) {
|
|
474
|
+
const siblings = doctrineSiblings(dirname(path));
|
|
475
|
+
const self = siblings.find((d) => d.name === path.split('/').pop()) || { path, objects: [], principles: principleNumbers(lines), headings: new Set(), sentences: sentencesOf(lines) };
|
|
476
|
+
const byName = (name) => siblings.find((d) => d.title === name.toLowerCase() || d.name === `${slugOf(name)}.md`);
|
|
477
|
+
const footerAt = lines.findIndex(({ line }) => DOCTRINE_CLOSING.test(line));
|
|
478
|
+
const body = footerAt === -1 ? lines : lines.slice(0, footerAt);
|
|
479
|
+
// The opening runs to the first section; its out-of-scope bullets are the one place in the body a link belongs.
|
|
480
|
+
const openingEnd = lines.findIndex(({ line }) => /^##\s+/.test(line));
|
|
481
|
+
const sections = body.slice(openingEnd === -1 ? body.length : openingEnd);
|
|
482
|
+
|
|
483
|
+
// A citation names a principle by number or a section by heading, in parentheses: "(principle 4)", "(auth, principle 3)", "(API protocol § Idempotency)".
|
|
484
|
+
if (!disabled.has('citation-unresolved')) {
|
|
485
|
+
for (const { n, line } of lines) {
|
|
486
|
+
for (const group of line.replace(/`[^`]*`/g, '').matchAll(/\(([^()]+)\)/g)) {
|
|
487
|
+
for (const part of group[1].split(';').map((p) => p.trim())) {
|
|
488
|
+
const principle = /^(?:(.+?),\s*)?principles?\s+(\d+(?:\s*(?:,|and)\s*\d+)*)$/i.exec(part);
|
|
489
|
+
const section = /^(.+?)\s+§\s+(.+)$/.exec(part);
|
|
490
|
+
if (!principle && !section) continue;
|
|
491
|
+
const name = principle ? principle[1] : section[1];
|
|
492
|
+
const target = name ? byName(name) : self;
|
|
493
|
+
if (!target) { findings.push({ path, n, severity: 'warn', id: 'citation-unresolved', message: `"${name}" names no doctrine document in this folder — cite a sibling by its title`, excerpt: part }); continue; }
|
|
494
|
+
const where = target === self ? 'this document' : target.name;
|
|
495
|
+
if (principle) for (const num of principle[2].split(/\s*(?:,|and)\s*/).map(Number))
|
|
496
|
+
if (!target.principles.has(num)) findings.push({ path, n, severity: 'block', id: 'citation-unresolved', message: `principle ${num} does not exist in ${where} (it numbers ${[...target.principles].join(', ') || 'none'})`, excerpt: part });
|
|
497
|
+
if (section && !target.headings.has(section[2].trim().toLowerCase())) findings.push({ path, n, severity: 'block', id: 'citation-unresolved', message: `${where} has no heading "${section[2].trim()}"`, excerpt: part });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// One concept has one home: an object two doctrine documents both govern is a boundary the set has not drawn.
|
|
503
|
+
if (!disabled.has('objects-shared')) {
|
|
504
|
+
for (const object of self.objects) {
|
|
505
|
+
const other = siblings.find((d) => d !== self && d.objects.includes(object));
|
|
506
|
+
if (other) findings.push({ path, n: 1, severity: 'warn', id: 'objects-shared', message: `\`${object}\` is also in ${other.name}'s objects — one concept, one governing document; the other cites it` });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// The body names its neighbours in plain words; links live in "For the agent" (references/style-guide.md § Self-contained documents).
|
|
510
|
+
if (!disabled.has('doctrine-body-link')) {
|
|
511
|
+
for (const { n, line } of sections)
|
|
512
|
+
if (/\]\([^)]+\)/.test(line.replace(/`[^`]*`/g, '')) && !line.includes('doc-lint-disable: doctrine-body-link')) findings.push({ path, n, severity: 'warn', id: 'doctrine-body-link', message: 'a link in a doctrine body — name the neighbour in plain words; its link belongs in Related documents, or in the opening\'s out-of-scope bullets', excerpt: line.trim().slice(0, 90) });
|
|
513
|
+
}
|
|
514
|
+
// The sentence rules of references/style-guide.md § Sentences and § Vocabulary, on the body's prose: headings, tables
|
|
515
|
+
// and quotations drop out, and the genre's name is checked to the end of the file.
|
|
516
|
+
const prose = (line) => line.replace(/`[^`]*`/g, 'X').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/\*"[^"]*"\*|"[^"]*"|“[^”]*”/g, 'Q').replace(/<!--.*?-->/g, '');
|
|
517
|
+
const proseLinesOf = (from) => from.filter(({ line }) => line.trim() && !/^\s*(#|\|)/.test(line));
|
|
518
|
+
for (const { n, line } of proseLinesOf(body)) {
|
|
519
|
+
const bare = prose(line);
|
|
520
|
+
const words = bare.replace(/\*\*/g, '').replace(/^\s*([-*]|\d+\.)\s+/, '');
|
|
521
|
+
if (!disabled.has('modal-verb') && /\b(must|should|shall)\b/i.test(words)) findings.push({ path, n, severity: 'warn', id: 'modal-verb', message: 'a modal in a specification — state what happens ("the request is rejected"); being in this document makes it a requirement', excerpt: line.trim().slice(0, 90) });
|
|
522
|
+
if (!disabled.has('question') && /\?(\s|$)/.test(words)) findings.push({ path, n, severity: 'warn', id: 'question', message: 'a question in prose — state the rule declaratively', excerpt: line.trim().slice(0, 90) });
|
|
523
|
+
if (!disabled.has('vague-term')) for (const [pattern, use] of VAGUE_TERMS) {
|
|
524
|
+
const m = pattern.exec(words);
|
|
525
|
+
if (m) { findings.push({ path, n, severity: 'warn', id: 'vague-term', message: `"${m[0]}" — use the standard term: ${use} (references/style-guide.md § Vocabulary)`, excerpt: line.trim().slice(0, 90) }); break; }
|
|
526
|
+
}
|
|
527
|
+
if (!disabled.has('long-sentence')) {
|
|
528
|
+
for (const sentence of words.split(/(?<=[.;!])\s+(?=[A-Z(])/)) {
|
|
529
|
+
const list = /:\s/.test(sentence) && (sentence.split(/:\s/).slice(1).join(' ').match(/[,;]/g) || []).length >= 2;
|
|
530
|
+
const count = (sentence.match(/[\w'’-]+/g) || []).length;
|
|
531
|
+
if (count > 25 && !list) { findings.push({ path, n, severity: 'warn', id: 'long-sentence', message: `${count} words in one sentence — about twenty-five at most, one idea each; a colon list is exempt`, excerpt: sentence.trim().slice(0, 90) }); break; }
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!disabled.has('doctrine-word')) {
|
|
536
|
+
for (const { n, line } of proseLinesOf(lines))
|
|
537
|
+
if (/\bdoctrines?\b/i.test(prose(line).replace(/^#+\s+/, ''))) findings.push({ path, n, severity: 'warn', id: 'doctrine-word', message: '"doctrine" in prose — the genre\'s tooling name; the document is "this document" or "this specification", its neighbours "related documents"', excerpt: line.trim().slice(0, 90) });
|
|
538
|
+
}
|
|
539
|
+
// Every object the document governs is defined once, in its Terms table.
|
|
540
|
+
const termsAt = lines.findIndex(({ line }) => /^##\s+Terms\s*$/.test(line));
|
|
541
|
+
if (termsAt !== -1 && !disabled.has('terms-missing')) {
|
|
542
|
+
const defined = new Set();
|
|
543
|
+
for (const { line } of lines.slice(termsAt + 1)) {
|
|
544
|
+
if (/^##\s+/.test(line)) break;
|
|
545
|
+
const cell = /^\|\s*([^|]+?)\s*\|/.exec(line);
|
|
546
|
+
if (cell && !/^(-{3,}|term)$/i.test(cell[1])) defined.add(cell[1].replace(/\*|`/g, '').toLowerCase());
|
|
547
|
+
}
|
|
548
|
+
for (const object of self.objects.length ? self.objects : listValue((frontmatter(text) || new Map()).get('objects')).map((o) => o.toLowerCase()))
|
|
549
|
+
if (!defined.has(object)) findings.push({ path, n: lines[termsAt].n, severity: 'warn', id: 'terms-missing', message: `\`${object}\` is in objects: but not in the Terms table — define every governed term once, there` });
|
|
550
|
+
}
|
|
551
|
+
// A doctrine document specifies; what one product has not built is that product's plan or backlog, never a marker here.
|
|
552
|
+
if (!disabled.has('doctrine-unbuilt')) {
|
|
553
|
+
const at = text.split('\n').findIndex((l) => /<!--\s*UNBUILT/.test(l));
|
|
554
|
+
if (at !== -1) findings.push({ path, n: at + 1, severity: 'warn', id: 'doctrine-unbuilt', message: 'an UNBUILT marker in a doctrine document — doctrine is the specification; a product that lacks a part has a gap to plan and build in its own repo' });
|
|
555
|
+
}
|
|
556
|
+
// A rule stated twice drifts; the second statement is a citation. Sentences sharing most of their content words are flagged once per pair.
|
|
557
|
+
if (!disabled.has('sibling-duplicate')) {
|
|
558
|
+
for (const s of self.sentences) {
|
|
559
|
+
for (const d of siblings) {
|
|
560
|
+
const match = d.sentences.find((t) => (d !== self || t.n !== s.n) && overlap(s.words, t.words) >= 0.7 && (d !== self || t.n > s.n));
|
|
561
|
+
if (!match) continue;
|
|
562
|
+
findings.push({ path, n: s.n, severity: 'warn', id: 'sibling-duplicate', message: `restates ${d === self ? 'this document' : d.name}:${match.n} — keep the rule in one home and cite it there`, excerpt: s.text.slice(0, 90) });
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Shared content words over the smaller sentence's, so a sentence quoted inside a longer one still counts.
|
|
570
|
+
function overlap(a, b) {
|
|
571
|
+
let shared = 0;
|
|
572
|
+
for (const w of a) if (b.has(w)) shared++;
|
|
573
|
+
return shared / Math.min(a.size, b.size);
|
|
403
574
|
}
|
|
404
575
|
|
|
405
576
|
// A changed plan whose committed version is already frozen must not be edited.
|
|
@@ -17,7 +17,7 @@ Every fact about a genre lives in one row of the table below: its template, its
|
|
|
17
17
|
| Feature | [feature.md](feature.md) | truth | `feature` | `docs/features/{feature}.md` | `type` `description` `status` `objects` | Scope · Promises · For the agent |
|
|
18
18
|
| Package | [package.md](package.md) | truth | `package` | `docs/architecture/packages/{package}.md` | `type` `description` `status` `objects` | Scope · Ownership · Usage · Promises · For the agent |
|
|
19
19
|
| System | [system.md](system.md) | truth | `system` | `docs/architecture/systems/{system}.md` | `type` `description` `status` `objects` | Scope · Flow · Promises · For the agent |
|
|
20
|
-
| Doctrine | [doctrine.md](doctrine.md) | truth | `doctrine` | `docs/architecture/common/{concern}.md` | `type` `description` `status` `objects` |
|
|
20
|
+
| Doctrine | [doctrine.md](doctrine.md) | truth | `doctrine` | `docs/architecture/common/{concern}.md` | `type` `description` `status` `objects` | Principles · Terms · Related documents |
|
|
21
21
|
| Architecture decision | [architecture-decision.md](architecture-decision.md) | truth | `architecture-decision` | `docs/architecture/decisions/AD-NNN-{subject}.md` | `type` `title` `description` `status` `last-updated` | Decision · Context · Consequences |
|
|
22
22
|
| UX behavior | [ux-behavior.md](ux-behavior.md) | truth | `ux` | `docs/features/{surface}-ux.md` | `type` `description` `status` `objects` | Purpose · Anatomy · States · Affordances · Promises · Transitions |
|
|
23
23
|
| System overview | [system-overview.md](system-overview.md) | truth | `system-overview` | `docs/architecture/system-design.md` | `type` `description` | — |
|
|
@@ -2,75 +2,79 @@
|
|
|
2
2
|
|
|
3
3
|
Truth document. Use for **one concern the platform handles everywhere**: authentication, authorization, storage, the API protocol, credentials, telemetry. One file per concern, named for the concern. A doctrine document speaks for no code: no source header names it, and it carries no `owns:`. The surfaces bound by it are the documents that design or touch the objects it governs, computed through those objects, and each names it in its Scope.
|
|
4
4
|
|
|
5
|
-
**A doctrine document states the design and nothing beneath it** (`references/style-guide.md` § Shape — the model first, the detail after). It is the most stable document in the tree, because it says why the platform is shaped the way it is, and that changes far less often than the code. It explains no mechanism: no function, file, column, cookie attribute, header value or per-route error code, since each of those fails the stability test. The code graph answers what the code does. This document answers why, and which principles any new code in the concern
|
|
5
|
+
**A doctrine document states the design and nothing beneath it** (`references/style-guide.md` § Shape — the model first, the detail after). It is the most stable document in the tree, because it says why the platform is shaped the way it is, and that changes far less often than the code. It explains no mechanism: no function, file, column, cookie attribute, header value or per-route error code, since each of those fails the stability test. The code graph answers what the code does. This document answers why, and which principles any new code in the concern keeps.
|
|
6
|
+
|
|
7
|
+
**A doctrine document is a specification, not a description.** It states how every product handles the concern, so a product whose code differs has a gap to plan and build, and the document never records which product has built what: no `UNBUILT` marker, no "today the product…", no product's name, setting value or code root. When one product's posture against the doctrine matters, it is that product's own security overview, plan or backlog.
|
|
8
|
+
|
|
9
|
+
**Earn the document before writing it.** List the decisions only this document would own, each checked against the documents that exist: a decision another document already states is a citation, not an owned decision. Fewer than about six owned decisions means the concern is a section of a neighbour, not a document (`references/document-sets.md` § Usefulness test).
|
|
6
10
|
|
|
7
11
|
**Required:**
|
|
8
12
|
|
|
9
13
|
- the frontmatter
|
|
10
|
-
- the opening
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **For the agent**
|
|
14
|
+
- the opening: the definition sentence, the scope bullets, and the specification sentence
|
|
15
|
+
- **Principles**: numbered, each a noun-phrase label and its rule, with its reason as a separate sentence wherever the rule meets a trigger of `references/style-guide.md` § Sentences
|
|
16
|
+
- **Terms**: one row per term the document uses, covering every name in `objects:`
|
|
17
|
+
- **Related documents**
|
|
15
18
|
|
|
16
|
-
**Optional:**
|
|
19
|
+
**Optional, each only with something the principles do not already say:**
|
|
17
20
|
|
|
18
|
-
-
|
|
21
|
+
- **A participant section** (e.g. "Callers") before Principles: who or what takes part, as a table where they share attributes
|
|
22
|
+
- **A parts section** (e.g. "Credential families"): one table placing the parts, where there are enough parts that a reader needs them placed, then its relations where two parts meet in a way no principle states
|
|
23
|
+
- **One section per part** that carries decisions of its own (e.g. "Capability links")
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
Sections run in that order: opening, participants, Principles, the parts, each part, Terms, Related documents. Each is named for its concept, never for its layer ("The model", "In depth"). Links live in the opening's out-of-scope bullets and in Related documents; the body names a neighbour in plain words (`references/style-guide.md` § Self-contained documents and the link surface). Expect eighty to two hundred and fifty lines; past that, mechanism or restatement has usually crept in.
|
|
21
26
|
|
|
22
27
|
```yaml
|
|
23
28
|
---
|
|
24
29
|
type: doctrine # required tooling key
|
|
25
30
|
description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
|
|
26
31
|
status: active # required — active | draft
|
|
27
|
-
objects: [principal, credential, device grant] # required — the concepts this
|
|
32
|
+
objects: [principal, credential, device grant] # required — the concepts this document governs, by their graph names
|
|
28
33
|
---
|
|
29
34
|
```
|
|
30
35
|
|
|
31
36
|
```
|
|
32
37
|
# {Concern}
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{The authority sentence: this document states the design, not the code; for what does what, read the code roots in the footer or ask the code graph; where this document and the code disagree on *what*, the code is right; on *why* or a principle, the disagreement is a defect to raise.}
|
|
39
|
+
This document defines {what, for every product}: {the specifics it covers, in parallel form}.
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
- **In scope:** {each item a noun phrase}
|
|
42
|
+
- **Out of scope:** {each item a noun phrase, then a colon and the linked document that owns it}
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
{Optional: one sentence of rationale, where the boundary with a neighbour needs justifying.}
|
|
41
45
|
|
|
42
|
-
{The
|
|
46
|
+
{The specification sentence, with the scope named: "Every product on {the scope, e.g. the enterprise plane} implements this document; a product whose code differs from it has a gap to plan and build."}
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
## {The participants, as a noun phrase — e.g. "Callers"} (optional)
|
|
45
49
|
|
|
46
|
-
{
|
|
50
|
+
{One row or bullet per participant: its name, its definition, and the attribute the parts are placed on. A table where the participants share attributes.}
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
## Principles
|
|
49
53
|
|
|
50
|
-
{
|
|
54
|
+
{Numbered rules that hold across every part. Each item: a bold noun-phrase label and a colon, then the rule as one sentence, then its reason as a separate sentence wherever the rule meets a trigger of the style guide's § Sentences: a tempting alternative, a cost, an arbitrary value, an invisible failure. Later sections cite them by number. A rule another document owns is cited — "(auth service, principle 5)" — never restated here.}
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
## {The parts, as a noun phrase — e.g. "Credential families"} (optional)
|
|
53
57
|
|
|
54
|
-
{
|
|
58
|
+
{One table: each part placed on the model's axes. A row whose only content is "see principle N" is not placing anything; drop the table when every row would be one. Then the relations, as bullets: only where two parts meet in a way no principle states — what they share, what keeps them apart, the order in which they are tried. A role-level diagram (mermaid) where the relationships are a flow.}
|
|
55
59
|
|
|
56
|
-
## {
|
|
60
|
+
## {One part, as a noun phrase — e.g. "Capability links", "Trusted systems"} (optional, one per part)
|
|
57
61
|
|
|
58
|
-
{
|
|
62
|
+
{The part's definition in one sentence, where its name does not already give it. Then the decisions this part rests on that no principle states, as bullets — a bold noun-phrase label, the rule, then its reason where earned — citing principles by number. A part with nothing beyond its principles gets no section. A sequence diagram at role level where the part is a flow.}
|
|
59
63
|
|
|
60
|
-
##
|
|
64
|
+
## Terms
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{
|
|
66
|
+
| Term | Definition |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| {every name in `objects:`, and every other term a reader could misread} | {one line} |
|
|
65
69
|
|
|
66
|
-
##
|
|
70
|
+
## Related documents
|
|
67
71
|
|
|
68
|
-
{
|
|
72
|
+
- [{Neighbour}]({neighbour}.md): {what it settles that this document does not}
|
|
69
73
|
```
|
|
70
74
|
|
|
71
|
-
###
|
|
75
|
+
### Citations
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
Principles are cited by number, sections by heading, always in parentheses: "(principle 4)" in the document itself, "(auth, principle 3)" or "(API protocol § Rate limits)" for a sibling, by its title. `doc-lint` resolves each against the folder and blocks a number or heading that does not exist, so a renumbering is caught at the next lint, not by a reader.
|
|
74
78
|
|
|
75
79
|
### What a doctrine document is not
|
|
76
80
|
|
|
@@ -78,5 +82,7 @@ Code cannot reveal what it does not yet do, so decided-but-unbuilt intent is one
|
|
|
78
82
|
| --- | --- |
|
|
79
83
|
| A tour of the code that implements the concern | The code graph |
|
|
80
84
|
| The catalog of routes, error codes or variables | An API reference or environment registry |
|
|
81
|
-
| The one feature that happens to use the concern most | That feature's own document, which names this
|
|
85
|
+
| The one feature that happens to use the concern most | That feature's own document, which names this document in its Scope |
|
|
82
86
|
| The story of how the concern got this way | The subject's architecture decision (`decisions/`), whose Decision Changelog tail carries how it moved, and the changelog for the when |
|
|
87
|
+
| A record of which product has built which part | That product's plan, backlog, or security overview |
|
|
88
|
+
| A restatement of a sibling's rule for completeness | A citation of the sibling's principle, with the one clause of context the sentence needs |
|
|
@@ -45,9 +45,9 @@ owns: ["packages/studio/src/server/devices/**"] # transitional — one region
|
|
|
45
45
|
|
|
46
46
|
## Scope
|
|
47
47
|
|
|
48
|
-
- **
|
|
48
|
+
- **In scope:** {the surfaces, commands and journeys this document owns}
|
|
49
49
|
- **Bound by:** {the doctrine documents it applies, each with what it settles}
|
|
50
|
-
- **
|
|
50
|
+
- **Out of scope:** {the neighbouring features a reader might expect here, each linked}
|
|
51
51
|
|
|
52
52
|
## Flows
|
|
53
53
|
|