@nurix/etna 0.19.54 → 0.19.55

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": "70b47e452344",
2
+ "fingerprint": "79c4b33f5911",
3
3
  "harnesses": {
4
4
  "apollo": {
5
5
  "plugin": "apollo",
@@ -15,5 +15,5 @@
15
15
  "docs/memory/main.md",
16
16
  ".mcp.json"
17
17
  ],
18
- "digest": "e86ea39dc854"
18
+ "digest": "6c5d5cfe5a12"
19
19
  }
@@ -134,6 +134,9 @@ export function scanFeatureDocs(projectDir) {
134
134
 
135
135
  // Build the ownership index text from every feature doc's frontmatter.
136
136
  export function buildIndex(projectDir) {
137
+ // Total markdown under docs/features/, counted independently of scanFeatureDocs — which
138
+ // drops a doc carrying no owns:, making a parse failure and a doc that owns nothing look alike.
139
+ const total = listMarkdown(path.join(projectDir, "docs", "features")).length;
137
140
  const docs = scanFeatureDocs(projectDir);
138
141
  const lines = [];
139
142
  let deprecated = 0;
@@ -152,7 +155,14 @@ export function buildIndex(projectDir) {
152
155
  const header =
153
156
  "# Ownership index (generated) — code-path glob → owning feature doc · module · keywords · status\n" +
154
157
  "# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
155
- const footer = deprecated > 0 ? `\n# (${deprecated} deprecated feature doc(s) omitted)` : "";
158
+ const indexed = docs.length - deprecated;
159
+ const skipped = total - docs.length;
160
+ const notes = [];
161
+ if (deprecated > 0) notes.push(`${deprecated} deprecated`);
162
+ if (skipped > 0) notes.push(`${skipped} with no owns:`);
163
+ const footer = notes.length
164
+ ? `\n# (${indexed}/${total} feature doc(s) indexed - ${notes.join(", ")})`
165
+ : "";
156
166
  return header + lines.join("\n") + footer;
157
167
  }
158
168
 
@@ -10,5 +10,5 @@
10
10
  "seeds": [
11
11
  ".mcp.json"
12
12
  ],
13
- "digest": "cc0eb9871a83"
13
+ "digest": "6057016c0f2e"
14
14
  }
@@ -134,6 +134,9 @@ export function scanFeatureDocs(projectDir) {
134
134
 
135
135
  // Build the ownership index text from every feature doc's frontmatter.
136
136
  export function buildIndex(projectDir) {
137
+ // Total markdown under docs/features/, counted independently of scanFeatureDocs — which
138
+ // drops a doc carrying no owns:, making a parse failure and a doc that owns nothing look alike.
139
+ const total = listMarkdown(path.join(projectDir, "docs", "features")).length;
137
140
  const docs = scanFeatureDocs(projectDir);
138
141
  const lines = [];
139
142
  let deprecated = 0;
@@ -152,7 +155,14 @@ export function buildIndex(projectDir) {
152
155
  const header =
153
156
  "# Ownership index (generated) — code-path glob → owning feature doc · module · keywords · status\n" +
154
157
  "# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
155
- const footer = deprecated > 0 ? `\n# (${deprecated} deprecated feature doc(s) omitted)` : "";
158
+ const indexed = docs.length - deprecated;
159
+ const skipped = total - docs.length;
160
+ const notes = [];
161
+ if (deprecated > 0) notes.push(`${deprecated} deprecated`);
162
+ if (skipped > 0) notes.push(`${skipped} with no owns:`);
163
+ const footer = notes.length
164
+ ? `\n# (${indexed}/${total} feature doc(s) indexed - ${notes.join(", ")})`
165
+ : "";
156
166
  return header + lines.join("\n") + footer;
157
167
  }
158
168
 
@@ -8,5 +8,5 @@
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "dd6fdce1e2aa"
11
+ "digest": "65f7f0f01864"
12
12
  }
@@ -1,4 +1,4 @@
1
- // harness/code/hooks/generate-ownership-index.mjs — SessionStart hook: scan docs/features/**/*.md,
1
+ // harness/delivery/hooks/generate-ownership-index.mjs — SessionStart hook: scan docs/features/**/*.md,
2
2
  // extract the focal frontmatter (owns / module / keywords / status), and inject a compact
3
3
  // code-path → owning-doc index via additionalContext. Fires on startup/resume/clear/compact so the
4
4
  // map survives /clear and /compact and is regenerated fresh every time. Writes no file.
@@ -50,6 +50,26 @@ function unquote(s) {
50
50
 
51
51
  // Read one frontmatter key: scalar (`k: v`), inline array (`k: [a, b]`), or a following `- item` block.
52
52
  // Returns a string for scalars, an array for lists, or undefined when the key is absent.
53
+ // One YAML list/array item -> clean value: a quoted item ends at its closing quote
54
+ // (anything after - trailing comments - drops); an unquoted item ends at ` #`.
55
+ function cleanItem(s) {
56
+ const t = String(s).trim();
57
+ const q = t[0];
58
+ if (q === '"' || q === "'") {
59
+ const end = t.indexOf(q, 1);
60
+ if (end > 0) return t.slice(1, end).trim();
61
+ }
62
+ const hash = t.search(/\s#/);
63
+ return unquote(hash >= 0 ? t.slice(0, hash) : t);
64
+ }
65
+
66
+ // `[a, b] # comment` -> items; content ends at the closing bracket, not the line end.
67
+ function parseInlineArray(raw) {
68
+ const end = raw.indexOf("]");
69
+ const body = (end >= 0 ? raw.slice(0, end) : raw).replace(/^\[/, "");
70
+ return body.split(",").map((s) => cleanItem(s)).filter(Boolean);
71
+ }
72
+
53
73
  function field(block, key) {
54
74
  const lines = block.split(/\r?\n/);
55
75
  const head = new RegExp(`^${key}\\s*:\\s*(.*)$`);
@@ -58,16 +78,21 @@ function field(block, key) {
58
78
  if (!m) continue;
59
79
  const raw = m[1].trim();
60
80
  if (raw === "") {
81
+ // A bare key may continue as an indented inline array on the next line, or a `- item` block.
82
+ const next = (lines[i + 1] || "").trim();
83
+ if (next.startsWith("[")) {
84
+ return parseInlineArray(next);
85
+ }
61
86
  const items = [];
62
87
  for (let j = i + 1; j < lines.length; j++) {
63
88
  const li = /^\s*-\s+(.*)$/.exec(lines[j]);
64
89
  if (!li) break;
65
- items.push(unquote(li[1]));
90
+ items.push(cleanItem(li[1]));
66
91
  }
67
92
  return items;
68
93
  }
69
94
  if (raw.startsWith("[")) {
70
- return raw.replace(/^\[|\]$/g, "").split(",").map((s) => unquote(s)).filter(Boolean);
95
+ return parseInlineArray(raw);
71
96
  }
72
97
  return unquote(raw);
73
98
  }
@@ -86,6 +111,7 @@ function buildIndex(projectDir) {
86
111
  const files = listMarkdown(featuresDir);
87
112
  const lines = [];
88
113
  let deprecated = 0;
114
+ let indexed = 0;
89
115
  for (const file of files) {
90
116
  let text;
91
117
  try {
@@ -104,6 +130,7 @@ function buildIndex(projectDir) {
104
130
  }
105
131
  const moduleLabel = (field(block, "module") || "—").toString();
106
132
  const keywords = asList(field(block, "keywords"));
133
+ indexed += 1;
107
134
  const doc = path.relative(projectDir, file).split(path.sep).join("/");
108
135
  const kw = keywords.length ? `[${keywords.join(", ")}]` : "[]";
109
136
  for (const glob of owns) {
@@ -115,7 +142,15 @@ function buildIndex(projectDir) {
115
142
  const header =
116
143
  "# Ownership index (generated) — code-path glob → owning feature doc · module · keywords · status\n" +
117
144
  "# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
118
- const footer = deprecated > 0 ? `\n# (${deprecated} deprecated feature doc(s) omitted)` : "";
145
+ // A doc dropped by a parse failure is indistinguishable from one that owns nothing,
146
+ // so report the tally: seen vs indexed is the only signal a silent skip leaves.
147
+ const skipped = files.length - indexed - deprecated;
148
+ const notes = [];
149
+ if (deprecated > 0) notes.push(`${deprecated} deprecated`);
150
+ if (skipped > 0) notes.push(`${skipped} with no owns:`);
151
+ const footer = notes.length
152
+ ? `\n# (${indexed}/${files.length} feature doc(s) indexed - ${notes.join(", ")})`
153
+ : "";
119
154
  return header + lines.join("\n") + footer;
120
155
  }
121
156
 
@@ -8,5 +8,5 @@
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "12d918b0c33e"
11
+ "digest": "83b3fa101c57"
12
12
  }
@@ -1,4 +1,4 @@
1
- // plugins/code/hooks/generate-ownership-index.mjs — SessionStart hook: scan docs/features/**/*.md,
1
+ // harness/etna/hooks/generate-ownership-index.mjs — SessionStart hook: scan docs/features/**/*.md,
2
2
  // extract the focal frontmatter (owns / module / keywords / status), and inject a compact
3
3
  // code-path → owning-doc index via additionalContext. Fires on startup/resume/clear/compact so the
4
4
  // map survives /clear and /compact and is regenerated fresh every time. Writes no file.
@@ -50,6 +50,26 @@ function unquote(s) {
50
50
 
51
51
  // Read one frontmatter key: scalar (`k: v`), inline array (`k: [a, b]`), or a following `- item` block.
52
52
  // Returns a string for scalars, an array for lists, or undefined when the key is absent.
53
+ // One YAML list/array item -> clean value: a quoted item ends at its closing quote
54
+ // (anything after - trailing comments - drops); an unquoted item ends at ` #`.
55
+ function cleanItem(s) {
56
+ const t = String(s).trim();
57
+ const q = t[0];
58
+ if (q === '"' || q === "'") {
59
+ const end = t.indexOf(q, 1);
60
+ if (end > 0) return t.slice(1, end).trim();
61
+ }
62
+ const hash = t.search(/\s#/);
63
+ return unquote(hash >= 0 ? t.slice(0, hash) : t);
64
+ }
65
+
66
+ // `[a, b] # comment` -> items; content ends at the closing bracket, not the line end.
67
+ function parseInlineArray(raw) {
68
+ const end = raw.indexOf("]");
69
+ const body = (end >= 0 ? raw.slice(0, end) : raw).replace(/^\[/, "");
70
+ return body.split(",").map((s) => cleanItem(s)).filter(Boolean);
71
+ }
72
+
53
73
  function field(block, key) {
54
74
  const lines = block.split(/\r?\n/);
55
75
  const head = new RegExp(`^${key}\\s*:\\s*(.*)$`);
@@ -58,16 +78,21 @@ function field(block, key) {
58
78
  if (!m) continue;
59
79
  const raw = m[1].trim();
60
80
  if (raw === "") {
81
+ // A bare key may continue as an indented inline array on the next line, or a `- item` block.
82
+ const next = (lines[i + 1] || "").trim();
83
+ if (next.startsWith("[")) {
84
+ return parseInlineArray(next);
85
+ }
61
86
  const items = [];
62
87
  for (let j = i + 1; j < lines.length; j++) {
63
88
  const li = /^\s*-\s+(.*)$/.exec(lines[j]);
64
89
  if (!li) break;
65
- items.push(unquote(li[1]));
90
+ items.push(cleanItem(li[1]));
66
91
  }
67
92
  return items;
68
93
  }
69
94
  if (raw.startsWith("[")) {
70
- return raw.replace(/^\[|\]$/g, "").split(",").map((s) => unquote(s)).filter(Boolean);
95
+ return parseInlineArray(raw);
71
96
  }
72
97
  return unquote(raw);
73
98
  }
@@ -86,6 +111,7 @@ function buildIndex(projectDir) {
86
111
  const files = listMarkdown(featuresDir);
87
112
  const lines = [];
88
113
  let deprecated = 0;
114
+ let indexed = 0;
89
115
  for (const file of files) {
90
116
  let text;
91
117
  try {
@@ -104,6 +130,7 @@ function buildIndex(projectDir) {
104
130
  }
105
131
  const moduleLabel = (field(block, "module") || "—").toString();
106
132
  const keywords = asList(field(block, "keywords"));
133
+ indexed += 1;
107
134
  const doc = path.relative(projectDir, file).split(path.sep).join("/");
108
135
  const kw = keywords.length ? `[${keywords.join(", ")}]` : "[]";
109
136
  for (const glob of owns) {
@@ -115,7 +142,15 @@ function buildIndex(projectDir) {
115
142
  const header =
116
143
  "# Ownership index (generated) — code-path glob → owning feature doc · module · keywords · status\n" +
117
144
  "# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
118
- const footer = deprecated > 0 ? `\n# (${deprecated} deprecated feature doc(s) omitted)` : "";
145
+ // A doc dropped by a parse failure is indistinguishable from one that owns nothing,
146
+ // so report the tally: seen vs indexed is the only signal a silent skip leaves.
147
+ const skipped = files.length - indexed - deprecated;
148
+ const notes = [];
149
+ if (deprecated > 0) notes.push(`${deprecated} deprecated`);
150
+ if (skipped > 0) notes.push(`${skipped} with no owns:`);
151
+ const footer = notes.length
152
+ ? `\n# (${indexed}/${files.length} feature doc(s) indexed - ${notes.join(", ")})`
153
+ : "";
119
154
  return header + lines.join("\n") + footer;
120
155
  }
121
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.54",
3
+ "version": "0.19.55",
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": "70b47e452344"
45
+ "etnaFingerprint": "79c4b33f5911"
46
46
  }