@nurix/etna 0.19.70 → 0.19.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "c39e1157ed11",
2
+ "fingerprint": "25655144dce0",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -59,7 +59,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
59
59
  For each `owns:` glob across the feature docs:
60
60
 
61
61
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
62
- - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
62
+ - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
63
63
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
64
64
 
65
65
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
@@ -57,7 +57,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
57
57
  For each `owns:` glob across the feature docs:
58
58
 
59
59
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
60
- - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
60
+ - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
61
61
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
62
62
 
63
63
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.8.4",
4
+ "version": "3.9.3",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -15,5 +15,5 @@
15
15
  "docs/memory/main.md",
16
16
  ".mcp.json"
17
17
  ],
18
- "digest": "cfd0df4274c4"
18
+ "digest": "9aba7c8ce3db"
19
19
  }
@@ -1,22 +1,57 @@
1
- // harness/code/hooks/check-ownership.mjs — CI backstop: every owns: glob must match at least one
2
- // file, unless an unexpired `waived:` dead-glob row in docs/operations/bookkeeping.md covers it
3
- // (the waiver names the glob verbatim). Exit 1 on any unwaived dead glob.
1
+ // harness/code/hooks/check-ownership.mjs — CI backstop for docs/features/ ownership. Two checks,
2
+ // one walk of the tree:
3
+ //
4
+ // 1. dead-glob — every owns: glob matches at least one file
5
+ // 2. ownership-overlap — no two feature docs claim a common file at EQUAL specificity, which
6
+ // leaves "most specific wins" unable to pick an owner
7
+ //
8
+ // Either is silenced by an unexpired `waived:` row in docs/operations/bookkeeping.md naming its
9
+ // kind plus the offending glob (dead-glob) or the shared path (ownership-overlap) verbatim.
10
+ // Exit 1 on anything unwaived.
11
+ //
12
+ // A glob at strictly HIGHER specificity carving a sub-path out of a broader `**` is the designed
13
+ // layering, not a conflict — only an exact tie is reported.
4
14
 
5
- import { deadGlobs } from "./lib/ownership.mjs";
15
+ import { deadGlobs, listRepoFiles, overlaps } from "./lib/ownership.mjs";
6
16
  import { readQueue, unexpiredWaivers } from "./lib/state.mjs";
7
17
 
8
18
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
9
19
  const today = new Date().toISOString().slice(0, 10);
10
20
 
11
- const dead = deadGlobs(projectDir);
12
- const waivers = unexpiredWaivers(readQueue(projectDir), "dead-glob", today);
13
- const unwaived = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
21
+ const files = listRepoFiles(projectDir);
22
+ const queue = readQueue(projectDir);
14
23
 
15
- if (unwaived.length === 0) {
16
- console.log(`check-ownership: OK (${dead.length} dead glob(s), all waived or none)`);
24
+ const dead = deadGlobs(projectDir, files);
25
+ const deadWaivers = unexpiredWaivers(queue, "dead-glob", today);
26
+ const unwaivedDead = dead.filter((d) => !deadWaivers.some((w) => w.body.includes(d.glob)));
27
+
28
+ const conflicts = overlaps(projectDir, files);
29
+ const overlapWaivers = unexpiredWaivers(queue, "ownership-overlap", today);
30
+ const unwaivedOverlaps = conflicts.filter((c) => !overlapWaivers.some((w) => w.body.includes(c.example)));
31
+
32
+ if (unwaivedDead.length === 0 && unwaivedOverlaps.length === 0) {
33
+ console.log(
34
+ `check-ownership: OK (${dead.length} dead glob(s), ${conflicts.length} equal-specificity overlap(s), all waived or none)`,
35
+ );
17
36
  process.exit(0);
18
37
  }
19
- console.error(`check-ownership: ${unwaived.length} unwaived dead owns: glob(s):`);
20
- for (const d of unwaived) console.error(` ${d.glob} → ${d.doc}`);
21
- console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row with reason + expires=.");
38
+
39
+ if (unwaivedDead.length > 0) {
40
+ console.error(`check-ownership: ${unwaivedDead.length} unwaived dead owns: glob(s):`);
41
+ for (const d of unwaivedDead) console.error(` ${d.glob} → ${d.doc}`);
42
+ console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row with reason + expires=.");
43
+ }
44
+
45
+ if (unwaivedOverlaps.length > 0) {
46
+ console.error(`check-ownership: ${unwaivedOverlaps.length} unwaived equal-specificity overlap(s):`);
47
+ for (const c of unwaivedOverlaps) {
48
+ console.error(` ${c.example}`);
49
+ console.error(` ${c.a.glob} → ${c.a.doc}`);
50
+ console.error(` ${c.b.glob} → ${c.b.doc}`);
51
+ }
52
+ console.error(
53
+ "Narrow one doc's glob so a single owner wins, or add a `waived:` ownership-overlap row naming the shared path with reason + expires=.",
54
+ );
55
+ }
56
+
22
57
  process.exit(1);
@@ -242,3 +242,60 @@ export function deadGlobs(projectDir, repoFiles) {
242
242
  }
243
243
  return dead;
244
244
  }
245
+
246
+ // Specificity of an owns: glob as a (literal, wildcard) segment tuple. The ownership rule is
247
+ // "most specific wins", so a glob at strictly higher specificity legitimately carves a sub-path
248
+ // out of a broader `**` — that is the designed layering, never a conflict. Only an exact tie
249
+ // leaves the owner ambiguous, which is what `overlaps()` reports.
250
+ export function specificity(glob) {
251
+ let literal = 0;
252
+ let wildcard = 0;
253
+ for (const segment of String(glob).split("/")) {
254
+ if (segment === "") continue;
255
+ if (segment.includes("*") || segment.includes("?")) wildcard++;
256
+ else literal++;
257
+ }
258
+ return { literal, wildcard };
259
+ }
260
+
261
+ // Two globs are equally specific when both tuple components match.
262
+ export function sameSpecificity(a, b) {
263
+ return a.literal === b.literal && a.wildcard === b.wildcard;
264
+ }
265
+
266
+ // Cross-doc equal-specificity ownership conflicts: two globs from DIFFERENT feature docs that
267
+ // match a common file at the same specificity, so "most specific wins" cannot pick an owner and
268
+ // the path is doubly owned. Returns [{ a: {doc, glob}, b: {doc, glob}, example }].
269
+ //
270
+ // This is the mechanical half of a rule the harness has always stated and never checked — the
271
+ // `documentation` skill's frontmatter reference calls an equal-specificity overlap "an ownership
272
+ // conflict to resolve", and the `maintenance` agent is told to flag one. Without this, both had
273
+ // to be enforced by eye.
274
+ export function overlaps(projectDir, repoFiles) {
275
+ const files = repoFiles || listRepoFiles(projectDir);
276
+ const owners = [];
277
+ for (const entry of scanFeatureDocs(projectDir)) {
278
+ if (entry.status === "deprecated") continue;
279
+ for (const glob of entry.owns) {
280
+ const re = globToRegExp(glob);
281
+ owners.push({ doc: entry.doc, glob, spec: specificity(glob), files: files.filter((f) => re.test(f)) });
282
+ }
283
+ }
284
+ return pairOverlaps(owners);
285
+ }
286
+
287
+ // The pure core of `overlaps()`, split out so tests can feed synthetic owners without a repo tree.
288
+ export function pairOverlaps(owners) {
289
+ const found = [];
290
+ for (let i = 0; i < owners.length; i++) {
291
+ for (let j = i + 1; j < owners.length; j++) {
292
+ const a = owners[i];
293
+ const b = owners[j];
294
+ if (a.doc === b.doc) continue;
295
+ if (!sameSpecificity(a.spec, b.spec)) continue;
296
+ const example = a.files.find((f) => b.files.includes(f));
297
+ if (example) found.push({ a: { doc: a.doc, glob: a.glob }, b: { doc: b.doc, glob: b.glob }, example });
298
+ }
299
+ }
300
+ return found;
301
+ }
@@ -18,6 +18,8 @@ Required: **Conventions**, at least one numbered behavior section, **Failure Rep
18
18
  - **State each section's trace.** `Pinned:` names the suite test that now guards it, or says `driven only` **with the reason** — an unmarked section is indistinguishable from an unexamined one.
19
19
  - **State each section's `AC:` keys.** Where the repo carries a keyed acceptance-criteria source, each section names the keys it proves, so the chain from criterion to checklist to suite test survives the last hop; the gate reports coverage and backlogs uncovered keys. `none — not AC-mapped` is the honest line where no keyed source exists.
20
20
  - **A known-failing assertion stays written as the contract.** A checklist states what the feature promises; the *run* records the failure and the backlog carries the defect. Never soften an assertion to make a run go green.
21
+ - **Each driven section carries a dated `Driven` line** — `Driven {YYYY-MM-DD} · PASS|FAIL ({environment}).` plus the evidence the assertion turned on. It is the only thing separating a promise someone verified from a promise someone typed, and its date is what makes staleness visible: a section still carrying last quarter's date next to a surface rewritten since is the file telling you it was not re-driven. `FAIL` is stamped and kept, with the backlog item it raised; a section blocked rather than driven says `NOT DRIVEN — {what blocked it}`.
22
+ - **The file is maintained by whoever drives it.** Nothing else watches it — a checklist owns no code, so no ownership glob implicates it and no hook fires when the surface underneath it changes. Retiring dead sections and repairing traces is part of running the gate, not a separate cleanup someone schedules.
21
23
 
22
24
  ```
23
25
  # {Feature} Test Plan
@@ -44,7 +46,8 @@ Each step is an action; each **Assert** is the observable it must produce.
44
46
  - [ ] **Assert**: {the observable, with exact literals}
45
47
  - [ ] **Assert**: {the persistence claim, where the behavior writes something}
46
48
 
47
- > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}`
49
+ > Driven {YYYY-MM-DD} · PASS ({environment}). {the evidence — the literal observed, the row that moved}
50
+ > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}` — or `none yet — {the test this section owes}`
48
51
  > AC: {the acceptance-criteria keys this section proves} — or `none — not AC-mapped`
49
52
 
50
53
  ### 2. {The next behavior}
@@ -151,6 +151,8 @@ When writing or discharging a ledger row, name the stratum, not just the altitud
151
151
  Runs only when the user triggers the verification gate (constraints — preconditions, verdict-inline, tier, honesty — are `.claude/rules/testing.md` § The gate). **Drive the real thing first, then pin what must not regress**: driving finds the defects that matter to a user, and shows which behaviors are worth the permanent cost of a test.
152
152
 
153
153
  1. **Write the checklist** — derive or update `docs/tests/{feature}.md` from the ledger and the LLD's promises (the `documentation` skill's `templates/behavioral-checklist.md`; one `**Assert**` per observable). Derived before the driving it is the script; derived after, it is merely a report with nothing to trace tests back to.
154
+
155
+ **The gate owns these files, not just their first draft.** Whoever drives a checklist maintains it in the same pass — nothing else in the repo is watching, because a checklist owns no code and no `owns:` glob implicates it, so a surface can be rewritten under a checklist and every hook stays silent. Before driving, reconcile the file against the surface you are about to drive: **retire** sections whose behavior no longer exists (delete them — a checklist is a truth document, and the history lives in git), **add** sections for behavior shipped since the last run, and **repair every `Pinned:` trace**, because a trace naming a test that no longer exists is worse than no trace — it reports coverage that cannot fail. A section you cannot pin says `none yet` and names the test it owes. **Never soften an assertion to make the run green**: the checklist states what the feature promises, the run records what happened, and the backlog carries the difference.
154
156
  2. **Drive the real surface** — browser automation for web UI (the `/test` runner skill where the repo wires it), the actual CLI binary, HTTP against the running worker. **A failing checklist assertion is never softened to make a run green** — record the defect, send it to the backlog. Where the runtime offers no browser automation, emit the browser-verification checklist for the user to run; their report is the rung's verdict and `Status: Complete` waits for it.
155
157
  3. **Pin the durable behaviors** as suite tests per `testing.md`, naming each test's stratum (§ Bridge). **A pinned test must pass from a cold shell** — `pnpm test` with no dev server hand-started, no seeded state assumed beyond what the suite builds for itself. A test whose oracle is "fetch the server I happen to have running" is a driven check wearing a test's name: it goes red the moment the session ends (on Workers, `@cloudflare/vitest-pool-workers` is the self-contained harness; an Express app is driven via `supertest`, never a port). Before discharging, run the suite once from a clean shell — a suite that needs the session's environment discharges nothing. A test is a permanent maintenance cost, bought deliberately:
156
158
 
@@ -163,4 +165,8 @@ Runs only when the user triggers the verification gate (constraints — precondi
163
165
  A driven check is evidence, not an artifact — it proves the change works today and leaves nothing behind; that is why this rung exists.
164
166
  4. **Run the affected suite**, plus the project's e2e smoke gate where one is wired.
165
167
  5. **Discharge every ledger row into exactly one outcome**: **satisfied** (test green, or driven check passed), **owes nothing** (with the reason — compiler-guarded, covered elsewhere), or **blocked** (missing infrastructure named, row converted to a backlog item). **Open by re-reading the ledger from the plan file on disk, never from conversation memory** — a context-held copy is how rows get missed. **Write each row's outcome into its `Outcome` cell in the plan file** — exactly `satisfied`, `owes nothing — {reason}`, or `blocked → backlog` — because the Stop wall and CI read that cell, and a row left unexamined blocks completion.
166
- 6. **Report results honestly, including failures**; record which test now guards each checklist section (`Pinned:` traces) and each section's `AC:` line where the repo carries a keyed acceptance-criteria source, closing with the coverage count — `AC coverage: {n}/{m}; uncovered: {keys} → backlog`. Completion steps — freezing the plan, the changelog entry — are the root policy file's, not yours.
168
+ 6. **Report results honestly, including failures**; record which test now guards each checklist section (`Pinned:` traces) and each section's `AC:` line where the repo carries a keyed acceptance-criteria source, closing with the coverage count — `AC coverage: {n}/{m}; uncovered: {keys} → backlog`.
169
+
170
+ **Stamp every section you drove, in the file, before reporting.** One line per section: `Driven {YYYY-MM-DD} · PASS|FAIL ({environment}).` followed by the evidence — the literal observed, the row version, the request body, whatever the assertion turned on. A section left unstamped since an earlier run keeps that run's older date, which is the signal that it was not re-driven. **A `FAIL` is stamped `FAIL` and stays** — with the backlog item it raised — because a corpus of driven records that has never recorded a failure is either describing a flawless product or being written by someone who already knew the answer, and a reader cannot tell which. Where the driving was blocked rather than failed, say `NOT DRIVEN — {what blocked it}`; an unexamined section and a passing one must never look alike.
171
+
172
+ Completion steps — freezing the plan, the changelog entry — are the root policy file's, not yours.
@@ -59,7 +59,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
59
59
  For each `owns:` glob across the feature docs:
60
60
 
61
61
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
62
- - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
62
+ - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
63
63
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
64
64
 
65
65
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
@@ -57,7 +57,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
57
57
  For each `owns:` glob across the feature docs:
58
58
 
59
59
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
60
- - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
60
+ - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
61
61
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
62
62
 
63
63
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.8",
4
+ "version": "0.2.11",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "bfd105b1ff3c"
11
+ "digest": "ea0919582c83"
12
12
  }
@@ -18,6 +18,8 @@ Required: **Conventions**, at least one numbered behavior section, **Failure Rep
18
18
  - **State each section's trace.** `Pinned:` names the suite test that now guards it, or says `driven only` **with the reason** — an unmarked section is indistinguishable from an unexamined one.
19
19
  - **State each section's `AC:` keys.** Where the repo carries a keyed acceptance-criteria source, each section names the keys it proves, so the chain from criterion to checklist to suite test survives the last hop; the gate reports coverage and backlogs uncovered keys. `none — not AC-mapped` is the honest line where no keyed source exists.
20
20
  - **A known-failing assertion stays written as the contract.** A checklist states what the feature promises; the *run* records the failure and the backlog carries the defect. Never soften an assertion to make a run go green.
21
+ - **Each driven section carries a dated `Driven` line** — `Driven {YYYY-MM-DD} · PASS|FAIL ({environment}).` plus the evidence the assertion turned on. It is the only thing separating a promise someone verified from a promise someone typed, and its date is what makes staleness visible: a section still carrying last quarter's date next to a surface rewritten since is the file telling you it was not re-driven. `FAIL` is stamped and kept, with the backlog item it raised; a section blocked rather than driven says `NOT DRIVEN — {what blocked it}`.
22
+ - **The file is maintained by whoever drives it.** Nothing else watches it — a checklist owns no code, so no ownership glob implicates it and no hook fires when the surface underneath it changes. Retiring dead sections and repairing traces is part of running the gate, not a separate cleanup someone schedules.
21
23
 
22
24
  ```
23
25
  # {Feature} Test Plan
@@ -44,7 +46,8 @@ Each step is an action; each **Assert** is the observable it must produce.
44
46
  - [ ] **Assert**: {the observable, with exact literals}
45
47
  - [ ] **Assert**: {the persistence claim, where the behavior writes something}
46
48
 
47
- > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}`
49
+ > Driven {YYYY-MM-DD} · PASS ({environment}). {the evidence — the literal observed, the row that moved}
50
+ > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}` — or `none yet — {the test this section owes}`
48
51
  > AC: {the acceptance-criteria keys this section proves} — or `none — not AC-mapped`
49
52
 
50
53
  ### 2. {The next behavior}
@@ -59,7 +59,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
59
59
  For each `owns:` glob across the feature docs:
60
60
 
61
61
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
62
- - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
62
+ - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
63
63
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
64
64
 
65
65
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
@@ -57,7 +57,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
57
57
  For each `owns:` glob across the feature docs:
58
58
 
59
59
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
60
- - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
60
+ - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
61
61
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
62
62
 
63
63
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.4.7",
4
+ "version": "1.4.10",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "593183c2c83c"
11
+ "digest": "a3ff16dda3c2"
12
12
  }
@@ -18,6 +18,8 @@ Required: **Conventions**, at least one numbered behavior section, **Failure Rep
18
18
  - **State each section's trace.** `Pinned:` names the suite test that now guards it, or says `driven only` **with the reason** — an unmarked section is indistinguishable from an unexamined one.
19
19
  - **State each section's `AC:` keys.** Where the repo carries a keyed acceptance-criteria source, each section names the keys it proves, so the chain from criterion to checklist to suite test survives the last hop; the gate reports coverage and backlogs uncovered keys. `none — not AC-mapped` is the honest line where no keyed source exists.
20
20
  - **A known-failing assertion stays written as the contract.** A checklist states what the feature promises; the *run* records the failure and the backlog carries the defect. Never soften an assertion to make a run go green.
21
+ - **Each driven section carries a dated `Driven` line** — `Driven {YYYY-MM-DD} · PASS|FAIL ({environment}).` plus the evidence the assertion turned on. It is the only thing separating a promise someone verified from a promise someone typed, and its date is what makes staleness visible: a section still carrying last quarter's date next to a surface rewritten since is the file telling you it was not re-driven. `FAIL` is stamped and kept, with the backlog item it raised; a section blocked rather than driven says `NOT DRIVEN — {what blocked it}`.
22
+ - **The file is maintained by whoever drives it.** Nothing else watches it — a checklist owns no code, so no ownership glob implicates it and no hook fires when the surface underneath it changes. Retiring dead sections and repairing traces is part of running the gate, not a separate cleanup someone schedules.
21
23
 
22
24
  ```
23
25
  # {Feature} Test Plan
@@ -44,7 +46,8 @@ Each step is an action; each **Assert** is the observable it must produce.
44
46
  - [ ] **Assert**: {the observable, with exact literals}
45
47
  - [ ] **Assert**: {the persistence claim, where the behavior writes something}
46
48
 
47
- > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}`
49
+ > Driven {YYYY-MM-DD} · PASS ({environment}). {the evidence — the literal observed, the row that moved}
50
+ > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}` — or `none yet — {the test this section owes}`
48
51
  > AC: {the acceptance-criteria keys this section proves} — or `none — not AC-mapped`
49
52
 
50
53
  ### 2. {The next behavior}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.70",
3
+ "version": "0.19.72",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "c39e1157ed11"
45
+ "etnaFingerprint": "25655144dce0"
46
46
  }