@nurix/etna 0.20.9 → 0.20.10
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/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +7 -4
- package/data/harnesses/code/hooks/lib/ownership.mjs +51 -19
- package/data/harnesses/code/skills/documentation/SKILL.md +10 -3
- package/data/harnesses/code/skills/documentation/references/frontmatter.md +21 -7
- package/data/harnesses/code/skills/documentation/templates/Readme.md +3 -1
- package/data/harnesses/code/skills/documentation/templates/package.md +131 -0
- package/data/harnesses/code/skills/documentation/templates/structure.md +9 -3
- package/data/harnesses/code/skills/documentation/templates/system.md +143 -0
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/SKILL.md +10 -3
- package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +21 -7
- package/data/harnesses/delivery/skills/documentation/templates/Readme.md +3 -1
- package/data/harnesses/delivery/skills/documentation/templates/package.md +131 -0
- package/data/harnesses/delivery/skills/documentation/templates/structure.md +9 -3
- package/data/harnesses/delivery/skills/documentation/templates/system.md +143 -0
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/SKILL.md +10 -3
- package/data/harnesses/etna/skills/documentation/references/frontmatter.md +21 -7
- package/data/harnesses/etna/skills/documentation/templates/Readme.md +3 -1
- package/data/harnesses/etna/skills/documentation/templates/package.md +131 -0
- package/data/harnesses/etna/skills/documentation/templates/structure.md +9 -3
- package/data/harnesses/etna/skills/documentation/templates/system.md +143 -0
- package/package.json +2 -2
package/data/harness-index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.14.
|
|
4
|
+
"version": "3.14.8",
|
|
5
5
|
"files": [
|
|
6
6
|
".prettierignore",
|
|
7
7
|
".prettierrc.json",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
}
|
|
26
26
|
],
|
|
27
27
|
"docsRoot": "docs",
|
|
28
|
-
"digest": "
|
|
28
|
+
"digest": "5de54f6b1a71"
|
|
29
29
|
}
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
resolveProjectDir,
|
|
14
14
|
buildIndex,
|
|
15
15
|
buildEdgeIndex,
|
|
16
|
-
|
|
16
|
+
scanOwningDocs,
|
|
17
|
+
isOwningDocPath,
|
|
17
18
|
owningDocs,
|
|
18
19
|
owningDoc,
|
|
19
20
|
listRepoFiles,
|
|
@@ -164,7 +165,7 @@ function driftBanner(projectDir) {
|
|
|
164
165
|
// Re-verify before counting: the walls must act on conditions that are live NOW, not on rows
|
|
165
166
|
// that happen to still be written down.
|
|
166
167
|
const swept = sweepResolvedRows(projectDir, repoFiles);
|
|
167
|
-
const docs =
|
|
168
|
+
const docs = scanOwningDocs(projectDir);
|
|
168
169
|
const dead = deadGlobs(projectDir, repoFiles, docs);
|
|
169
170
|
const queueRows = readQueue(projectDir);
|
|
170
171
|
const counts = countRows(queueRows);
|
|
@@ -534,8 +535,10 @@ function postToolUse(projectDir, payload) {
|
|
|
534
535
|
const rel = relIfInside(projectDir, input.file_path || input.notebook_path);
|
|
535
536
|
if (rel) detectorForFile(projectDir, rel, meta, rows);
|
|
536
537
|
const root = docsRoot(projectDir);
|
|
537
|
-
// Editing
|
|
538
|
-
|
|
538
|
+
// Editing an owning doc's frontmatter can orphan globs the same way a rename can. The test is
|
|
539
|
+
// the scan's own predicate, not a `features/` prefix — a package or system doc owns code too,
|
|
540
|
+
// and a folder guess here silently stops checking the moment a genre lands somewhere else.
|
|
541
|
+
if (rel && isOwningDocPath(projectDir, rel)) detectRenames(projectDir, "git mv", meta, rows);
|
|
539
542
|
// A manifest or history file written by a non-Bash tool is the same evasion surface.
|
|
540
543
|
if (rel && (path.posix.basename(rel) === "package.json" || rel.startsWith(`${root}/history/`))) {
|
|
541
544
|
detectOutcomes(projectDir, meta, rows);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// harness/code/hooks/lib/ownership.mjs — frontmatter scan + owns:-glob resolution, shared by the
|
|
2
2
|
// hook entrypoints (awareness, compliance) and the CI checks so gates and pipeline cannot disagree.
|
|
3
|
-
// Everything here is read-only over docs
|
|
3
|
+
// Everything here is read-only over the docs tree and the source tree; no network, no deps.
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
@@ -81,7 +81,7 @@ export function field(block, key) {
|
|
|
81
81
|
// A bare key may continue as an indented inline array on the next line, or a `- item` block.
|
|
82
82
|
// The array may span several lines, so join through the closing bracket before parsing —
|
|
83
83
|
// reading only the first line yields a bare "[", which parses to [] and makes the whole doc
|
|
84
|
-
// look like it owns nothing. `
|
|
84
|
+
// look like it owns nothing. `scanOwningDocs` skips such docs, so the failure is invisible:
|
|
85
85
|
// the doc drops out of the ownership index and out of every dead-glob count.
|
|
86
86
|
const next = (lines[i + 1] || "").trim();
|
|
87
87
|
if (next.startsWith("[")) {
|
|
@@ -114,13 +114,38 @@ export function asList(v) {
|
|
|
114
114
|
return (Array.isArray(v) ? v : [v]).map((s) => String(s).trim()).filter(Boolean);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Folders under the docs root that never hold an owning document, whatever their frontmatter says.
|
|
118
|
+
// `history/` is frozen records — a completed plan's frontmatter is history, not a live claim on code;
|
|
119
|
+
// `research/` is working material that has not been folded into the truth tree yet.
|
|
120
|
+
const NON_OWNING_DIRS = new Set(["history", "research"]);
|
|
121
|
+
|
|
122
|
+
// True when a repo-relative docs path is one the ownership scan reads — the same predicate
|
|
123
|
+
// `scanOwningDocs` filters by, exported so callers cannot drift from it. A hook asking "could
|
|
124
|
+
// editing this file have orphaned a glob?" must ask exactly this question, not a folder guess.
|
|
125
|
+
export function isOwningDocPath(projectDir, relPath) {
|
|
126
|
+
const rel = String(relPath).split(path.sep).join("/");
|
|
127
|
+
const root = docsRoot(projectDir);
|
|
128
|
+
if (!rel.startsWith(`${root}/`) || !rel.endsWith(".md") || rel.includes(".bkp.")) return false;
|
|
129
|
+
return !NON_OWNING_DIRS.has(rel.slice(root.length + 1).split("/")[0]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Scan the docs tree and return one entry per document that declares owns:.
|
|
118
133
|
// Shape: { doc (repo-relative, /-separated), owns[], module, keywords[], status }.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
//
|
|
135
|
+
// OWNERSHIP IS A FRONTMATTER FACT, NOT A FOLDER FACT. This scanned `docs/features/` alone, which
|
|
136
|
+
// silently un-owned every path claimed by a genre living elsewhere — a `type: package` doc under
|
|
137
|
+
// `docs/architecture/packages/`, a `type: system` doc under `docs/architecture/systems/`. Their
|
|
138
|
+
// globs parsed fine and matched real files; nothing failed, the paths simply read as unowned in
|
|
139
|
+
// the generated index and in the drift report, which is the one failure mode neither surfaces.
|
|
140
|
+
export function scanOwningDocs(projectDir) {
|
|
141
|
+
const root = path.join(projectDir, docsRoot(projectDir));
|
|
142
|
+
// *.bkp.* files are install-time backups awaiting the maintenance fold — a backed-up doc
|
|
143
|
+
// still carries owns: frontmatter, and scanning it would double-own every path it names.
|
|
144
|
+
const files = listMarkdown(root).filter((f) => {
|
|
145
|
+
if (f.includes(".bkp.")) return false;
|
|
146
|
+
const rel = path.relative(root, f).split(path.sep);
|
|
147
|
+
return !NON_OWNING_DIRS.has(rel[0]);
|
|
148
|
+
});
|
|
124
149
|
const docs = [];
|
|
125
150
|
for (const file of files) {
|
|
126
151
|
let text;
|
|
@@ -146,10 +171,17 @@ export function scanFeatureDocs(projectDir) {
|
|
|
146
171
|
|
|
147
172
|
// Build the ownership index text from every feature doc's frontmatter.
|
|
148
173
|
export function buildIndex(projectDir) {
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
// Every document eligible to own, counted independently of scanOwningDocs — which drops a doc
|
|
175
|
+
// carrying no owns:, making a parse failure and a doc that owns nothing look alike. Genres that
|
|
176
|
+
// never own (checklists, operations notes) sit in this total, so `skipped` is a ceiling on the
|
|
177
|
+
// parse failures rather than a count of them; it is a smell to look at, not a gate.
|
|
178
|
+
const root = path.join(projectDir, docsRoot(projectDir));
|
|
179
|
+
const total = listMarkdown(root).filter((f) => {
|
|
180
|
+
if (f.includes(".bkp.")) return false;
|
|
181
|
+
const rel = path.relative(root, f).split(path.sep);
|
|
182
|
+
return !NON_OWNING_DIRS.has(rel[0]);
|
|
183
|
+
}).length;
|
|
184
|
+
const docs = scanOwningDocs(projectDir);
|
|
153
185
|
const lines = [];
|
|
154
186
|
let deprecated = 0;
|
|
155
187
|
for (const entry of docs) {
|
|
@@ -171,7 +203,7 @@ export function buildIndex(projectDir) {
|
|
|
171
203
|
if (lines.length === 0) return "";
|
|
172
204
|
lines.sort();
|
|
173
205
|
const header =
|
|
174
|
-
"# Ownership index (generated) — owning
|
|
206
|
+
"# Ownership index (generated) — owning doc · module · status, then the code regions it governs.\n" +
|
|
175
207
|
"# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
|
|
176
208
|
const indexed = docs.length - deprecated;
|
|
177
209
|
const skipped = total - docs.length;
|
|
@@ -179,7 +211,7 @@ export function buildIndex(projectDir) {
|
|
|
179
211
|
if (deprecated > 0) notes.push(`${deprecated} deprecated`);
|
|
180
212
|
if (skipped > 0) notes.push(`${skipped} with no owns:`);
|
|
181
213
|
const footer = notes.length
|
|
182
|
-
? `\n# (${indexed}/${total}
|
|
214
|
+
? `\n# (${indexed}/${total} doc(s) indexed - ${notes.join(", ")})`
|
|
183
215
|
: "";
|
|
184
216
|
return header + lines.join("\n") + footer;
|
|
185
217
|
}
|
|
@@ -270,11 +302,11 @@ export function globToRegExp(glob) {
|
|
|
270
302
|
return new RegExp(`^${re}$`);
|
|
271
303
|
}
|
|
272
304
|
|
|
273
|
-
//
|
|
305
|
+
// Documents owning the given repo-relative path (deprecated docs excluded).
|
|
274
306
|
export function owningDocs(projectDir, relPath, docs) {
|
|
275
307
|
const rel = relPath.split(path.sep).join("/");
|
|
276
308
|
const out = [];
|
|
277
|
-
for (const entry of docs ||
|
|
309
|
+
for (const entry of docs || scanOwningDocs(projectDir)) {
|
|
278
310
|
if (entry.status === "deprecated") continue;
|
|
279
311
|
if (entry.owns.some((g) => globToRegExp(g).test(rel))) out.push(entry);
|
|
280
312
|
}
|
|
@@ -305,12 +337,12 @@ export function listRepoFiles(projectDir) {
|
|
|
305
337
|
return out;
|
|
306
338
|
}
|
|
307
339
|
|
|
308
|
-
// Globs across all
|
|
340
|
+
// Globs across all owning docs that match zero files on disk.
|
|
309
341
|
// Returns [{ glob, doc }]; pass precomputed file/doc lists to amortize the walks.
|
|
310
342
|
export function deadGlobs(projectDir, repoFiles, docs) {
|
|
311
343
|
const files = repoFiles || listRepoFiles(projectDir);
|
|
312
344
|
const dead = [];
|
|
313
|
-
for (const entry of docs ||
|
|
345
|
+
for (const entry of docs || scanOwningDocs(projectDir)) {
|
|
314
346
|
if (entry.status === "deprecated") continue;
|
|
315
347
|
for (const glob of entry.owns) {
|
|
316
348
|
const re = globToRegExp(glob);
|
|
@@ -376,7 +408,7 @@ export function owningDoc(projectDir, relPath, docs) {
|
|
|
376
408
|
export function overlaps(projectDir, repoFiles, docs) {
|
|
377
409
|
const files = repoFiles || listRepoFiles(projectDir);
|
|
378
410
|
const owners = [];
|
|
379
|
-
for (const entry of docs ||
|
|
411
|
+
for (const entry of docs || scanOwningDocs(projectDir)) {
|
|
380
412
|
if (entry.status === "deprecated") continue;
|
|
381
413
|
for (const glob of entry.owns) {
|
|
382
414
|
const s = specificity(glob);
|
|
@@ -60,6 +60,8 @@ The two guarantees are complementary and both break on mixing: a reader holding
|
|
|
60
60
|
| Baseline / reconcile docs | `maintenance` agent (baseline mode) — it calls this skill per template |
|
|
61
61
|
| Document feature/module (LLD) | [`templates/feature.md`](templates/feature.md) |
|
|
62
62
|
| Document data flow / state / Redux | [`templates/feature.md`](templates/feature.md) — they are sections of the feature doc |
|
|
63
|
+
| Document a workspace package (`docs/architecture/packages/{package}.md`) | [`templates/package.md`](templates/package.md) — the **residual** owner of `packages/{name}/**`: its manifest, build, entry, layout and every behavior no system or feature doc claims at higher specificity |
|
|
64
|
+
| Document one system spanning packages (`docs/architecture/systems/{system}.md`) | [`templates/system.md`](templates/system.md) — one machine end to end; its `owns:` names the machine's subtrees, narrower than the package glob, so it wins by specificity. Not a screen — a system doc written as a giant feature doc is the mistake it prevents |
|
|
63
65
|
| Create/update changelog entry | [`templates/changelog-entry.md`](templates/changelog-entry.md) (record — one per unit of work; carries the refinement fields, frozen inside its commit, distilled by the `retro` agent) |
|
|
64
66
|
| Create/update plan | [`templates/plan.md`](templates/plan.md) (record) |
|
|
65
67
|
| Record/revise architecture decision | [`templates/architecture-decision.md`](templates/architecture-decision.md) — one AD per subject; revisions update it in place |
|
|
@@ -82,7 +84,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
|
|
|
82
84
|
| Add to protected files | [`templates/protected-files.md`](templates/protected-files.md) |
|
|
83
85
|
| Create/update root README | [`templates/root-readme.md`](templates/root-readme.md) |
|
|
84
86
|
| Scaffold a standard doc (docs index, `structure.md`, CHANGELOG, backlog, memory) | [`templates/Readme.md`](templates/Readme.md) — the index names each scaffold file |
|
|
85
|
-
| Derive/update
|
|
87
|
+
| Derive/update an owning doc's `owns:` (feature, package, system) | [`references/frontmatter.md`](references/frontmatter.md) |
|
|
86
88
|
| Update existing doc | § Editing an existing doc (below) |
|
|
87
89
|
| Instruction file (`CLAUDE.md`, a rule, an agent, skill prose) | [`references/instruction-style.md`](references/instruction-style.md) — not these templates; every-load prose stays lean, and the present-tense rule still applies |
|
|
88
90
|
| Author or restructure a **skill** (fit, triggers, shape, anatomy, shipping) | The skill-authoring field guide — the `etna` repo (`nurixlabs/etna`), folder `docs/playbooks/skills/`, chapters `00`–`10`. **No etna checkout available ⇒ ask the user for access to the etna repo, or proceed from first principles** with [`references/instruction-style.md`](references/instruction-style.md) governing the prose. This row is the only place the playbook is referenced |
|
|
@@ -105,6 +107,8 @@ The two guarantees are complementary and both break on mixing: a reader holding
|
|
|
105
107
|
| Document | Nature | Name | Lifecycle |
|
|
106
108
|
| --- | --- | --- | --- |
|
|
107
109
|
| Feature (LLD) | Truth | `kebab-case.md` | Living; `status:` `draft` → `active` → `deprecated` |
|
|
110
|
+
| Package | Truth | `{package-folder}.md` | Living; `status:` as the feature doc |
|
|
111
|
+
| System | Truth | `{machine}.md` | Living; `status:` as the feature doc |
|
|
108
112
|
| Architecture Decision | Truth | `AD-NNN-kebab-case.md` | Living; `Proposed` → `Accepted` → `Deprecated`; revisions rewrite the body + append to its Decision Changelog |
|
|
109
113
|
| Steering docs (overview, domain model, glossary, non-goals, security, testing strategy) | Truth | fixed names | Living |
|
|
110
114
|
| API reference / environment registry | Truth | fixed names | Living |
|
|
@@ -125,8 +129,11 @@ doc, plus the genre's own keys beside it; a document without its frontmatter fai
|
|
|
125
129
|
contract and is not done. **There is no frontmatter-less genre.** A feature or UX doc carries the
|
|
126
130
|
full set — `owns:` (required), `module:` (optional), `keywords:` (required), `status:` (required),
|
|
127
131
|
`applies:` (the architecture docs it obeys; omitted when none), optional `last-reviewed:`. A
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
**package** doc (`docs/architecture/packages/`) and a **system** doc (`docs/architecture/systems/`)
|
|
133
|
+
carry the same set minus `module:` — a package is already a bounded unit and a machine spanning
|
|
134
|
+
packages belongs to no single context. A behavioral checklist (`docs/tests/`) carries
|
|
135
|
+
`type: checklist` and `verifies:` — the doc whose promises it proves. An acceptance-criteria doc
|
|
136
|
+
(`docs/acceptance/`) carries `type:` and `status:`.
|
|
130
137
|
|
|
131
138
|
**The frontmatter is where the docs tree's edges live**, and a doc that declares none is a node
|
|
132
139
|
nothing can reach: no change implicates it, so it rots without the rot being detectable. Deriving
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Frontmatter Derivation — the doc graph
|
|
2
2
|
|
|
3
|
-
Load when: creating a doc that declares an edge (a feature doc, a UX doc, a behavioral checklist), updating one whose scope moved, or running a baseline/reconcile pass over many docs.
|
|
3
|
+
Load when: creating a doc that declares an edge (a feature, package or system doc, a UX doc, a behavioral checklist), updating one whose scope moved, or running a baseline/reconcile pass over many docs.
|
|
4
4
|
|
|
5
5
|
**A docs tree is a graph, and every edge in it is declared in frontmatter.** Two kinds of edge exist: **doc → code** (`owns:`), and **doc → doc** (`verifies:`, `applies:`). The SessionStart hook reads them all and injects the generated indexes; there is no hand-maintained map of either kind.
|
|
6
6
|
|
|
@@ -14,13 +14,25 @@ The consuming end is also the end that **knows**. A checklist author knows what
|
|
|
14
14
|
|
|
15
15
|
## When to run
|
|
16
16
|
|
|
17
|
-
- **Baseline / reconcile** — for every
|
|
18
|
-
- **New
|
|
17
|
+
- **Baseline / reconcile** — for every owning doc whose code paths aren't yet declared in its `owns:` frontmatter (the `maintenance` agent (baseline mode) calls this)
|
|
18
|
+
- **New owning doc** — when creating one via the [Feature](../templates/feature.md), [Package](../templates/package.md) or [System](../templates/system.md) template, project its `owns:` globs
|
|
19
19
|
- **Doc update** — when the documented scope changes, update the doc's `owns:` globs
|
|
20
20
|
|
|
21
|
+
## Which genres own code
|
|
22
|
+
|
|
23
|
+
**Three do, and no others: `feature`, `package`, `system`.** They layer by glob specificity over the same tree, which is what lets a repo be documented at three altitudes without subtracting anything by hand:
|
|
24
|
+
|
|
25
|
+
| Genre | Claims | Wins because |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `package` (`docs/architecture/packages/`) | `packages/{name}/**` — the whole tree, one glob | Nothing; it is the **residual** owner and loses every narrower claim, keeping what no one else takes |
|
|
28
|
+
| `system` (`docs/architecture/systems/`) | the subtrees and exact files one machine is made of | Its globs are narrower than the package glob — specificity |
|
|
29
|
+
| `feature` (`docs/features/`) | one capability's or one surface's globs | Same — specificity over the package glob |
|
|
30
|
+
|
|
31
|
+
Every other genre owns no code and carries no `owns:`. That includes the cross-cutting doctrine in `docs/architecture/` (and `docs/architecture/common/`), which describes patterns rather than regions — a surface declares it is bound by one through `applies:`, and the doctrine never claims the surface back. Checklists, acceptance docs, ADs, operations procedures, research and brainstorm records own nothing.
|
|
32
|
+
|
|
21
33
|
## How to derive the frontmatter
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
An owning doc's ownership frontmatter is `owns:` (required), `module:` (optional — feature docs only; a package is already a bounded unit and a system spans contexts by definition), `keywords:` (required), and `status:` (required). An optional `last-reviewed:` date (stamped whenever the doc is substantively reviewed or updated) gives the `maintenance` agent a staleness signal to rank its audits by — it is tooling metadata, never narrated in the body. `type:` (required — the genre, enum = the template names) and `rev:` (generator-written generation stamp) are the same class: external tooling reads them, sessions **preserve them on edits** and never narrate or inject them. The `maintenance` agent stamps `type:` on docs that predate the key. `slug:` completes the set on generated docs: the identity key, minted once at creation and never re-derived from a display name — in the repo the filename is the slug, and the `maintenance` agent re-aligns the two when they diverge.
|
|
24
36
|
|
|
25
37
|
**1. `owns:`** (the path scope)
|
|
26
38
|
|
|
@@ -35,15 +47,17 @@ Prefer one region glob over many file globs. Ten paths say nothing about which i
|
|
|
35
47
|
| A feature's own directory | `src/feature/**` |
|
|
36
48
|
| A feature split across two directories | One glob each |
|
|
37
49
|
| A single entry-point file, and nothing beside it | The exact file path |
|
|
50
|
+
| A whole workspace package, residually | `packages/{name}/**` — one glob, on the package doc |
|
|
51
|
+
| A machine spanning packages | One glob or exact path per subtree it is made of, on the system doc |
|
|
38
52
|
| A shared file several features import | **Nothing — leave it unowned** |
|
|
39
53
|
|
|
40
54
|
**An unowned shared file is the system working, not a gap.** A shared module's authority is the contract it exposes, not any one consumer, so a library-references file or a common utility legitimately has no owning doc. Claiming it would make the index bigger and the routing less certain at once.
|
|
41
55
|
|
|
42
56
|
Glob each pattern against the codebase. At `status: active`, drop any that match nothing; at `status: draft`, projected globs may match nothing yet — confirm them against real code when the doc flips to `active`.
|
|
43
57
|
|
|
44
|
-
**2. `module:`**
|
|
58
|
+
**2. `module:`** (feature docs only)
|
|
45
59
|
|
|
46
|
-
The single bounded-context label this feature belongs to, or omit it for a cross-cutting feature that belongs to no single context. A module is a label, not a folder — features sharing a `module:` value are what ties that module to code (a filter over the generated ownership index).
|
|
60
|
+
The single bounded-context label this feature belongs to, or omit it for a cross-cutting feature that belongs to no single context. A module is a label, not a folder — features sharing a `module:` value are what ties that module to code (a filter over the generated ownership index). Package and system docs omit it entirely.
|
|
47
61
|
|
|
48
62
|
**3. `keywords:` & `status:`**
|
|
49
63
|
|
|
@@ -67,7 +81,7 @@ That edge has a property the others lack, and it is worth naming: **a `Pinned:`
|
|
|
67
81
|
|
|
68
82
|
## Rules
|
|
69
83
|
|
|
70
|
-
- One canonical doc per path. A doc's `owns:` globs must not overlap another doc's at equal specificity — the narrower glob wins; equal-specificity overlap is an ownership conflict to resolve.
|
|
84
|
+
- One canonical doc per path. A doc's `owns:` globs must not overlap another doc's at equal specificity — the narrower glob wins; equal-specificity overlap is an ownership conflict to resolve. Overlap at **unequal** specificity is the layering working as designed: a package doc's `packages/{name}/**` is meant to be outranked, and what survives every narrower claim is exactly what that doc describes.
|
|
71
85
|
- Use glob patterns for directories, explicit paths for individual files. At `active`, never list paths that don't exist in the codebase; a `draft`'s projected paths are confirmed at `active`.
|
|
72
86
|
- When a doc covers a deep feature with sub-specs (e.g., `editor/`), each sub-spec owns its own globs — don't duplicate a path across siblings.
|
|
73
87
|
- For baseline/reconcile with many docs, use parallel sub-agents to read source and propose `owns:` globs.
|
|
@@ -5,6 +5,8 @@ One file per template. Every template is either a **truth document** (states cur
|
|
|
5
5
|
| Template | File | Nature |
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| Feature (LLD) | [feature.md](feature.md) | Truth |
|
|
8
|
+
| Package (one workspace package) | [package.md](package.md) | Truth |
|
|
9
|
+
| System (one machine spanning packages) | [system.md](system.md) | Truth |
|
|
8
10
|
| Architecture Decision (AD) | [architecture-decision.md](architecture-decision.md) | Truth (Decision Changelog tail sanctioned) |
|
|
9
11
|
| System overview | [system-overview.md](system-overview.md) | Truth |
|
|
10
12
|
| Domain model | [domain-model.md](domain-model.md) | Truth |
|
|
@@ -35,4 +37,4 @@ One file per template. Every template is either a **truth document** (states cur
|
|
|
35
37
|
|
|
36
38
|
**Baseline scaffolds** — files that must exist after every baseline; the `maintenance` agent (baseline mode) creates any that are missing: [docs-readme.md](docs-readme.md), [structure.md](structure.md), [changelog-index.md](changelog-index.md), [backlog.md](backlog.md), [memory-main.md](memory-main.md), and [root-readme.md](root-readme.md) (never overwriting an existing root README).
|
|
37
39
|
|
|
38
|
-
**Ownership.**
|
|
40
|
+
**Ownership.** Three genres carry code — **feature**, **package**, **system** — and each is the canonical reference for the code paths in its **`owns:` frontmatter**, the only ownership key. To find what a doc owns, read its `owns:` globs; to find the doc for a path, match the path against the injected ownership index (the union of every owning doc's globs), narrower glob wins. That is what layers them: a package doc claims `packages/{name}/**` and is the tree's **residual** owner, while a system doc claims the narrower subtrees of one machine and a feature doc claims one surface — both win those paths by specificity, and the package doc keeps everything neither claims. Two docs may never claim a path at **equal** specificity. Every other genre here owns no code, including the cross-cutting doctrine in `docs/architecture/common/`. See [`../references/frontmatter.md`](../references/frontmatter.md) for turning code into `owns:` globs.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Package Documentation (docs/architecture/packages/{package}.md)
|
|
2
|
+
|
|
3
|
+
Truth document. Use for: **one workspace package — what it is and how it behaves**. One file per package, named for the package's folder (`packages/cli/` → `docs/architecture/packages/cli.md`). It states the package's own machinery: its manifest and published identity, its build and entry point, its source layout, and every behavior of its tree that no system or feature document claims.
|
|
4
|
+
|
|
5
|
+
**A package document is the residual owner of its own tree.** Its `owns:` is the whole package glob — `["packages/{name}/**"]` — and that glob is the *broadest* claim anything makes on those paths, so it loses to every narrower one. A system document naming `packages/{name}/src/server/runs/**` wins those files by specificity; a feature document naming `packages/{name}/src/surfaces/apps/**` wins those. What is left after every narrower claim is what this document describes, and it describes that exhaustively. **The residual claim is a promise of completeness, not of scope**: a package document with nothing left after the systems and features have claimed their subtrees is still owed — the manifest, the build, the entry, the layout, the test wiring and the deploy shape are always residual.
|
|
6
|
+
|
|
7
|
+
**Two rules bind the glob.** Neither genre may claim a path another document claims at **equal specificity** — equal-specificity overlap is an ownership conflict to resolve before either document ships, not a duplication to live with. And every glob **matches at least one file on disk** at `status: active`; a projected glob is permitted only at `draft`, and is confirmed when the document flips.
|
|
8
|
+
|
|
9
|
+
**How this differs from its neighbours.** A **feature** document covers one capability or one page a person names, and owns that surface's globs. A **system** document covers one machine that spans packages, and owns the subtrees that machine is made of. A **package** document covers one unit of distribution — a manifest, a build, a binary or a bundle — and owns what is left of it. The unit is the package boundary, so if the subject you are writing about does not stop at a `package.json`, it is not this genre. Route it: one screen → feature; one machine crossing two packages → [system](system.md).
|
|
10
|
+
|
|
11
|
+
**Required vs optional.** Required in every package document: the **frontmatter**, the **opening** (title, identity line, what the package is), the **ownership boundary** (what this document does not describe and which document does), at least one **behavior section**, **Files**, and **Related**. Everything else is optional — include a section only when the package has something real to put in it. A package with no test wiring of its own gets no verification section; a package that nothing else in the repo consumes says so in one line rather than growing a Consumers section. A short accurate document beats a full skeleton with empty headings.
|
|
12
|
+
|
|
13
|
+
Package documents run long because they are exhaustive over their residue — roughly 150 to 600 lines is the working range, and the top of that range is a package holding an application, not a document that has lost discipline.
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
---
|
|
17
|
+
owns: ["packages/{name}/**"] # required — the whole package tree, one glob; the residual claim
|
|
18
|
+
keywords: [package-name, binary, build, entry] # required — discovery terms: the npm name, the binary, the surfaces
|
|
19
|
+
status: active # required — active | draft | deprecated
|
|
20
|
+
applies: [docs/architecture/common/api.md] # optional — cross-cutting doctrine this package is BOUND by (obligation, not a bibliography); omit when none
|
|
21
|
+
type: package # required tooling key — genre stamp; preserve on edits, never narrate
|
|
22
|
+
---
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> A package document carries no `module:` — a package is already a bounded unit, and a second label
|
|
26
|
+
> for the same boundary is a thing to keep in sync for nothing. `last-reviewed:` is optional and,
|
|
27
|
+
> like `type:`, is tooling metadata: preserve it, never narrate it.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
# {package-path} — {the one-line role}
|
|
31
|
+
|
|
32
|
+
**Package:** `@scope/name` · **Ships as:** {a public npm package with one binary `x` / a private
|
|
33
|
+
source-first workspace package / a Worker on {domain}} · **Source:** `packages/{name}`
|
|
34
|
+
|
|
35
|
+
{One paragraph: what this package is and the job it does in the product. Name what it holds that
|
|
36
|
+
nothing else does, and what it deliberately does not hold. Written for someone who has not opened
|
|
37
|
+
the folder.}
|
|
38
|
+
|
|
39
|
+
{One paragraph: the ownership boundary. This document is the residual owner of `packages/{name}/**`;
|
|
40
|
+
these subtrees are claimed at higher specificity by these documents and are pointed at, not
|
|
41
|
+
restated. Where the routing is more than a sentence, make it the table below instead.}
|
|
42
|
+
|
|
43
|
+
{Optional, and worth its line whenever the body names paths: "Paths below are relative to
|
|
44
|
+
`packages/{name}/` unless they name another package."}
|
|
45
|
+
|
|
46
|
+
## What is built and what is not
|
|
47
|
+
|
|
48
|
+
{Optional — required in substance whenever some of the tree does not run. State plainly which
|
|
49
|
+
halves exist and which do not, with the source evidence for each claim, and say what a reader who
|
|
50
|
+
runs the unbuilt half will actually observe. A package whose subject as a whole does not run is
|
|
51
|
+
`status: draft` and says so here; a scoped gap in a working package stays `active` and takes an
|
|
52
|
+
UNBUILT marker at the section that describes the gap.}
|
|
53
|
+
|
|
54
|
+
## Ownership — packages/{name}
|
|
55
|
+
|
|
56
|
+
{Optional — use the table form when more than two documents claim subtrees of this package.}
|
|
57
|
+
|
|
58
|
+
**The package glob is the residual claim.** A system or feature document's `owns:` is more specific
|
|
59
|
+
and wins by specificity, so this document describes only what none of them claims, and points to
|
|
60
|
+
each for the rest.
|
|
61
|
+
|
|
62
|
+
| Path | What it is |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `src/index.ts` | {the residue: the entry, the dispatch, the layout} |
|
|
65
|
+
|
|
66
|
+
| Subtree | Stated and owned by |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `src/{machine}/**` | [{system}](../systems/{system}.md) |
|
|
69
|
+
| `src/surfaces/{x}/**` | [{feature}](../../features/{feature}.md) |
|
|
70
|
+
|
|
71
|
+
## {Behavior sections — named for this package's own machinery}
|
|
72
|
+
|
|
73
|
+
{The body. There is no fixed section list, and inventing one would be a lie: what a CLI owes
|
|
74
|
+
(commands, the store, the on-ramp) and what a UI package owes (the host port, the frame, the route
|
|
75
|
+
zones, the component pool) share no headings. Name each section for the thing it describes and
|
|
76
|
+
order them the way a reader meets them — identity and build first, then the surface, then what it
|
|
77
|
+
writes, then how it is verified. Prefer a table wherever the content is a roster (commands, routes,
|
|
78
|
+
exports, bindings, environment variables); prefer prose wherever the content is a rule and its
|
|
79
|
+
reason. State mechanisms and contracts, never pasted implementations.}
|
|
80
|
+
|
|
81
|
+
## Tests
|
|
82
|
+
|
|
83
|
+
{Optional. What proves this package, and what each suite pins — a `Suite | Pins` table, or the
|
|
84
|
+
commands a reader runs. A package whose verification is a build check names the check instead
|
|
85
|
+
(`## Checks`, `## Build and test`); a package with no suite of its own says so in one line in the
|
|
86
|
+
build section and carries no heading here.}
|
|
87
|
+
|
|
88
|
+
## Files
|
|
89
|
+
|
|
90
|
+
| Path | Role |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `packages/{name}/package.json` | {the manifest: the bin, the exports, the scripts} |
|
|
93
|
+
| `packages/{name}/src/index.ts` | {the entry} |
|
|
94
|
+
|
|
95
|
+
{One row or bullet per file this document owns, with what it holds — the roster a reader uses to go
|
|
96
|
+
from the document to the code. Close with the inverse where it helps: "Read here, owned elsewhere:"
|
|
97
|
+
naming the paths the body describes but another document claims.}
|
|
98
|
+
|
|
99
|
+
## Related
|
|
100
|
+
|
|
101
|
+
- [`{system}.md`](../systems/{system}.md) — {why a reader of this document would open that one}
|
|
102
|
+
- [`{other-package}.md`]({other-package}.md) — {the seam between the two}
|
|
103
|
+
- [`{doctrine}.md`](../common/{doctrine}.md) — {the cross-cutting contract this package answers to}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### The identity line
|
|
107
|
+
|
|
108
|
+
The bolded `·`-separated run under the title is what a reader checks first, and it answers three
|
|
109
|
+
questions no body section should have to: **what is this called** on the registry or in the
|
|
110
|
+
workspace, **how does it ship** (a public tarball, a private source-first package, a deployed
|
|
111
|
+
Worker, a signed installer), and **where is its source**. A package that ships two artifacts names
|
|
112
|
+
both. Add a `**Crate:**` or `**Deploys as:**` term where the package has one; drop any term that
|
|
113
|
+
would restate the title.
|
|
114
|
+
|
|
115
|
+
### Marking what is decided and unbuilt
|
|
116
|
+
|
|
117
|
+
Where the package holds code for a design that is decided and not built — or a surface whose other
|
|
118
|
+
half does not exist — wrap the prose in a marker so the gap is machine-findable and its evidence
|
|
119
|
+
travels with it:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
<!-- UNBUILT:START — {what is decided, where it is decided (an AD or an owner ruling), and the
|
|
123
|
+
source evidence that it is not built: the file that does not exist, the route that is not mounted,
|
|
124
|
+
the binding that is not declared} -->
|
|
125
|
+
{the prose describing the unbuilt half}
|
|
126
|
+
<!-- UNBUILT:END -->
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**A design that was decided *against* carries no marker.** A marker means the work is owed and
|
|
130
|
+
absent; using one for a rejected design turns the register of debt into a register of opinions.
|
|
131
|
+
Say in prose that it is not built and not owed, and name the decision that settled it.
|
|
@@ -18,8 +18,10 @@ Each topic has one canonical location. Read from here, not from plans or changel
|
|
|
18
18
|
|
|
19
19
|
| Topic | Authority | Key Files |
|
|
20
20
|
| ----- | --------- | --------- |
|
|
21
|
-
| Code-path ownership (path → doc + module) |
|
|
21
|
+
| Code-path ownership (path → doc + module) | `owns:` frontmatter on features, packages and systems | the generated ownership index |
|
|
22
22
|
| System overview | `docs/architecture/` | system-design.md |
|
|
23
|
+
| One workspace package | `docs/architecture/packages/` | {package-folder}.md |
|
|
24
|
+
| One machine spanning packages | `docs/architecture/systems/` | {machine}.md |
|
|
23
25
|
| Steering (domain model, glossary, non-goals) | `docs/architecture/` | domain-model.md, glossary.md, non-goals.md |
|
|
24
26
|
| Architecture decisions (ADs) | `docs/architecture/decisions/` | AD-NNN-*.md |
|
|
25
27
|
| API surface | `docs/architecture/` | api.md |
|
|
@@ -45,8 +47,12 @@ Every doc is a **truth document** (states current absolutes, topic-named, update
|
|
|
45
47
|
|
|
46
48
|
## Folder Rules
|
|
47
49
|
|
|
48
|
-
- `docs/features/`: Living reference for implemented features. Each feature doc **carries** the `owns:` / `module:` / `keywords:` / `status:` frontmatter — `owns:` declares the code-path globs it owns (the only ownership key), `module:` is an optional bounded-context label, `keywords:` are discovery terms, `status:` is `active`/`draft`/`deprecated`. The SessionStart hook unions every
|
|
49
|
-
- `docs/architecture/`: The truth layer for system-wide shape: overview, domain model, glossary, non-goals, API surface, security, and `decisions/` (ADs — one per subject, updated in place, journey in each AD's Decision Changelog). Update when cross-cutting reality changes.
|
|
50
|
+
- `docs/features/`: Living reference for implemented features. Each feature doc **carries** the `owns:` / `module:` / `keywords:` / `status:` frontmatter — `owns:` declares the code-path globs it owns (the only ownership key), `module:` is an optional bounded-context label, `keywords:` are discovery terms, `status:` is `active`/`draft`/`deprecated`. The SessionStart hook unions every owning doc's `owns:` globs into the injected ownership index.
|
|
51
|
+
- `docs/architecture/`: The truth layer for system-wide shape: overview, domain model, glossary, non-goals, API surface, security, and `decisions/` (ADs — one per subject, updated in place, journey in each AD's Decision Changelog). Update when cross-cutting reality changes. The cross-cutting doctrine directly under `architecture/` (and under `architecture/common/` where a repo separates it) **owns no code** — it describes patterns every surface obeys, and `applies:` is how a surface declares it is bound.
|
|
52
|
+
- `docs/architecture/packages/`: One doc per workspace package (`{package-folder}.md`), present in a workspace repo. Each is the **residual owner** of its tree — `owns: ["packages/{name}/**"]` — describing the manifest, build, entry, layout and every behavior no system or feature doc claims at higher specificity. Template: the `documentation` skill's `templates/package.md`.
|
|
53
|
+
- `docs/architecture/systems/`: One doc per machine that spans packages (`{machine}.md`) — the walk, the egress, the ingest. Its `owns:` names the machine's own subtrees and files, narrower than the package glob, so it wins those paths by specificity and the package doc keeps the rest. Not a surface doc: what a person sees stays in `docs/features/`. Template: the `documentation` skill's `templates/system.md`.
|
|
54
|
+
|
|
55
|
+
**Three genres carry `owns:`** — features, packages, systems — and no others. Their globs layer by specificity (package `**` residual, system and feature narrower), and two docs may never claim a path at **equal** specificity: that is an ownership conflict to resolve, not a duplication to live with. Every glob matches at least one file on disk at `status: active`.
|
|
50
56
|
- `docs/operations/`: How-to procedures for setup, development, and deployment, plus the environment registry. Update when workflows change. One-shot migrations do NOT live here — they are records (`docs/history/migrations/`).
|
|
51
57
|
- `docs/tests/`: Testing strategy and test docs. Present when the repo has a test posture worth documenting.
|
|
52
58
|
- `docs/history/plans/`: Records. Must have `Status:` field. Never modify after completion.
|