@nurix/etna 0.18.18 → 0.18.20

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 (100) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +2 -2
  3. package/data/harnesses/code/rules/general.md +2 -1
  4. package/data/harnesses/code/skills/design/SKILL.md +65 -0
  5. package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
  6. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
  7. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
  8. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
  9. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
  10. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
  11. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
  12. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
  13. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
  14. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
  15. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
  16. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
  17. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
  18. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
  19. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
  20. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
  21. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
  22. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
  23. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
  24. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
  25. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
  26. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
  27. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
  28. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
  29. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
  30. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
  31. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
  32. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
  33. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
  34. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
  35. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
  36. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
  37. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
  38. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
  39. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
  40. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
  41. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
  42. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
  43. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
  44. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
  45. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
  46. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
  47. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
  48. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
  49. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
  50. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
  51. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
  52. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
  53. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
  54. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
  55. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
  56. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
  57. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
  58. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
  59. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
  60. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
  61. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
  62. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
  63. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
  64. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
  65. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
  66. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
  67. package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
  68. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
  69. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
  70. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
  71. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
  72. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
  73. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
  74. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
  75. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
  76. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
  77. package/data/harnesses/code/skills/design/references/create.md +126 -0
  78. package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
  79. package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
  80. package/data/harnesses/code/skills/design/references/improve.md +85 -0
  81. package/data/harnesses/code/skills/design/references/interaction.md +77 -0
  82. package/data/harnesses/code/skills/design/references/introspect.md +100 -0
  83. package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
  84. package/data/harnesses/code/skills/design/references/personality.md +504 -0
  85. package/data/harnesses/code/skills/design/references/structure.md +138 -0
  86. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
  87. package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
  88. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
  89. package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
  90. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
  91. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
  92. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
  93. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
  94. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
  95. package/data/harnesses/etna/CLAUDE.md +1 -1
  96. package/data/harnesses/etna/harness.json +2 -2
  97. package/data/harnesses/etna/rules/general.md +2 -1
  98. package/data/harnesses/mobile/harness.json +2 -2
  99. package/data/harnesses/mobile/rules/general.md +2 -1
  100. package/package.json +1 -1
@@ -0,0 +1,432 @@
1
+ // skills/design/scripts/self-integrity.mjs — the design skill's own CI check: anchor parity, catalog invariants, stated-count and fixture-pair validation
2
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
3
+ import { join, dirname, basename } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
8
+ const SKILL_ROOT = join(SCRIPTS_DIR, "..");
9
+
10
+ const CATALOG_CSV = join(SKILL_ROOT, "data/catalog/rules.csv");
11
+ const FIXTURES_DIR = join(SKILL_ROOT, "data/catalog/fixtures");
12
+ const HARD_RULES_MD = join(SKILL_ROOT, "references/hard-rules.md");
13
+ const DETECTOR_MJS = join(SCRIPTS_DIR, "detector.mjs");
14
+
15
+ const CSV_HEADER = ["id", "dimension", "statement", "numbers", "grade", "fire_points", "detector", "scope_note", "provenance", "fixture_pair"];
16
+ const VALID_DIMENSIONS = new Set(["P1", "P2", "P3", "P4", "P5", "P6", "P7", "S1", "S2", "S3", "S4", "B1", "B2", "B3", "B4", "B5"]);
17
+ const ANCHOR_PREFIXES = "typ|col|spc|dep|int|mot|sta|cod|proc|inst|trust";
18
+ const ANCHOR_TOKEN = new RegExp("`(" + ANCHOR_PREFIXES + ")-(\\d{2})`", "g");
19
+ const ANCHOR_ID = "(?:" + ANCHOR_PREFIXES + ")-\\d{2}";
20
+
21
+ const PINNED = { rows: 131, source: 66, render: 10, archetypes: 6, primitives: 26, colorTokens: 34, semanticTokens: 17, substrateTokens: 17 };
22
+
23
+ // Read a UTF-8 file, returning null when it does not exist.
24
+ function readSafe(path) {
25
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
26
+ }
27
+
28
+ // Parse RFC4180-style CSV text into an array of field arrays (quotes + doubled-quote escapes honored).
29
+ function parseCsv(text) {
30
+ const rows = [];
31
+ let field = "";
32
+ let record = [];
33
+ let inQuotes = false;
34
+ for (let i = 0; i < text.length; i++) {
35
+ const ch = text[i];
36
+ if (inQuotes) {
37
+ if (ch === '"') {
38
+ if (text[i + 1] === '"') { field += '"'; i++; }
39
+ else inQuotes = false;
40
+ } else field += ch;
41
+ } else if (ch === '"') inQuotes = true;
42
+ else if (ch === ",") { record.push(field); field = ""; }
43
+ else if (ch === "\n") { record.push(field); rows.push(record); record = []; field = ""; }
44
+ else if (ch === "\r") { /* skip */ }
45
+ else field += ch;
46
+ }
47
+ if (field.length > 0 || record.length > 0) { record.push(field); rows.push(record); }
48
+ return rows;
49
+ }
50
+
51
+ // Split a rule id into its {prefix, num} parts, or null when it is not a well-formed anchor id.
52
+ function splitId(id) {
53
+ const m = /^([a-z]+)-(\d{2})$/.exec(id);
54
+ return m ? { prefix: m[1], num: parseInt(m[2], 10) } : null;
55
+ }
56
+
57
+ // Strip leading whitespace and a single markdown list marker from a line.
58
+ function stripListMarker(line) {
59
+ return line.replace(/^\s*/, "").replace(/^[-*+]\s+/, "");
60
+ }
61
+
62
+ // Load and index the rule catalog into typed row objects plus derived counts.
63
+ function loadCatalog() {
64
+ const text = readSafe(CATALOG_CSV);
65
+ if (text === null) throw new Error("rules.csv not found at " + CATALOG_CSV);
66
+ const raw = parseCsv(text).filter((r) => r.length > 1 || (r.length === 1 && r[0].trim() !== ""));
67
+ const header = raw[0];
68
+ const dataRows = raw.slice(1).map((cells) => {
69
+ const obj = {};
70
+ CSV_HEADER.forEach((k, idx) => { obj[k] = cells[idx] !== undefined ? cells[idx] : ""; });
71
+ obj._cells = cells;
72
+ return obj;
73
+ });
74
+ return { header, rows: dataRows };
75
+ }
76
+
77
+ // ── Check 1: rules.csv is well-formed ──────────────────────────────────────────
78
+ function checkCsvWellFormed(catalog) {
79
+ const errors = [];
80
+ const { header, rows } = catalog;
81
+
82
+ if (header.length !== CSV_HEADER.length || CSV_HEADER.some((h, i) => header[i] !== h))
83
+ errors.push(`header mismatch: expected [${CSV_HEADER.join(",")}] got [${header.join(",")}]`);
84
+
85
+ const seen = new Map();
86
+ const byPrefix = new Map();
87
+ for (const row of rows) {
88
+ if (row._cells.length !== CSV_HEADER.length)
89
+ errors.push(`${row.id || "?"}: has ${row._cells.length} columns, expected ${CSV_HEADER.length}`);
90
+
91
+ const parts = splitId(row.id);
92
+ if (!parts) { errors.push(`malformed id "${row.id}" (want <prefix>-NN)`); }
93
+ else {
94
+ if (seen.has(row.id)) errors.push(`duplicate id ${row.id}`);
95
+ seen.set(row.id, true);
96
+ if (!byPrefix.has(parts.prefix)) byPrefix.set(parts.prefix, []);
97
+ byPrefix.get(parts.prefix).push(parts.num);
98
+ }
99
+
100
+ if (!VALID_DIMENSIONS.has(row.dimension))
101
+ errors.push(`${row.id}: invalid dimension "${row.dimension}"`);
102
+
103
+ // grade/detector/fixture_pair invariants
104
+ if (row.detector === "source" && row.fixture_pair !== "yes")
105
+ errors.push(`${row.id}: detector=source must have fixture_pair=yes (got "${row.fixture_pair}")`);
106
+ if (row.grade === "judged" && !(row.detector === "none" || row.detector === "render"))
107
+ errors.push(`${row.id}: grade=judged must have detector none|render (got "${row.detector}")`);
108
+ if (row.grade === "procedure" && row.detector !== "procedure")
109
+ errors.push(`${row.id}: grade=procedure must have detector=procedure (got "${row.detector}")`);
110
+ if (!["machine", "judged", "procedure"].includes(row.grade))
111
+ errors.push(`${row.id}: invalid grade "${row.grade}"`);
112
+ if (!["source", "render", "procedure", "none"].includes(row.detector))
113
+ errors.push(`${row.id}: invalid detector "${row.detector}"`);
114
+ if (!["yes", "na"].includes(row.fixture_pair))
115
+ errors.push(`${row.id}: invalid fixture_pair "${row.fixture_pair}"`);
116
+ }
117
+
118
+ // Contiguous numbering per prefix: 1..N with no gaps, no duplicates.
119
+ for (const [prefix, nums] of byPrefix) {
120
+ const sorted = [...nums].sort((a, b) => a - b);
121
+ for (let i = 0; i < sorted.length; i++) {
122
+ if (sorted[i] !== i + 1) {
123
+ errors.push(`${prefix}-* numbering not contiguous: expected ${i + 1} at position ${i}, got ${sorted[i]} (full: ${sorted.join(",")})`);
124
+ break;
125
+ }
126
+ }
127
+ }
128
+
129
+ return { name: "rules.csv well-formed", pass: errors.length === 0, detail: errors.length ? `${errors.length} issue(s)` : `${rows.length} rows, ${byPrefix.size} prefixes`, errors };
130
+ }
131
+
132
+ // ── Check 2: anchor parity between rules.csv and hard-rules.md ──────────────────
133
+ function checkAnchorParity(catalog) {
134
+ const errors = [];
135
+ const md = readSafe(HARD_RULES_MD);
136
+ if (md === null) return { name: "anchor parity (prose<->catalog)", pass: false, detail: "hard-rules.md missing", errors: ["hard-rules.md not found at " + HARD_RULES_MD] };
137
+
138
+ const lines = md.split("\n");
139
+ const defCounts = new Map(); // id -> { main, judged }
140
+ const rangeCovered = new Set();
141
+ const allBacktickIds = new Set();
142
+ const judgedHeader = /^##\s+judged dimensions/i;
143
+ let inJudged = false;
144
+
145
+ const bump = (id, section) => {
146
+ if (!defCounts.has(id)) defCounts.set(id, { main: 0, judged: 0 });
147
+ defCounts.get(id)[section]++;
148
+ };
149
+
150
+ for (const line of lines) {
151
+ if (judgedHeader.test(line.trim())) inJudged = true;
152
+
153
+ let m;
154
+ ANCHOR_TOKEN.lastIndex = 0;
155
+ while ((m = ANCHOR_TOKEN.exec(line)) !== null) allBacktickIds.add(`${m[1]}-${m[2]}`);
156
+
157
+ const body = stripListMarker(line);
158
+ const rangeMatch = new RegExp("^`(" + ANCHOR_ID + ")`\\s*[\\u2013\\u2014-]\\s*`(" + ANCHOR_ID + ")`").exec(body);
159
+ const defMatch = new RegExp("^`(" + ANCHOR_ID + ")`").exec(body);
160
+
161
+ if (rangeMatch) {
162
+ const a = splitId(rangeMatch[1]);
163
+ const b = splitId(rangeMatch[2]);
164
+ if (a && b && a.prefix === b.prefix) for (let n = a.num; n <= b.num; n++) rangeCovered.add(`${a.prefix}-${String(n).padStart(2, "0")}`);
165
+ }
166
+ if (defMatch) bump(defMatch[1], inJudged ? "judged" : "main");
167
+ }
168
+
169
+ const csvIds = new Set(catalog.rows.map((r) => r.id));
170
+ const proseCovered = new Set([...rangeCovered, ...defCounts.keys()]);
171
+
172
+ // No prose anchor without a csv row.
173
+ for (const id of allBacktickIds)
174
+ if (!csvIds.has(id)) errors.push(`prose anchor ${id} has no rules.csv row`);
175
+
176
+ for (const row of catalog.rows) {
177
+ const c = defCounts.get(row.id) || { main: 0, judged: 0 };
178
+ const total = c.main + c.judged;
179
+ const isInstTrust = row.id.startsWith("inst-") || row.id.startsWith("trust-");
180
+
181
+ if (row.grade === "machine" || row.grade === "procedure") {
182
+ if (c.main === 1 && c.judged === 0) { /* ok */ }
183
+ else if (total === 0) errors.push(`${row.grade} rule ${row.id} has no backticked definition line in hard-rules.md`);
184
+ else if (total > 1) errors.push(`${row.id} defined ${total} times (want exactly 1)`);
185
+ else if (c.judged > 0) errors.push(`${row.grade} rule ${row.id} is defined inside the Judged dimensions section`);
186
+ } else if (row.grade === "judged" && isInstTrust) {
187
+ if (!rangeCovered.has(row.id) && total === 0)
188
+ errors.push(`judged range rule ${row.id} not covered by a range pointer line`);
189
+ } else if (row.grade === "judged") {
190
+ if (c.judged === 1 && c.main === 0) { /* ok */ }
191
+ else if (total === 0) errors.push(`judged rule ${row.id} missing from the Judged dimensions index`);
192
+ else if (c.main > 0) errors.push(`judged rule ${row.id} defined outside the Judged dimensions section`);
193
+ else if (c.judged > 1) errors.push(`judged rule ${row.id} indexed ${c.judged} times (want exactly 1)`);
194
+ }
195
+
196
+ if (!proseCovered.has(row.id)) errors.push(`rules.csv row ${row.id} has no prose coverage`);
197
+ }
198
+
199
+ // The two collapsed range pointers must be present and complete.
200
+ for (let n = 1; n <= 11; n++) if (!rangeCovered.has(`inst-${String(n).padStart(2, "0")}`)) errors.push(`inst-${String(n).padStart(2, "0")} not covered by the inst range pointer`);
201
+ for (let n = 1; n <= 4; n++) if (!rangeCovered.has(`trust-0${n}`)) errors.push(`trust-0${n} not covered by the trust range pointer`);
202
+
203
+ const defTotal = [...defCounts.keys()].length;
204
+ return { name: "anchor parity (prose<->catalog)", pass: errors.length === 0, detail: errors.length ? `${errors.length} parity break(s)` : `${defTotal} definitions, ${rangeCovered.size} range-covered, all ${catalog.rows.length} rows covered`, errors };
205
+ }
206
+
207
+ // ── Check 3: every fixture_pair=yes rule ships a flag + pass fixture ────────────
208
+ function checkFixturePresence(catalog) {
209
+ const errors = [];
210
+ const files = existsSync(FIXTURES_DIR) ? readdirSync(FIXTURES_DIR) : null;
211
+ if (files === null) {
212
+ const needed = catalog.rows.filter((r) => r.fixture_pair === "yes");
213
+ return { name: "fixture-pair presence", pass: false, detail: `fixtures dir missing; ${needed.length} pairs needed`, errors: ["data/catalog/fixtures/ does not exist"] };
214
+ }
215
+ const present = new Set(files);
216
+ const hasKind = (id, kind) => [...present].some((f) => f.startsWith(`${id}.${kind}.`));
217
+ let ok = 0;
218
+ for (const row of catalog.rows) {
219
+ if (row.fixture_pair !== "yes") continue;
220
+ const hasFlag = hasKind(row.id, "flag");
221
+ const hasPass = hasKind(row.id, "pass");
222
+ if (!hasFlag) errors.push(`${row.id}: missing ${row.id}.flag.*`);
223
+ if (!hasPass) errors.push(`${row.id}: missing ${row.id}.pass.*`);
224
+ if (hasFlag && hasPass) ok++;
225
+ }
226
+ const total = catalog.rows.filter((r) => r.fixture_pair === "yes").length;
227
+ return { name: "fixture-pair presence", pass: errors.length === 0, detail: `${ok}/${total} pairs present`, errors };
228
+ }
229
+
230
+ // Run detector.mjs on one fixture file and return the set of anchor ids it reports.
231
+ function runDetector(file) {
232
+ // Scan the leading comment block for a `// detector-args:` directive (the mandatory file-path
233
+ // comment occupies line 1, so the directive cannot be line 0). Relative paths in it (e.g. a
234
+ // --manifest sibling) resolve against FIXTURES_DIR, matching each fixture's documented verify command.
235
+ let extra = [];
236
+ for (const line of (readSafe(file) || "").split("\n")) {
237
+ const t = line.trim();
238
+ if (t !== "" && !t.startsWith("//")) break;
239
+ const argMatch = /^\/\/\s*detector-args:\s*(.+)$/.exec(t);
240
+ if (argMatch) { extra = argMatch[1].trim().split(/\s+/); break; }
241
+ }
242
+ const res = spawnSync("node", [DETECTOR_MJS, file, "--json", ...extra], { encoding: "utf8", timeout: 30000, cwd: FIXTURES_DIR });
243
+ const out = `${res.stdout || ""}\n${res.stderr || ""}`;
244
+ const ids = new Set();
245
+ try {
246
+ const parsed = JSON.parse(res.stdout || "null");
247
+ const collect = (v) => {
248
+ if (v == null) return;
249
+ if (typeof v === "string") { const mm = /^(?:typ|col|spc|dep|int|mot|sta|cod|proc|inst|trust)-\d{2}$/.exec(v); if (mm) ids.add(v); return; }
250
+ if (Array.isArray(v)) { v.forEach(collect); return; }
251
+ if (typeof v === "object") Object.values(v).forEach(collect);
252
+ };
253
+ collect(parsed);
254
+ } catch { /* fall back to scanning raw output below */ }
255
+ let m;
256
+ ANCHOR_TOKEN.lastIndex = 0;
257
+ const bare = new RegExp("\\b(" + ANCHOR_PREFIXES + ")-(\\d{2})\\b", "g");
258
+ while ((m = bare.exec(out)) !== null) ids.add(`${m[1]}-${m[2]}`);
259
+ return { ids, spawnError: res.error ? String(res.error) : null };
260
+ }
261
+
262
+ // ── Check 4: contrastive validity — flag fires the rule, pass does not ──────────
263
+ function checkContrastiveValidity(catalog) {
264
+ const errors = [];
265
+ if (!existsSync(DETECTOR_MJS)) return { name: "contrastive fixture validity", pass: false, detail: "detector.mjs missing", errors: ["scripts/detector.mjs not found — cannot exercise fixtures"] };
266
+ if (!existsSync(FIXTURES_DIR)) return { name: "contrastive fixture validity", pass: false, detail: "fixtures dir missing", errors: ["data/catalog/fixtures/ does not exist"] };
267
+ const files = readdirSync(FIXTURES_DIR);
268
+ const present = new Set(files);
269
+ const findFile = (id, kind) => files.find((f) => f.startsWith(`${id}.${kind}.`));
270
+ let checked = 0;
271
+ for (const row of catalog.rows) {
272
+ if (row.fixture_pair !== "yes") continue;
273
+ const flag = findFile(row.id, "flag");
274
+ const pass = findFile(row.id, "pass");
275
+ if (!flag || !pass) continue; // absence is check 3's job
276
+ checked++;
277
+ const flagRes = runDetector(join(FIXTURES_DIR, flag));
278
+ if (flagRes.spawnError) { errors.push(`${row.id}: detector spawn failed on flag (${flagRes.spawnError})`); continue; }
279
+ if (!flagRes.ids.has(row.id)) errors.push(`${row.id}: flag fixture did not report ${row.id} (reported: ${[...flagRes.ids].join(",") || "none"})`);
280
+ const passRes = runDetector(join(FIXTURES_DIR, pass));
281
+ if (passRes.spawnError) { errors.push(`${row.id}: detector spawn failed on pass (${passRes.spawnError})`); continue; }
282
+ if (passRes.ids.has(row.id)) errors.push(`${row.id}: pass fixture wrongly reported ${row.id}`);
283
+ }
284
+ return { name: "contrastive fixture validity", pass: errors.length === 0, detail: `${checked} pair(s) exercised`, errors };
285
+ }
286
+
287
+ const NUMBER_WORDS = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, nineteen: 19, twenty: 20, "twenty-six": 26 };
288
+
289
+ // Resolve a numeric token (digits or an English number word) to a number, or NaN.
290
+ function toNumber(tok) {
291
+ if (/^\d+$/.test(tok)) return parseInt(tok, 10);
292
+ return NUMBER_WORDS[tok.toLowerCase()] ?? NaN;
293
+ }
294
+
295
+ // ── Check 5: stated counts in prose match the ground truth ─────────────────────
296
+ function checkStatedCounts(catalog, ground) {
297
+ const errors = [];
298
+ // Ground truth drift guard: derived CSV counts must match the pinned numbers.
299
+ if (ground.rows !== PINNED.rows) errors.push(`derived row count ${ground.rows} != pinned ${PINNED.rows}`);
300
+ if (ground.source !== PINNED.source) errors.push(`derived source count ${ground.source} != pinned ${PINNED.source}`);
301
+ if (ground.render !== PINNED.render) errors.push(`derived machine-render count ${ground.render} != pinned ${PINNED.render}`);
302
+
303
+ const gtMap = {
304
+ rules: ground.rows, rule: ground.rows, rows: ground.rows, row: ground.rows,
305
+ source: ground.source, render: ground.render,
306
+ archetypes: PINNED.archetypes, archetype: PINNED.archetypes,
307
+ primitives: PINNED.primitives, primitive: PINNED.primitives,
308
+ "color-tokens": PINNED.colorTokens, "color tokens": PINNED.colorTokens, colortokens: PINNED.colorTokens,
309
+ "semantic-tokens": PINNED.semanticTokens, "substrate-tokens": PINNED.substrateTokens,
310
+ };
311
+
312
+ const targets = [join(SKILL_ROOT, "SKILL.md"), ...(existsSync(join(SKILL_ROOT, "references")) ? readdirSync(join(SKILL_ROOT, "references")).filter((f) => f.endsWith(".md")).map((f) => join(SKILL_ROOT, "references", f)) : [])];
313
+ let commentClaims = 0;
314
+ let bareClaims = 0;
315
+ for (const file of targets) {
316
+ const text = readSafe(file);
317
+ if (text === null) continue;
318
+ const rel = file.replace(SKILL_ROOT + "/", "");
319
+ const linesArr = text.split("\n");
320
+ linesArr.forEach((line, i) => {
321
+ // (a) machine-readable HTML comment claims: <!-- count:<name>=<n> -->
322
+ const cmt = /<!--\s*count:([a-z0-9 _-]+)=(\d+)\s*-->/gi;
323
+ let cm;
324
+ while ((cm = cmt.exec(line)) !== null) {
325
+ commentClaims++;
326
+ const name = cm[1].trim().toLowerCase().replace(/\s+/g, "-");
327
+ const val = parseInt(cm[2], 10);
328
+ const expected = gtMap[name] ?? gtMap[name.replace(/-/g, " ")];
329
+ if (expected === undefined) errors.push(`${rel}:${i + 1}: count comment names unknown quantity "${cm[1].trim()}"`);
330
+ else if (expected !== val) errors.push(`${rel}:${i + 1}: count:${cm[1].trim()}=${val} but ground truth is ${expected}`);
331
+ }
332
+ // (b) bare claims of the exact named forms. Require adjacency (number immediately
333
+ // before the noun) so qualified subset phrases ("24 static primitives", "one layout
334
+ // primitive") are not misread as grand-total claims.
335
+ const bare = /\b(\d+|[a-z]+)\s+(rules?|archetypes?|primitives?)\b/gi;
336
+ let bm;
337
+ while ((bm = bare.exec(line)) !== null) {
338
+ const n = toNumber(bm[1]);
339
+ if (Number.isNaN(n)) continue;
340
+ const key = bm[2].toLowerCase();
341
+ const expected = gtMap[key];
342
+ if (expected === undefined) continue;
343
+ bareClaims++;
344
+ if (n !== expected) errors.push(`${rel}:${i + 1}: bare claim "${bm[0].trim()}" (=${n}) contradicts ground truth ${expected}`);
345
+ }
346
+ });
347
+ }
348
+ return { name: "stated-count integrity", pass: errors.length === 0, detail: `${commentClaims} comment + ${bareClaims} bare claim(s) checked`, errors };
349
+ }
350
+
351
+ // ── Check 6: every brief-pinned path exists ────────────────────────────────────
352
+ function checkFileTree() {
353
+ const errors = [];
354
+ const files = [
355
+ "SKILL.md",
356
+ "references/hard-rules.md", "references/create.md", "references/improve.md", "references/introspect.md",
357
+ "references/diagnosis.md", "references/structure.md", "references/personality.md", "references/interaction.md", "references/lifecycle.md",
358
+ "data/catalog/rules.csv",
359
+ "data/design-md/SCHEMA.md", "data/design-md/nurix.md",
360
+ "data/operator-pack/surface-catalog.md", "data/operator-pack/fixtures.md", "data/operator-pack/trust-rubric.md",
361
+ "data/operator-pack/exemplars/list-detail.tsx", "data/operator-pack/exemplars/dashboard.tsx",
362
+ "data/operator-pack/exemplars/entity-detail.tsx", "data/operator-pack/exemplars/create-edit.tsx",
363
+ "data/operator-pack/exemplars/log-stream.tsx", "data/operator-pack/exemplars/command-center.tsx",
364
+ "scripts/detector.mjs", "scripts/phase-gate.mjs", "scripts/preship-sweep.mjs", "scripts/render-rung.mjs",
365
+ "scripts/scaffold.mjs", "scripts/derive-personality.mjs", "scripts/generate-fixtures.mjs",
366
+ "scripts/self-integrity.mjs", "scripts/hook-config.json",
367
+ ];
368
+ for (const rel of files) if (!existsSync(join(SKILL_ROOT, rel))) errors.push(`missing ${rel}`);
369
+
370
+ const dirs = ["data/catalog/fixtures", "data/design-md/library"];
371
+ for (const rel of dirs) {
372
+ const abs = join(SKILL_ROOT, rel);
373
+ if (!existsSync(abs)) errors.push(`missing dir ${rel}/`);
374
+ else if (readdirSync(abs).length === 0) errors.push(`empty dir ${rel}/`);
375
+ }
376
+ const found = files.length - errors.filter((e) => e.startsWith("missing ")).length;
377
+ return { name: "file-tree presence", pass: errors.length === 0, detail: `${found}/${files.length} files present`, errors };
378
+ }
379
+
380
+ // Derive the ground-truth counts from the catalog itself.
381
+ function deriveGround(catalog) {
382
+ return {
383
+ rows: catalog.rows.length,
384
+ source: catalog.rows.filter((r) => r.detector === "source").length,
385
+ render: catalog.rows.filter((r) => r.detector === "render" && r.grade === "machine").length,
386
+ };
387
+ }
388
+
389
+ // Run every check, print the summary table, and exit non-zero on any failure.
390
+ function main() {
391
+ const results = [];
392
+ let catalog = null;
393
+ try { catalog = loadCatalog(); } catch (e) {
394
+ console.error("FATAL: " + e.message);
395
+ process.exit(2);
396
+ }
397
+ const ground = deriveGround(catalog);
398
+
399
+ const runners = [
400
+ () => checkCsvWellFormed(catalog),
401
+ () => checkAnchorParity(catalog),
402
+ () => checkFixturePresence(catalog),
403
+ () => checkContrastiveValidity(catalog),
404
+ () => checkStatedCounts(catalog, ground),
405
+ () => checkFileTree(),
406
+ ];
407
+ for (const run of runners) {
408
+ try { results.push(run()); }
409
+ catch (e) { results.push({ name: run.name || "check", pass: false, detail: "threw", errors: [e.stack || String(e)] }); }
410
+ }
411
+
412
+ for (const r of results) {
413
+ if (r.errors && r.errors.length) {
414
+ console.log(`\n[${r.pass ? "PASS" : "FAIL"}] ${r.name}`);
415
+ for (const e of r.errors) console.log(` - ${e}`);
416
+ }
417
+ }
418
+
419
+ console.log("\n" + "=".repeat(60));
420
+ console.log("self-integrity — design skill");
421
+ console.log("=".repeat(60));
422
+ const pad = (s, n) => (s + " ".repeat(n)).slice(0, n);
423
+ for (const r of results) console.log(`${r.pass ? "PASS" : "FAIL"} ${pad(r.name, 34)} ${r.detail}`);
424
+ console.log("=".repeat(60));
425
+
426
+ const failed = results.filter((r) => !r.pass);
427
+ if (failed.length) { console.log(`${failed.length} check(s) failed.`); process.exit(1); }
428
+ console.log("all checks passed.");
429
+ process.exit(0);
430
+ }
431
+
432
+ main();
@@ -146,7 +146,7 @@ When implementing a plan, if you discover work that is important but outside the
146
146
 
147
147
  ## Authoring Conventions
148
148
 
149
- Most artifacts are markdown — skills, agents, rules, docs. Follow the conventions in `docs/skills/` and the `kb` / `nomenclature` rules; markdown has no file-path-comment rule. Keep the `.claude/rules/*` reference docs frontmatter-free.
149
+ Most artifacts are markdown — skills, agents, rules, docs. Follow the conventions in `docs/architecture/skills/` and the `kb` / `nomenclature` rules; markdown has no file-path-comment rule. Keep the `.claude/rules/*` reference docs frontmatter-free.
150
150
 
151
151
  For real code (the CLI under `packages/`, hook scripts):
152
152
 
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.1.2",
4
+ "version": "1.1.3",
5
5
  "files": [
6
6
  ".prettierrc.json",
7
7
  ".prettierignore",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "e76a61b23051"
10
+ "digest": "2e0c2a4cd5a8"
11
11
  }
@@ -41,6 +41,7 @@ No alternatives. Just use these.
41
41
  | @monaco-editor/react | ^4.7.0 | Code editor | codemirror, ace |
42
42
  | localforage | ^1.10.0 | Client-side storage | idb, dexie |
43
43
  | react-markdown | ^10.1.0 | Markdown rendering | marked, markdown-it |
44
+ | remark-gfm | ^4.0.1 | GFM (tables, tasklists) | remark-github, custom |
44
45
  | fast-deep-equal | ^3.1.3 | Deep equality checks | lodash.isEqual |
45
46
  | @ngneat/falso | latest | Fake data generation | @faker-js/faker, chance |
46
47
  | pino | ^10.1.0 | Logging | winston |
@@ -49,7 +50,7 @@ No alternatives. Just use these.
49
50
 
50
51
  Tailwind v4 + shadcn-based UI; conventions below.
51
52
 
52
- - **Build every UI through the `frontend-design` skill** — route the brief through it instead of hand-rolling layouts; it owns the archetype, personality tokens, and `@nurix/components` wiring.
53
+ - **Build every UI through the `design` skill** — route the brief through it instead of hand-rolling layouts; it owns the archetype, personality tokens, and `@nurix/components` wiring.
53
54
  - **Light mode is the default** — design for the light theme as the default; dark mode is opt-in, never the default.
54
55
  - **@nurix/\* are restricted-scope npm packages** — installed from the npm registry, auto-authed by a read token in `~/.npmrc`. Just attempt the install; the token authenticates you automatically. Don't pre-empt or second-guess it. If the install actually fails, flag it to the user — never reach for alternatives, a substitute package, or `npm login`.
55
56
  - **@nurix/components** — the UI component library (restricted-scope npm package, pre-built); import by subpath, e.g. `@nurix/components/button`.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "mobile",
3
3
  "name": "mobile",
4
- "version": "1.1.2",
4
+ "version": "1.1.3",
5
5
  "files": [
6
6
  ".prettierrc.json",
7
7
  ".prettierignore",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "bc67c99bb1c5"
10
+ "digest": "1ab1fd85719f"
11
11
  }
@@ -41,6 +41,7 @@ No alternatives. Just use these.
41
41
  | @monaco-editor/react | ^4.7.0 | Code editor | codemirror, ace |
42
42
  | localforage | ^1.10.0 | Client-side storage | idb, dexie |
43
43
  | react-markdown | ^10.1.0 | Markdown rendering | marked, markdown-it |
44
+ | remark-gfm | ^4.0.1 | GFM (tables, tasklists) | remark-github, custom |
44
45
  | fast-deep-equal | ^3.1.3 | Deep equality checks | lodash.isEqual |
45
46
  | @ngneat/falso | latest | Fake data generation | @faker-js/faker, chance |
46
47
  | pino | ^10.1.0 | Logging | winston |
@@ -49,7 +50,7 @@ No alternatives. Just use these.
49
50
 
50
51
  Tailwind v4 + shadcn-based UI; conventions below.
51
52
 
52
- - **Build every UI through the `frontend-design` skill** — route the brief through it instead of hand-rolling layouts; it owns the archetype, personality tokens, and `@nurix/components` wiring.
53
+ - **Build every UI through the `design` skill** — route the brief through it instead of hand-rolling layouts; it owns the archetype, personality tokens, and `@nurix/components` wiring.
53
54
  - **Light mode is the default** — design for the light theme as the default; dark mode is opt-in, never the default.
54
55
  - **@nurix/\* are restricted-scope npm packages** — installed from the npm registry, auto-authed by a read token in `~/.npmrc`. Just attempt the install; the token authenticates you automatically. Don't pre-empt or second-guess it. If the install actually fails, flag it to the user — never reach for alternatives, a substitute package, or `npm login`.
55
56
  - **@nurix/components** — the UI component library (restricted-scope npm package, pre-built); import by subpath, e.g. `@nurix/components/button`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.18.18",
3
+ "version": "0.18.20",
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": {