@nurix/etna 0.20.43 → 0.20.45

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "774d2e20c947",
2
+ "fingerprint": "17e5c42fa923",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.20.13",
4
+ "version": "3.20.15",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -29,5 +29,5 @@
29
29
  "origin": "https://cdn.nustack.io",
30
30
  "track": "latest"
31
31
  },
32
- "digest": "55a739ce31d2"
32
+ "digest": "a95ada7dc2ef"
33
33
  }
@@ -23,13 +23,24 @@ This skill writes one document at a time: the genre registry says what kind of d
23
23
  2. **Find the existing document.** If a document on the topic already exists, extend it; a second document on one topic is drift. If the file is a frozen record (a plan at `Complete` or `Abandoned`, a committed changelog entry, an executed runbook), never edit it: write a new dated record that links back.
24
24
  3. **Earn it, and route a set.** A new truth document that states rules (doctrine, a system, an architecture decision) first lists the decisions only it would own, each checked against the documents that exist; under about six, its rules go to their homes and the document is not written. More than one sibling document, or any doctrine document, follows `references/document-sets.md` from here: a term table and a rule ledger before prose, rendered from the ledger, one cold read.
25
25
  4. **Place and name it** at the registry's location. `docs/structure.md` is the placement authority for anything the registry leaves open.
26
- 5. **Write it from the template.** Keep the template's section names exactly; omit optional sections with nothing real to say. Write the body from the outside in (style guide § Shape), every heading and bold label a noun phrase, every sentence after a label third person, one rule per sentence in the term engineers use (style guide § Sentences, § Vocabulary). In a truth document, every sentence is present tense (§ The three natures). Every line owns a decision: before stating a rule, search the neighbours for it, and cite the one found rather than restating it (style guide § Net-net).
26
+ 5. **Write it from the template, then run its genre procedure.** A template's procedure section binds that genre only: doctrine's is the reason check (`templates/doctrine.md` § Reason check). Keep the template's section names exactly; omit optional sections with nothing real to say. Write the body from the outside in (style guide § Shape), every heading and bold label a noun phrase, every sentence after a label third person, one rule per sentence in the term engineers use (style guide § Sentences, § Vocabulary). In a truth document, every sentence is present tense (§ The three natures). Every line owns a decision: before stating a rule, search the neighbours for it, and cite the one found rather than restating it (style guide § Net-net).
27
27
  6. **Write the frontmatter.** `type:` from the registry, `description:` in at most 300 characters saying what the document settles and where its boundary sits, then the genre's required keys (`references/frontmatter.md` for each key's rules).
28
28
  7. **Run the lint** from the repo root on the repo-relative path: `node <skill>/scripts/doc-lint.mjs docs/features/sign-in.md` (or `--changed`). Fix every `[block]` finding; read every `[warn]` and fix it unless the judgment it flags is genuinely yours. A disable takes bare rule ids, `<!-- doc-lint-disable: id -->`, alone on its own line for the whole file with the reason in a second comment on the next line, or at the end of one line for that line only.
29
29
  8. **Edit, don't append, on a substantive change to an existing truth document.** Keep its section shape, shed any history narration, progress marker or stale tripwire in the sections you touch, bump `last-reviewed:`, update `objects:` if what it designs changed, and remove any retired key on the way through.
30
30
 
31
31
  ---
32
32
 
33
+ ## Genre procedures
34
+
35
+ Some genres carry steps beyond writing from the template. Each procedure lives in its template, binds that genre alone, and runs at step 5 of § Writing one document.
36
+
37
+ | Genre | Procedure | Why the genre needs it |
38
+ | --- | --- | --- |
39
+ | Doctrine | `templates/doctrine.md` § Reason check: every reason judged obvious or non-obvious by a context-free sub-agent; non-obvious reasons stay or are added, obvious ones are cut | Written once in a while and read for years, so each line pays for itself; the writer cannot judge obviousness, because it knows every reason |
40
+ | Doctrine, and any set of siblings | `references/document-sets.md`: usefulness test, term table, rule ledger, one cold read | A set written one document at a time restates itself |
41
+
42
+ A genre with no row here has no procedure beyond its template.
43
+
33
44
  ## The three natures
34
45
 
35
46
  Every document is exactly one of three things, and the registry names which for every genre. Decide before writing a word: every other rule follows from the split.
@@ -48,6 +48,7 @@ Each document is written from its ledger rows and nothing else:
48
48
 
49
49
  - **Principles and part decisions:** each rule one sentence, and its reason the next wherever the rule meets a trigger of the style guide's § Sentences.
50
50
  - **Part sections:** the rows homed as part decisions. A part with no such row gets no section.
51
+ - **Reason check:** each rendered doctrine document then runs `templates/doctrine.md` § Reason check before the lint; the cold read sees only reasons a judge kept.
51
52
  - **Rules homed elsewhere:** a citation with the one clause of context the sentence needs (style guide principle 7), never the rule in new words.
52
53
  - **A sentence with no row:** filler, which is cut. A rule the writer finds missing is a question for the user (§ 3), never a row the writer adds for itself.
53
54
 
@@ -84,12 +85,12 @@ A finding names the file and line, quotes the text, names its class, and propose
84
85
  | Wrong citation | A citation whose target does not say what the citing sentence claims (the lint checks that it exists; the reader checks what it says) |
85
86
  | Split term | Two names for one concept, or one name for two |
86
87
  | No decision of its own | A document under about six owned decisions; a section, table or bullet that states nothing the principles do not |
87
- | Missing reason | A rule that meets a trigger of the style guide's § Sentences (a tempting alternative, a cost, an arbitrary value, an invisible failure) and carries no reason |
88
+ | Missing reason | A rule that meets a trigger of the style guide's § Sentences (a tempting alternative, a cost, an arbitrary value, an invisible failure) and carries no reason; in doctrine, a reason the reason check judged non-obvious and the document lacks |
88
89
  | Filler | A sentence carrying no fact, rule, boundary or reason: a scene-setting opener, a generic truth used as framing, a metaphor or slogan standing in for a rule, a question |
89
90
  | Wrong layer | Mechanism, a product's specifics or build state in doctrine; history in a truth document |
90
91
  | Fused reason | A rule and its reason joined into one sentence, so a reader cannot tell which half binds |
91
92
  | Two readings | A rule a reader could act on in two incompatible ways |
92
93
 
93
- **NIT:** wording, order, a label, a sentence that could be shorter, a plainer synonym for a standard term, a cross-reference that would help. NITs never block.
94
+ **NIT:** wording, order, a label, a sentence that could be shorter, a reason a competent engineer would supply unprompted, a plainer synonym for a standard term, a cross-reference that would help. NITs never block.
94
95
 
95
96
  In ledger mode the same classes apply to rows: two rows that contradict, a rule homed twice, a term used in two senses, a document left with fewer than about six rows, a row without its reason or without its source, a rule homed in a document whose question it does not answer.
@@ -136,7 +136,7 @@ The reader is an engineer deciding what to build or checking what was built, so
136
136
  | The value looks arbitrary: a number or a cutoff | a fifteen-minute window; a twenty-four-hour overlap |
137
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
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").
139
+ In doctrine the triggers propose and a cold reader decides: every reason, existing or proposed, goes through the reason check (`templates/doctrine.md` § Reason check), and an obvious one is cut. 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
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
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
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.
@@ -462,7 +462,8 @@ function sentencesOf(lines) {
462
462
  if (!opened) continue;
463
463
  if (DOCTRINE_CLOSING.test(line)) break;
464
464
  if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
465
- const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
465
+ // A citation's words name where a rule lives; two sentences citing one heading share no rule by it.
466
+ const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '').replace(/\([^()]*(?:principles?\s+\d|§)[^()]*\)/g, '');
466
467
  for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
467
468
  if (SPEC_SENTENCE.test(sentence)) continue;
468
469
  const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
@@ -72,6 +72,19 @@ This document defines {what, for every product}: {the specifics it covers, in pa
72
72
  - [{Neighbour}]({neighbour}.md): {what it settles that this document does not}
73
73
  ```
74
74
 
75
+ ### Reason check
76
+
77
+ Doctrine is written once in a while and read for years, so every reason in it earns its line against a reader who has not seen the writing. The writer cannot judge that: it already knows why every rule holds. So whether a reason stays is decided by a cold reader, never by the writer.
78
+
79
+ 1. **Collect the candidates:** every reason sentence the document carries, and every reason the writer would add for a rule that meets a trigger of `references/style-guide.md` § Sentences. Each candidate is the rule it explains plus the reason sentence, quoted verbatim.
80
+ 2. **Dispatch the judge:** one fresh sub-agent per document, at the standard tier, briefed with the document's path and the candidate list and nothing from the session. It reads the whole document first, because a reason another line already makes plain is obvious. For each candidate it returns `obvious` or `non-obvious` and one line of why.
81
+ 3. **Apply the verdicts:** a non-obvious reason stays, or is added as the sentence after its rule. An obvious reason is cut, and its rule stays. A reason the document cites from a sibling is judged like any other, and an obvious one loses its citation with it.
82
+
83
+ - **The test:** a reason is obvious when a competent engineer holding this document would supply it unprompted. It is non-obvious when that engineer would reach for the alternative, would not know the failure, or would read the value as arbitrary.
84
+ - **Sources still bind:** the judge decides whether a reason stays, never what it says. A non-obvious reason with no source (an owner's ruling, a sibling, the code) is a question for the owner, as in `references/document-sets.md` § 3.
85
+ - **Batching:** the judge reads each document once and returns a verdict for each candidate in it. A set gets one judge per document, run in parallel. No dispatch mechanism ⇒ the writer judges with the brief's test, and the cold read rechecks every reason (`references/document-sets.md` § Rubric).
86
+ - **Brief:** "Read `<path>` in full. For each candidate below (a rule, then its reason), answer `obvious` or `non-obvious`, with one line of why. A reason is obvious when a competent engineer holding this document would supply it unprompted. Quote each candidate back verbatim. Edit nothing."
87
+
75
88
  ### Citations
76
89
 
77
90
  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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.48",
4
+ "version": "0.2.50",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "9f42da40515c"
17
+ "digest": "3a2fc96cc22a"
18
18
  }
@@ -23,13 +23,24 @@ This skill writes one document at a time: the genre registry says what kind of d
23
23
  2. **Find the existing document.** If a document on the topic already exists, extend it; a second document on one topic is drift. If the file is a frozen record (a plan at `Complete` or `Abandoned`, a committed changelog entry, an executed runbook), never edit it: write a new dated record that links back.
24
24
  3. **Earn it, and route a set.** A new truth document that states rules (doctrine, a system, an architecture decision) first lists the decisions only it would own, each checked against the documents that exist; under about six, its rules go to their homes and the document is not written. More than one sibling document, or any doctrine document, follows `references/document-sets.md` from here: a term table and a rule ledger before prose, rendered from the ledger, one cold read.
25
25
  4. **Place and name it** at the registry's location. `docs/structure.md` is the placement authority for anything the registry leaves open.
26
- 5. **Write it from the template.** Keep the template's section names exactly; omit optional sections with nothing real to say. Write the body from the outside in (style guide § Shape), every heading and bold label a noun phrase, every sentence after a label third person, one rule per sentence in the term engineers use (style guide § Sentences, § Vocabulary). In a truth document, every sentence is present tense (§ The three natures). Every line owns a decision: before stating a rule, search the neighbours for it, and cite the one found rather than restating it (style guide § Net-net).
26
+ 5. **Write it from the template, then run its genre procedure.** A template's procedure section binds that genre only: doctrine's is the reason check (`templates/doctrine.md` § Reason check). Keep the template's section names exactly; omit optional sections with nothing real to say. Write the body from the outside in (style guide § Shape), every heading and bold label a noun phrase, every sentence after a label third person, one rule per sentence in the term engineers use (style guide § Sentences, § Vocabulary). In a truth document, every sentence is present tense (§ The three natures). Every line owns a decision: before stating a rule, search the neighbours for it, and cite the one found rather than restating it (style guide § Net-net).
27
27
  6. **Write the frontmatter.** `type:` from the registry, `description:` in at most 300 characters saying what the document settles and where its boundary sits, then the genre's required keys (`references/frontmatter.md` for each key's rules).
28
28
  7. **Run the lint** from the repo root on the repo-relative path: `node <skill>/scripts/doc-lint.mjs docs/features/sign-in.md` (or `--changed`). Fix every `[block]` finding; read every `[warn]` and fix it unless the judgment it flags is genuinely yours. A disable takes bare rule ids, `<!-- doc-lint-disable: id -->`, alone on its own line for the whole file with the reason in a second comment on the next line, or at the end of one line for that line only.
29
29
  8. **Edit, don't append, on a substantive change to an existing truth document.** Keep its section shape, shed any history narration, progress marker or stale tripwire in the sections you touch, bump `last-reviewed:`, update `objects:` if what it designs changed, and remove any retired key on the way through.
30
30
 
31
31
  ---
32
32
 
33
+ ## Genre procedures
34
+
35
+ Some genres carry steps beyond writing from the template. Each procedure lives in its template, binds that genre alone, and runs at step 5 of § Writing one document.
36
+
37
+ | Genre | Procedure | Why the genre needs it |
38
+ | --- | --- | --- |
39
+ | Doctrine | `templates/doctrine.md` § Reason check: every reason judged obvious or non-obvious by a context-free sub-agent; non-obvious reasons stay or are added, obvious ones are cut | Written once in a while and read for years, so each line pays for itself; the writer cannot judge obviousness, because it knows every reason |
40
+ | Doctrine, and any set of siblings | `references/document-sets.md`: usefulness test, term table, rule ledger, one cold read | A set written one document at a time restates itself |
41
+
42
+ A genre with no row here has no procedure beyond its template.
43
+
33
44
  ## The three natures
34
45
 
35
46
  Every document is exactly one of three things, and the registry names which for every genre. Decide before writing a word: every other rule follows from the split.
@@ -48,6 +48,7 @@ Each document is written from its ledger rows and nothing else:
48
48
 
49
49
  - **Principles and part decisions:** each rule one sentence, and its reason the next wherever the rule meets a trigger of the style guide's § Sentences.
50
50
  - **Part sections:** the rows homed as part decisions. A part with no such row gets no section.
51
+ - **Reason check:** each rendered doctrine document then runs `templates/doctrine.md` § Reason check before the lint; the cold read sees only reasons a judge kept.
51
52
  - **Rules homed elsewhere:** a citation with the one clause of context the sentence needs (style guide principle 7), never the rule in new words.
52
53
  - **A sentence with no row:** filler, which is cut. A rule the writer finds missing is a question for the user (§ 3), never a row the writer adds for itself.
53
54
 
@@ -84,12 +85,12 @@ A finding names the file and line, quotes the text, names its class, and propose
84
85
  | Wrong citation | A citation whose target does not say what the citing sentence claims (the lint checks that it exists; the reader checks what it says) |
85
86
  | Split term | Two names for one concept, or one name for two |
86
87
  | No decision of its own | A document under about six owned decisions; a section, table or bullet that states nothing the principles do not |
87
- | Missing reason | A rule that meets a trigger of the style guide's § Sentences (a tempting alternative, a cost, an arbitrary value, an invisible failure) and carries no reason |
88
+ | Missing reason | A rule that meets a trigger of the style guide's § Sentences (a tempting alternative, a cost, an arbitrary value, an invisible failure) and carries no reason; in doctrine, a reason the reason check judged non-obvious and the document lacks |
88
89
  | Filler | A sentence carrying no fact, rule, boundary or reason: a scene-setting opener, a generic truth used as framing, a metaphor or slogan standing in for a rule, a question |
89
90
  | Wrong layer | Mechanism, a product's specifics or build state in doctrine; history in a truth document |
90
91
  | Fused reason | A rule and its reason joined into one sentence, so a reader cannot tell which half binds |
91
92
  | Two readings | A rule a reader could act on in two incompatible ways |
92
93
 
93
- **NIT:** wording, order, a label, a sentence that could be shorter, a plainer synonym for a standard term, a cross-reference that would help. NITs never block.
94
+ **NIT:** wording, order, a label, a sentence that could be shorter, a reason a competent engineer would supply unprompted, a plainer synonym for a standard term, a cross-reference that would help. NITs never block.
94
95
 
95
96
  In ledger mode the same classes apply to rows: two rows that contradict, a rule homed twice, a term used in two senses, a document left with fewer than about six rows, a row without its reason or without its source, a rule homed in a document whose question it does not answer.
@@ -136,7 +136,7 @@ The reader is an engineer deciding what to build or checking what was built, so
136
136
  | The value looks arbitrary: a number or a cutoff | a fifteen-minute window; a twenty-four-hour overlap |
137
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
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").
139
+ In doctrine the triggers propose and a cold reader decides: every reason, existing or proposed, goes through the reason check (`templates/doctrine.md` § Reason check), and an obvious one is cut. 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
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
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
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.
@@ -462,7 +462,8 @@ function sentencesOf(lines) {
462
462
  if (!opened) continue;
463
463
  if (DOCTRINE_CLOSING.test(line)) break;
464
464
  if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
465
- const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
465
+ // A citation's words name where a rule lives; two sentences citing one heading share no rule by it.
466
+ const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '').replace(/\([^()]*(?:principles?\s+\d|§)[^()]*\)/g, '');
466
467
  for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
467
468
  if (SPEC_SENTENCE.test(sentence)) continue;
468
469
  const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
@@ -72,6 +72,19 @@ This document defines {what, for every product}: {the specifics it covers, in pa
72
72
  - [{Neighbour}]({neighbour}.md): {what it settles that this document does not}
73
73
  ```
74
74
 
75
+ ### Reason check
76
+
77
+ Doctrine is written once in a while and read for years, so every reason in it earns its line against a reader who has not seen the writing. The writer cannot judge that: it already knows why every rule holds. So whether a reason stays is decided by a cold reader, never by the writer.
78
+
79
+ 1. **Collect the candidates:** every reason sentence the document carries, and every reason the writer would add for a rule that meets a trigger of `references/style-guide.md` § Sentences. Each candidate is the rule it explains plus the reason sentence, quoted verbatim.
80
+ 2. **Dispatch the judge:** one fresh sub-agent per document, at the standard tier, briefed with the document's path and the candidate list and nothing from the session. It reads the whole document first, because a reason another line already makes plain is obvious. For each candidate it returns `obvious` or `non-obvious` and one line of why.
81
+ 3. **Apply the verdicts:** a non-obvious reason stays, or is added as the sentence after its rule. An obvious reason is cut, and its rule stays. A reason the document cites from a sibling is judged like any other, and an obvious one loses its citation with it.
82
+
83
+ - **The test:** a reason is obvious when a competent engineer holding this document would supply it unprompted. It is non-obvious when that engineer would reach for the alternative, would not know the failure, or would read the value as arbitrary.
84
+ - **Sources still bind:** the judge decides whether a reason stays, never what it says. A non-obvious reason with no source (an owner's ruling, a sibling, the code) is a question for the owner, as in `references/document-sets.md` § 3.
85
+ - **Batching:** the judge reads each document once and returns a verdict for each candidate in it. A set gets one judge per document, run in parallel. No dispatch mechanism ⇒ the writer judges with the brief's test, and the cold read rechecks every reason (`references/document-sets.md` § Rubric).
86
+ - **Brief:** "Read `<path>` in full. For each candidate below (a rule, then its reason), answer `obvious` or `non-obvious`, with one line of why. A reason is obvious when a competent engineer holding this document would supply it unprompted. Quote each candidate back verbatim. Edit nothing."
87
+
75
88
  ### Citations
76
89
 
77
90
  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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.38",
4
+ "version": "1.5.40",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "3e652499a2b3"
17
+ "digest": "dd5546415b1b"
18
18
  }
@@ -23,13 +23,24 @@ This skill writes one document at a time: the genre registry says what kind of d
23
23
  2. **Find the existing document.** If a document on the topic already exists, extend it; a second document on one topic is drift. If the file is a frozen record (a plan at `Complete` or `Abandoned`, a committed changelog entry, an executed runbook), never edit it: write a new dated record that links back.
24
24
  3. **Earn it, and route a set.** A new truth document that states rules (doctrine, a system, an architecture decision) first lists the decisions only it would own, each checked against the documents that exist; under about six, its rules go to their homes and the document is not written. More than one sibling document, or any doctrine document, follows `references/document-sets.md` from here: a term table and a rule ledger before prose, rendered from the ledger, one cold read.
25
25
  4. **Place and name it** at the registry's location. `docs/structure.md` is the placement authority for anything the registry leaves open.
26
- 5. **Write it from the template.** Keep the template's section names exactly; omit optional sections with nothing real to say. Write the body from the outside in (style guide § Shape), every heading and bold label a noun phrase, every sentence after a label third person, one rule per sentence in the term engineers use (style guide § Sentences, § Vocabulary). In a truth document, every sentence is present tense (§ The three natures). Every line owns a decision: before stating a rule, search the neighbours for it, and cite the one found rather than restating it (style guide § Net-net).
26
+ 5. **Write it from the template, then run its genre procedure.** A template's procedure section binds that genre only: doctrine's is the reason check (`templates/doctrine.md` § Reason check). Keep the template's section names exactly; omit optional sections with nothing real to say. Write the body from the outside in (style guide § Shape), every heading and bold label a noun phrase, every sentence after a label third person, one rule per sentence in the term engineers use (style guide § Sentences, § Vocabulary). In a truth document, every sentence is present tense (§ The three natures). Every line owns a decision: before stating a rule, search the neighbours for it, and cite the one found rather than restating it (style guide § Net-net).
27
27
  6. **Write the frontmatter.** `type:` from the registry, `description:` in at most 300 characters saying what the document settles and where its boundary sits, then the genre's required keys (`references/frontmatter.md` for each key's rules).
28
28
  7. **Run the lint** from the repo root on the repo-relative path: `node <skill>/scripts/doc-lint.mjs docs/features/sign-in.md` (or `--changed`). Fix every `[block]` finding; read every `[warn]` and fix it unless the judgment it flags is genuinely yours. A disable takes bare rule ids, `<!-- doc-lint-disable: id -->`, alone on its own line for the whole file with the reason in a second comment on the next line, or at the end of one line for that line only.
29
29
  8. **Edit, don't append, on a substantive change to an existing truth document.** Keep its section shape, shed any history narration, progress marker or stale tripwire in the sections you touch, bump `last-reviewed:`, update `objects:` if what it designs changed, and remove any retired key on the way through.
30
30
 
31
31
  ---
32
32
 
33
+ ## Genre procedures
34
+
35
+ Some genres carry steps beyond writing from the template. Each procedure lives in its template, binds that genre alone, and runs at step 5 of § Writing one document.
36
+
37
+ | Genre | Procedure | Why the genre needs it |
38
+ | --- | --- | --- |
39
+ | Doctrine | `templates/doctrine.md` § Reason check: every reason judged obvious or non-obvious by a context-free sub-agent; non-obvious reasons stay or are added, obvious ones are cut | Written once in a while and read for years, so each line pays for itself; the writer cannot judge obviousness, because it knows every reason |
40
+ | Doctrine, and any set of siblings | `references/document-sets.md`: usefulness test, term table, rule ledger, one cold read | A set written one document at a time restates itself |
41
+
42
+ A genre with no row here has no procedure beyond its template.
43
+
33
44
  ## The three natures
34
45
 
35
46
  Every document is exactly one of three things, and the registry names which for every genre. Decide before writing a word: every other rule follows from the split.
@@ -48,6 +48,7 @@ Each document is written from its ledger rows and nothing else:
48
48
 
49
49
  - **Principles and part decisions:** each rule one sentence, and its reason the next wherever the rule meets a trigger of the style guide's § Sentences.
50
50
  - **Part sections:** the rows homed as part decisions. A part with no such row gets no section.
51
+ - **Reason check:** each rendered doctrine document then runs `templates/doctrine.md` § Reason check before the lint; the cold read sees only reasons a judge kept.
51
52
  - **Rules homed elsewhere:** a citation with the one clause of context the sentence needs (style guide principle 7), never the rule in new words.
52
53
  - **A sentence with no row:** filler, which is cut. A rule the writer finds missing is a question for the user (§ 3), never a row the writer adds for itself.
53
54
 
@@ -84,12 +85,12 @@ A finding names the file and line, quotes the text, names its class, and propose
84
85
  | Wrong citation | A citation whose target does not say what the citing sentence claims (the lint checks that it exists; the reader checks what it says) |
85
86
  | Split term | Two names for one concept, or one name for two |
86
87
  | No decision of its own | A document under about six owned decisions; a section, table or bullet that states nothing the principles do not |
87
- | Missing reason | A rule that meets a trigger of the style guide's § Sentences (a tempting alternative, a cost, an arbitrary value, an invisible failure) and carries no reason |
88
+ | Missing reason | A rule that meets a trigger of the style guide's § Sentences (a tempting alternative, a cost, an arbitrary value, an invisible failure) and carries no reason; in doctrine, a reason the reason check judged non-obvious and the document lacks |
88
89
  | Filler | A sentence carrying no fact, rule, boundary or reason: a scene-setting opener, a generic truth used as framing, a metaphor or slogan standing in for a rule, a question |
89
90
  | Wrong layer | Mechanism, a product's specifics or build state in doctrine; history in a truth document |
90
91
  | Fused reason | A rule and its reason joined into one sentence, so a reader cannot tell which half binds |
91
92
  | Two readings | A rule a reader could act on in two incompatible ways |
92
93
 
93
- **NIT:** wording, order, a label, a sentence that could be shorter, a plainer synonym for a standard term, a cross-reference that would help. NITs never block.
94
+ **NIT:** wording, order, a label, a sentence that could be shorter, a reason a competent engineer would supply unprompted, a plainer synonym for a standard term, a cross-reference that would help. NITs never block.
94
95
 
95
96
  In ledger mode the same classes apply to rows: two rows that contradict, a rule homed twice, a term used in two senses, a document left with fewer than about six rows, a row without its reason or without its source, a rule homed in a document whose question it does not answer.
@@ -136,7 +136,7 @@ The reader is an engineer deciding what to build or checking what was built, so
136
136
  | The value looks arbitrary: a number or a cutoff | a fifteen-minute window; a twenty-four-hour overlap |
137
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
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").
139
+ In doctrine the triggers propose and a cold reader decides: every reason, existing or proposed, goes through the reason check (`templates/doctrine.md` § Reason check), and an obvious one is cut. 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
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
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
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.
@@ -462,7 +462,8 @@ function sentencesOf(lines) {
462
462
  if (!opened) continue;
463
463
  if (DOCTRINE_CLOSING.test(line)) break;
464
464
  if (/^\s*(#|\|)/.test(line) || !line.trim()) continue;
465
- const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '');
465
+ // A citation's words name where a rule lives; two sentences citing one heading share no rule by it.
466
+ const plain = line.replace(/^\s*([-*]|\d+\.)\s+/, '').replace(/\*\*[^*]+\*\*:?/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/`/g, '').replace(/\([^()]*(?:principles?\s+\d|§)[^()]*\)/g, '');
466
467
  for (const sentence of plain.split(/(?<=[.;])\s+(?=[A-Z(])/)) {
467
468
  if (SPEC_SENTENCE.test(sentence)) continue;
468
469
  const words = new Set((sentence.toLowerCase().match(/[a-z][a-z'-]+/g) || []).filter((w) => w.length > 2 && !STOPWORDS.has(w)));
@@ -72,6 +72,19 @@ This document defines {what, for every product}: {the specifics it covers, in pa
72
72
  - [{Neighbour}]({neighbour}.md): {what it settles that this document does not}
73
73
  ```
74
74
 
75
+ ### Reason check
76
+
77
+ Doctrine is written once in a while and read for years, so every reason in it earns its line against a reader who has not seen the writing. The writer cannot judge that: it already knows why every rule holds. So whether a reason stays is decided by a cold reader, never by the writer.
78
+
79
+ 1. **Collect the candidates:** every reason sentence the document carries, and every reason the writer would add for a rule that meets a trigger of `references/style-guide.md` § Sentences. Each candidate is the rule it explains plus the reason sentence, quoted verbatim.
80
+ 2. **Dispatch the judge:** one fresh sub-agent per document, at the standard tier, briefed with the document's path and the candidate list and nothing from the session. It reads the whole document first, because a reason another line already makes plain is obvious. For each candidate it returns `obvious` or `non-obvious` and one line of why.
81
+ 3. **Apply the verdicts:** a non-obvious reason stays, or is added as the sentence after its rule. An obvious reason is cut, and its rule stays. A reason the document cites from a sibling is judged like any other, and an obvious one loses its citation with it.
82
+
83
+ - **The test:** a reason is obvious when a competent engineer holding this document would supply it unprompted. It is non-obvious when that engineer would reach for the alternative, would not know the failure, or would read the value as arbitrary.
84
+ - **Sources still bind:** the judge decides whether a reason stays, never what it says. A non-obvious reason with no source (an owner's ruling, a sibling, the code) is a question for the owner, as in `references/document-sets.md` § 3.
85
+ - **Batching:** the judge reads each document once and returns a verdict for each candidate in it. A set gets one judge per document, run in parallel. No dispatch mechanism ⇒ the writer judges with the brief's test, and the cold read rechecks every reason (`references/document-sets.md` § Rubric).
86
+ - **Brief:** "Read `<path>` in full. For each candidate below (a rule, then its reason), answer `obvious` or `non-obvious`, with one line of why. A reason is obvious when a competent engineer holding this document would supply it unprompted. Quote each candidate back verbatim. Edit nothing."
87
+
75
88
  ### Citations
76
89
 
77
90
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.43",
3
+ "version": "0.20.45",
4
4
  "description": "Bootstrap a project with a Nurix harness — writes the committed contract (CLAUDE.md, AGENTS.md, .claude/harness.json, settings.json, the mount hook) and mounts the harness itself, gitignored, from a bundled payload; sessions keep it current from the CDN.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,5 +46,5 @@
46
46
  "agent",
47
47
  "plugin"
48
48
  ],
49
- "etnaFingerprint": "774d2e20c947"
49
+ "etnaFingerprint": "17e5c42fa923"
50
50
  }