@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.
Files changed (25) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +2 -2
  3. package/data/harnesses/code/hooks/awareness.mjs +7 -4
  4. package/data/harnesses/code/hooks/lib/ownership.mjs +51 -19
  5. package/data/harnesses/code/skills/documentation/SKILL.md +10 -3
  6. package/data/harnesses/code/skills/documentation/references/frontmatter.md +21 -7
  7. package/data/harnesses/code/skills/documentation/templates/Readme.md +3 -1
  8. package/data/harnesses/code/skills/documentation/templates/package.md +131 -0
  9. package/data/harnesses/code/skills/documentation/templates/structure.md +9 -3
  10. package/data/harnesses/code/skills/documentation/templates/system.md +143 -0
  11. package/data/harnesses/delivery/harness.json +2 -2
  12. package/data/harnesses/delivery/skills/documentation/SKILL.md +10 -3
  13. package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +21 -7
  14. package/data/harnesses/delivery/skills/documentation/templates/Readme.md +3 -1
  15. package/data/harnesses/delivery/skills/documentation/templates/package.md +131 -0
  16. package/data/harnesses/delivery/skills/documentation/templates/structure.md +9 -3
  17. package/data/harnesses/delivery/skills/documentation/templates/system.md +143 -0
  18. package/data/harnesses/etna/harness.json +2 -2
  19. package/data/harnesses/etna/skills/documentation/SKILL.md +10 -3
  20. package/data/harnesses/etna/skills/documentation/references/frontmatter.md +21 -7
  21. package/data/harnesses/etna/skills/documentation/templates/Readme.md +3 -1
  22. package/data/harnesses/etna/skills/documentation/templates/package.md +131 -0
  23. package/data/harnesses/etna/skills/documentation/templates/structure.md +9 -3
  24. package/data/harnesses/etna/skills/documentation/templates/system.md +143 -0
  25. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "7d1e7785bd0c",
2
+ "fingerprint": "0fa183cce9ee",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.14.7",
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": "80ccc743454e"
28
+ "digest": "5de54f6b1a71"
29
29
  }
@@ -13,7 +13,8 @@ import {
13
13
  resolveProjectDir,
14
14
  buildIndex,
15
15
  buildEdgeIndex,
16
- scanFeatureDocs,
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 = scanFeatureDocs(projectDir);
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 a feature doc's frontmatter can orphan globs the same way a rename can.
538
- if (rel && rel.startsWith(`${root}/features/`)) detectRenames(projectDir, "git mv", meta, rows);
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/features/ and the source tree; no network, no deps.
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. `scanFeatureDocs` skips such docs, so the failure is invisible:
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
- // Scan docs/features/**/*.md and return one entry per feature doc that declares owns:.
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
- export function scanFeatureDocs(projectDir) {
120
- const featuresDir = path.join(projectDir, docsRoot(projectDir), "features");
121
- // *.bkp.* files are install-time backups awaiting the maintenance fold — a backed-up feature
122
- // doc still carries owns: frontmatter, and scanning it would double-own every path it names.
123
- const files = listMarkdown(featuresDir).filter((f) => !f.includes(".bkp."));
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
- // Total markdown under docs/features/, counted independently of scanFeatureDocs — which
150
- // drops a doc carrying no owns:, making a parse failure and a doc that owns nothing look alike.
151
- const total = listMarkdown(path.join(projectDir, docsRoot(projectDir), "features")).length;
152
- const docs = scanFeatureDocs(projectDir);
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 feature doc · module · status, then the code regions it governs.\n" +
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} feature doc(s) indexed - ${notes.join(", ")})`
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
- // Feature docs owning the given repo-relative path (deprecated docs excluded).
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 || scanFeatureDocs(projectDir)) {
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 feature docs that match zero files on disk.
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 || scanFeatureDocs(projectDir)) {
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 || scanFeatureDocs(projectDir)) {
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 a feature doc's `owns:` | [`references/frontmatter.md`](references/frontmatter.md) |
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
- behavioral checklist (`docs/tests/`) carries `type: checklist` and `verifies:` — the doc whose
129
- promises it proves. An acceptance-criteria doc (`docs/acceptance/`) carries `type:` and `status:`.
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 feature doc whose code paths aren't yet declared in its `owns:` frontmatter (the `maintenance` agent (baseline mode) calls this)
18
- - **New feature doc** — when creating a doc via the [Feature template](../templates/feature.md), project its `owns:` globs
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
- A feature doc's ownership frontmatter is `owns:` (required), `module:` (optional), `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.
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.** A feature doc 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 feature's `owns:` globs), narrower glob wins. See [`../references/frontmatter.md`](../references/frontmatter.md) for turning code into `owns:` globs.
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) | feature `owns:` frontmatter | the generated ownership index |
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 feature's `owns:` globs into the injected ownership index.
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.