@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
|
@@ -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
|
|
|
@@ -45,7 +45,7 @@ owns: ["packages/{name}/**"] # transitional — the residual
|
|
|
45
45
|
|
|
46
46
|
- **Role:** {what it holds that nothing else does}
|
|
47
47
|
- **Consumers:** {who consumes it, and how and when it ships}
|
|
48
|
-
- **
|
|
48
|
+
- **Out of scope:** {what it deliberately does not hold, and where that lives}
|
|
49
49
|
|
|
50
50
|
## Ownership
|
|
51
51
|
|
|
@@ -41,8 +41,8 @@ owns: # transitional — region globs
|
|
|
41
41
|
|
|
42
42
|
## Scope
|
|
43
43
|
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
44
|
+
- **In scope:** {what this machine decides}
|
|
45
|
+
- **Out of scope:** {subject} — {owning document}; {subject} — {owning document}
|
|
46
46
|
|
|
47
47
|
## Flow
|
|
48
48
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "delivery",
|
|
3
3
|
"name": "delivery",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.47",
|
|
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": "
|
|
17
|
+
"digest": "0e79c085c86d"
|
|
18
18
|
}
|
|
@@ -11,9 +11,9 @@ This skill writes one document at a time: the genre registry says what kind of d
|
|
|
11
11
|
|
|
12
12
|
1. This file.
|
|
13
13
|
2. Your genre's row in [`templates/Readme.md`](templates/Readme.md) § Registry, then the template that row names.
|
|
14
|
-
3. [`references/style-guide.md`](references/style-guide.md) § The model and §
|
|
14
|
+
3. [`references/style-guide.md`](references/style-guide.md) § The model, § Labels, § Sentences and § Vocabulary before writing prose; the rest of it only for the section you are writing.
|
|
15
15
|
4. [`references/frontmatter.md`](references/frontmatter.md) when you write or change frontmatter keys beyond `type:` and `description:`.
|
|
16
|
-
5. Only for their own tasks: [`references/untemplated.md`](references/untemplated.md) (no registry row fits), [`references/instruction-style.md`](references/instruction-style.md) (instruction files), [`references/source-comments.md`](references/source-comments.md) (source-file comments), [`references/examples.md`](references/examples.md) (worked good and bad examples).
|
|
16
|
+
5. Only for their own tasks: [`references/document-sets.md`](references/document-sets.md) (more than one sibling document, or a doctrine document), [`references/untemplated.md`](references/untemplated.md) (no registry row fits), [`references/instruction-style.md`](references/instruction-style.md) (instruction files), [`references/source-comments.md`](references/source-comments.md) (source-file comments), [`references/examples.md`](references/examples.md) (worked good and bad examples).
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -21,11 +21,12 @@ This skill writes one document at a time: the genre registry says what kind of d
|
|
|
21
21
|
|
|
22
22
|
1. **Find the genre.** Match the request against § Routing, then read that genre's registry row: nature, `type:`, location, required keys, required sections. No row fits ⇒ `references/untemplated.md`.
|
|
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
|
-
3. **
|
|
25
|
-
4. **
|
|
26
|
-
5. **Write the
|
|
27
|
-
6. **
|
|
28
|
-
7. **
|
|
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
|
+
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).
|
|
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
|
+
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
|
+
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.
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -67,7 +68,7 @@ The full strip-list and the record-side rules: `references/style-guide.md` § Th
|
|
|
67
68
|
- **Intent and promises; facts are the graph's.** An owning document (feature, package, system) says why the product is shaped this way and what it promises, and names the objects it designs in `objects:`. Every computable fact (a table's columns, which function writes which column, which route sits behind which guard, which file holds what) is the code's, and a hand-typed copy is wrong the day the code moves. `doc-lint` blocks the retired mechanism sections (`mechanism-section`) and an owning document with no `objects:`.
|
|
68
69
|
- **Read by people, pointed at by the footer.** Sharp bullets under noun-phrase headings, each a bold label, a colon, one or two plain sentences; code named only where a person types or sees it, or at a cross-cutting seam; every other pointer in the closing **For the agent** footer (style guide § Owning documents — the body and the footer).
|
|
69
70
|
- **The stability test.** A sentence that would change when a function is renamed, a file moves, or a route, column or error code changes is the code's, not the document's. The code is the authority on *what*; the document on *why* (style guide § Shape).
|
|
70
|
-
- **Unbuilt work has one home per kind.**
|
|
71
|
+
- **Unbuilt work has one home per kind, outside doctrine.** A doctrine document is a specification and records no product's build state. Elsewhere, decided, unbuilt design sits in the body, wrapped in an `UNBUILT` marker; a shortfall in what is built is one bullet under Gaps; the work that would close either is a backlog row, which the document never tracks (style guide § The stability test).
|
|
71
72
|
- **No empty stubs.** Omit a section with nothing real to say. Write "None" only where the absence is itself information, such as a plan's Cross-Module Contracts Affected.
|
|
72
73
|
- **No redirect documents.** Moving a document deletes the old file and repoints every inbound link; a "Moved to X" stub rots and creates false search hits.
|
|
73
74
|
|
|
@@ -92,7 +93,8 @@ A document names the objects it designs in `objects:`, and a source file names t
|
|
|
92
93
|
| Document a feature (LLD) | [`templates/feature.md`](templates/feature.md) |
|
|
93
94
|
| Document a feature's data flow or state | The feature doc's Flows (behaviour) and Promises (guarantees); a persisted shape belongs in the API reference (style guide § State at persistence points); component and transient state are not documented |
|
|
94
95
|
| Document a workspace package | [`templates/package.md`](templates/package.md), the residual owner of `packages/{name}/**` |
|
|
95
|
-
| Document one concern the platform handles everywhere (authentication, storage, the API protocol) | [`templates/doctrine.md`](templates/doctrine.md): design and principles, no mechanism, owns no code |
|
|
96
|
+
| Document one concern the platform handles everywhere (authentication, storage, the API protocol) | [`templates/doctrine.md`](templates/doctrine.md): design and principles, no mechanism, owns no code; written through [`references/document-sets.md`](references/document-sets.md) |
|
|
97
|
+
| Write or rework several sibling documents at once (a doctrine folder, a family of features) | [`references/document-sets.md`](references/document-sets.md): usefulness test, term table, rule ledger, render, one cold read |
|
|
96
98
|
| Document one system spanning packages | [`templates/system.md`](templates/system.md): one machine end to end, never a giant feature doc |
|
|
97
99
|
| Record or revise an architecture decision | [`templates/architecture-decision.md`](templates/architecture-decision.md): one AD per subject, revised in place |
|
|
98
100
|
| Create or update a changelog entry | [`templates/changelog-entry.md`](templates/changelog-entry.md) (record; frozen inside its commit; no row in `CHANGELOG.md`) |
|
|
@@ -125,12 +127,13 @@ A document names the objects it designs in `objects:`, and a source file names t
|
|
|
125
127
|
| --- | --- |
|
|
126
128
|
| `templates/Readme.md` | The genre registry: every genre's nature, `type:`, location, required keys and sections; `doc-lint` reads it |
|
|
127
129
|
| `templates/` | One template per genre the registry names |
|
|
128
|
-
| `references/
|
|
130
|
+
| `references/document-sets.md` | Writing several sibling documents as one design: the usefulness test, term table, rule ledger, render, and the review rubric the `doc-reviewer` agent reads |
|
|
131
|
+
| `references/style-guide.md` | The prose authority: the nine principles, what earns a line, depth per genre, the outside-in shape and stability test, labels and voice, owning documents, tense, diagrams, reference documents |
|
|
129
132
|
| `references/frontmatter.md` | Each frontmatter key's rules, the transitional `owns:`, the retired keys |
|
|
130
133
|
| `references/instruction-style.md` | The style for instruction files |
|
|
131
134
|
| `references/untemplated.md` | Writing a document no template covers, and co-authoring one with a person |
|
|
132
135
|
| `references/source-comments.md` | Source-file comments, and the scripts that check them |
|
|
133
136
|
| `references/examples.md` | Good and bad documentation, side by side |
|
|
134
|
-
| `scripts/doc-lint.mjs` | Mechanical checks over written docs: tense and history, dates, redirect stubs, frontmatter, the registry's keys and sections, index shape, owning-document body |
|
|
137
|
+
| `scripts/doc-lint.mjs` | Mechanical checks over written docs: tense and history, dates, redirect stubs, frontmatter, the registry's keys and sections, index shape, owning-document body, and a doctrine folder read as a set (citations resolve; no shared objects, restated sentences, body links or `UNBUILT` markers) |
|
|
135
138
|
| `scripts/source-lint.mjs` | Mechanical checks over source comments; run `--changed` before finishing any change that writes source |
|
|
136
139
|
| `scripts/comment-strip.mjs` | Proof that a comment sweep changed no executable code |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Writing a set of documents
|
|
2
|
+
|
|
3
|
+
Load when writing or reworking more than one sibling document at once (a doctrine folder, a family of feature documents), or one doctrine document whose rules meet its siblings'. A single feature or package document needs only the steps in `SKILL.md`. One document joining an existing set runs every step below, with the existing documents as the rest of the set (§ One document into an existing set).
|
|
4
|
+
|
|
5
|
+
A set written one document at a time restates itself. Each document is made self-contained, so the same rule lands in four documents in four wordings, two of them contradict, and the defects surface only on rereading, which costs more than the writing did. So the set is designed once, as data, before any prose: every rule gets one home, every concept one name, and the review reads a table, where a fix is one edit, instead of twenty documents, where a fix is a reread of every one that states the rule.
|
|
6
|
+
|
|
7
|
+
## 1. Usefulness test
|
|
8
|
+
|
|
9
|
+
For each proposed document, list the decisions only it would own: rules a reader would get wrong without it, each checked against the documents that exist, by a search for its distinctive phrase. A decision another document already states belongs to that document.
|
|
10
|
+
|
|
11
|
+
- **Fewer than about six owned decisions:** the document is a section of a neighbour, or its rules belong in several neighbours. Fold them and drop the document.
|
|
12
|
+
- **Two documents owning overlapping lists:** the boundary between them is not drawn. Redraw it or merge them.
|
|
13
|
+
- **The output:** the document list, each with its owned decisions and its boundary sentence (the questions next to it, and which document answers each).
|
|
14
|
+
|
|
15
|
+
## 2. Term table
|
|
16
|
+
|
|
17
|
+
One row per concept: its name, a one-line definition, the one document that governs it (which lists it in `objects:`), and the synonyms it bans. The table comes before the ledger, because a rule written with two names for one concept becomes two rules.
|
|
18
|
+
|
|
19
|
+
The table starts from what the existing documents already name: every sibling's `objects:` and the definitions in its Terms table (an owning document's "For the agent" objects line). A new term is checked against them before it is used, because the collision a writer cannot see is with a name a sibling already gave another concept ("consent" for a client's scope, beside a sibling's consent of a data subject).
|
|
20
|
+
|
|
21
|
+
| Term | Means | Governed by | Not |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| operator | a person on the platform's own staff | authorization service | admin, staff account |
|
|
24
|
+
|
|
25
|
+
## 3. Rule ledger
|
|
26
|
+
|
|
27
|
+
One row per rule, from the user's rulings, the documents that exist, and the code where it states a design:
|
|
28
|
+
|
|
29
|
+
| Id | Rule | Reason | Source | Home | As | Stated in | Cited by |
|
|
30
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
31
|
+
| R12 | A change that narrows access ends the sessions it affects | a narrowed role must not outlive its token | owner ruling 6 | auth service | principle 5 | auth service | authorization service, security |
|
|
32
|
+
|
|
33
|
+
- **A source for every row:** an owner's ruling, a sibling's statement, or the code where it states a design. A rule with no source (a case the rulings leave open, a consequence the writer finds plausible) is not a row: it is a question for the user, with a recommendation, and the documents say nothing on the case until it is answered. Rules a writer supplies itself are where a written set's own defects concentrate, because no one has decided them.
|
|
34
|
+
|
|
35
|
+
- **Stated in, searched, not remembered:** each row's distinctive phrase is searched across the existing documents, and every document already stating the rule is listed. The ledger's defects are mostly here: a rule the writer homes in a new document while a sibling still states it is a double home on the first read.
|
|
36
|
+
- **One home per rule:** the document whose question the rule answers. A rule that seems to need two homes is resolved here, by picking one and adding the other to "Cited by". A rule a sibling already states keeps that home unless the same change edits the sibling to cite the new one; a home moves only with both edits.
|
|
37
|
+
- **Contradictions resolved here:** two rows that cannot both hold are settled before any prose exists. Where one source plainly outranks the other (the owner's latest ruling over an older sibling sentence), the ledger follows it and the losing statement is a sibling edit owed; the rest go to the user in one batch with the open cases, a recommendation first for each.
|
|
38
|
+
- **As:** a principle where the rule holds across the document's parts, a part's decision where it holds for one part only.
|
|
39
|
+
- **Scratch, never committed:** the documents are the record, and a committed ledger would be a second copy to drift from them.
|
|
40
|
+
|
|
41
|
+
## 4. Ledger review
|
|
42
|
+
|
|
43
|
+
One reviewer reads the ledger and the term table, not prose: the `doc-reviewer` agent in ledger mode, with § Rubric. Without the agent, a context-free sub-agent briefed with § Rubric and the paths, never the writer itself: a writer reviewing its own ledger passes its own blind spots, and in this skill's eval a self-reviewed ledger reached the cold read carrying seven blocking findings. Fixes land in the ledger, so every document rendered after them inherits them.
|
|
44
|
+
|
|
45
|
+
## 5. Render
|
|
46
|
+
|
|
47
|
+
Each document is written from its ledger rows and nothing else:
|
|
48
|
+
|
|
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
|
+
- **Part sections:** the rows homed as part decisions. A part with no such row gets no section.
|
|
51
|
+
- **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
|
+
- **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
|
+
Writers may run in parallel, one sub-agent per document or per few documents. Each brief carries the ledger, the term table, the template, the style guide's § Labels, § Voice (with § Sentences and § Vocabulary) and § Net-net, the rows homed in its documents, and the instruction to write only its own files. Each writer returns paths, not prose.
|
|
55
|
+
|
|
56
|
+
## 6. Lint
|
|
57
|
+
|
|
58
|
+
`doc-lint` over the folder. Block findings are fixed. Each `sibling-duplicate` warning is a restatement the ledger did not ask for, or a row the ledger missed; fix it in the document.
|
|
59
|
+
|
|
60
|
+
## 7. Review
|
|
61
|
+
|
|
62
|
+
One cold read of the set by the `doc-reviewer` agent in docs mode, with § Rubric. Its brief carries the paths and nothing of the session, because a reader who saw the session already knows every undefined term. Without the agent, a context-free sub-agent briefed with § Rubric does the read.
|
|
63
|
+
|
|
64
|
+
- **BLOCKING findings:** fixed.
|
|
65
|
+
- **NITs:** reported to the user as one list, never looped on.
|
|
66
|
+
- **After the fixes:** the changed lines only are verified: re-lint, and re-read each citation a fix touched. A fix never adds an unsourced rule: a finding that needs a decision the sources do not make (a Two readings the rulings left open) narrows the sentence to what the sources say and goes to the user as a question. There is no second full cold read: a review that keeps finding new BLOCKING items is reading prose the ledger never governed, so the fix is to the ledger and a re-render of the documents it touches.
|
|
67
|
+
|
|
68
|
+
## One document into an existing set
|
|
69
|
+
|
|
70
|
+
- **The siblings are the set:** the term table and the ledger are built against them (§ 2, § 3), and the cold read reads the new document against them.
|
|
71
|
+
- **A fix that belongs in a sibling is made in the sibling,** in the same change: a rule whose home moved, a term renamed, a principle that now has an exception. Where the sibling is outside the change's scope, the fix is reported to the user as a sibling edit owed, never absorbed by restating the rule in the new document.
|
|
72
|
+
- **A contradiction with a sibling's principle** is never settled by the new document alone: it is an exception the sibling's principle must cite, or a question for the user.
|
|
73
|
+
|
|
74
|
+
## Rubric
|
|
75
|
+
|
|
76
|
+
A finding names the file and line, quotes the text, names its class, and proposes the fix. "Could be clearer" with no fix is not a finding.
|
|
77
|
+
|
|
78
|
+
**BLOCKING:** the set is wrong, or unreadable as a set, until fixed.
|
|
79
|
+
|
|
80
|
+
| Class | The defect |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| Contradiction | Two statements that cannot both hold, in one document or across two |
|
|
83
|
+
| Double home | A rule stated, not cited, in two documents, or twice in one |
|
|
84
|
+
| 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
|
+
| Split term | Two names for one concept, or one name for two |
|
|
86
|
+
| 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
|
+
| 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
|
+
| Wrong layer | Mechanism, a product's specifics or build state in doctrine; history in a truth document |
|
|
90
|
+
| Fused reason | A rule and its reason joined into one sentence, so a reader cannot tell which half binds |
|
|
91
|
+
| Two readings | A rule a reader could act on in two incompatible ways |
|
|
92
|
+
|
|
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
|
+
|
|
95
|
+
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.
|