@nurix/etna 0.19.70 → 0.19.71

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": "6492994f8d44",
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.1",
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": "bc889c51aa02"
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
+ }
@@ -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.9",
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": "cebc16bda479"
12
12
  }
@@ -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.8",
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": "e69567759f7b"
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.70",
3
+ "version": "0.19.71",
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": "6492994f8d44"
46
46
  }