@nurix/etna 0.18.19 → 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.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/skills/design/SKILL.md +65 -0
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
- package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
- package/data/harnesses/code/skills/design/references/create.md +126 -0
- package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
- package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
- package/data/harnesses/code/skills/design/references/improve.md +85 -0
- package/data/harnesses/code/skills/design/references/interaction.md +77 -0
- package/data/harnesses/code/skills/design/references/introspect.md +100 -0
- package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
- package/data/harnesses/code/skills/design/references/personality.md +504 -0
- package/data/harnesses/code/skills/design/references/structure.md +138 -0
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
- package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
- package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
- package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
- 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();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "0.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": {
|