@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,552 @@
1
+ // skills/design/scripts/scaffold.mjs — generates the non-creative app substrate (routes, nav, per-surface state shells) from docs/design/structure.md
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+
5
+ const ARCHETYPES = ["list-detail", "dashboard", "entity-detail", "create-edit", "log-stream", "command-center"];
6
+ const LANDING = new Set(["list-detail", "dashboard", "log-stream", "command-center"]);
7
+
8
+ // Print the refusal (problem + exact fix) to stderr and exit non-zero.
9
+ function refuse(problem, fix) {
10
+ process.stderr.write(`scaffold.mjs: ${problem}\n`);
11
+ process.stderr.write(`fix: ${fix}\n`);
12
+ process.exit(1);
13
+ }
14
+
15
+ // Parse argv into positionals (_) and --flags (supporting --k v and --k=v).
16
+ function parseArgs(argv) {
17
+ const args = { _: [] };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ if (a.startsWith("--")) {
21
+ const eq = a.indexOf("=");
22
+ if (eq !== -1) {
23
+ args[a.slice(2, eq)] = a.slice(eq + 1);
24
+ } else {
25
+ const next = argv[i + 1];
26
+ if (next !== undefined && !next.startsWith("--")) {
27
+ args[a.slice(2)] = next;
28
+ i++;
29
+ } else {
30
+ args[a.slice(2)] = true;
31
+ }
32
+ }
33
+ } else {
34
+ args._.push(a);
35
+ }
36
+ }
37
+ return args;
38
+ }
39
+
40
+ // Strip a trailing/full-line YAML comment (structure-block values never contain '#').
41
+ function stripComment(line) {
42
+ const h = line.indexOf("#");
43
+ if (h === -1) return line;
44
+ if (h === 0 || /\s/.test(line[h - 1])) return line.slice(0, h).replace(/\s+$/, "");
45
+ return line;
46
+ }
47
+
48
+ // Extract the body of the fenced ```yaml structure block by its info-string.
49
+ function extractStructureBlock(text) {
50
+ const m = text.match(/```yaml structure[^\n]*\n([\s\S]*?)\n```/);
51
+ return m ? m[1] : null;
52
+ }
53
+
54
+ // Collapse the block into indent-tagged, comment-free, non-blank lines.
55
+ function toLines(body) {
56
+ const out = [];
57
+ for (const raw of body.split(/\r?\n/)) {
58
+ const noComment = stripComment(raw);
59
+ if (noComment.trim() === "") continue;
60
+ out.push({ indent: noComment.length - noComment.trimStart().length, text: noComment.trim() });
61
+ }
62
+ return out;
63
+ }
64
+
65
+ // Parse a scalar, inline flow sequence [a, b], or inline flow mapping {k: v}.
66
+ function parseScalarOrFlow(s) {
67
+ const t = s.trim();
68
+ if (t.startsWith("[") && t.endsWith("]")) {
69
+ const inner = t.slice(1, -1).trim();
70
+ if (inner === "") return [];
71
+ return inner.split(",").map((x) => parseScalarOrFlow(x));
72
+ }
73
+ if (t.startsWith("{") && t.endsWith("}")) {
74
+ const inner = t.slice(1, -1).trim();
75
+ const obj = {};
76
+ if (inner === "") return obj;
77
+ for (const pair of inner.split(",")) {
78
+ const idx = pair.indexOf(":");
79
+ if (idx === -1) continue;
80
+ obj[pair.slice(0, idx).trim()] = parseScalarOrFlow(pair.slice(idx + 1));
81
+ }
82
+ return obj;
83
+ }
84
+ return t;
85
+ }
86
+
87
+ // Dispatch a contiguous line range [start,end) to a sequence or mapping parse.
88
+ function parseBlock(lines, start, end) {
89
+ if (start >= end) return null;
90
+ const base = lines[start].indent;
91
+ if (lines[start].text.startsWith("- ")) return parseSequence(lines, start, end, base);
92
+ return parseMapping(lines, start, end, base);
93
+ }
94
+
95
+ // Parse a YAML sequence: each "- " item, with inline-first-pair mapping items merged with their children.
96
+ function parseSequence(lines, start, end, base) {
97
+ const items = [];
98
+ let i = start;
99
+ while (i < end && lines[i].indent === base && lines[i].text.startsWith("- ")) {
100
+ const content = lines[i].text.slice(2).trim();
101
+ let j = i + 1;
102
+ while (j < end && lines[j].indent > base) j++;
103
+ if (content.startsWith("{") || content.startsWith("[")) {
104
+ items.push(parseScalarOrFlow(content));
105
+ } else if (content.indexOf(":") !== -1) {
106
+ const map = {};
107
+ const c = content.indexOf(":");
108
+ const key = content.slice(0, c).trim();
109
+ const rest = content.slice(c + 1).trim();
110
+ map[key] = rest !== "" ? parseScalarOrFlow(rest) : null;
111
+ if (i + 1 < j) {
112
+ const child = parseBlock(lines, i + 1, j);
113
+ if (child && typeof child === "object" && !Array.isArray(child)) Object.assign(map, child);
114
+ }
115
+ items.push(map);
116
+ } else {
117
+ items.push(parseScalarOrFlow(content));
118
+ }
119
+ i = j;
120
+ }
121
+ return items;
122
+ }
123
+
124
+ // Parse a YAML mapping: key/value pairs at one indent, recursing into nested blocks.
125
+ function parseMapping(lines, start, end, base) {
126
+ const map = {};
127
+ let i = start;
128
+ while (i < end && lines[i].indent === base && !lines[i].text.startsWith("- ")) {
129
+ const line = lines[i].text;
130
+ const c = line.indexOf(":");
131
+ if (c === -1) {
132
+ i++;
133
+ continue;
134
+ }
135
+ const key = line.slice(0, c).trim();
136
+ const rest = line.slice(c + 1).trim();
137
+ if (rest !== "") {
138
+ map[key] = parseScalarOrFlow(rest);
139
+ i++;
140
+ } else {
141
+ let j = i + 1;
142
+ while (j < end && lines[j].indent > base) j++;
143
+ map[key] = i + 1 < j ? parseBlock(lines, i + 1, j) : null;
144
+ i = j;
145
+ }
146
+ }
147
+ return map;
148
+ }
149
+
150
+ // Split a name into alphanumeric words on any separator.
151
+ function words(s) {
152
+ return String(s).trim().split(/[^A-Za-z0-9]+/).filter(Boolean);
153
+ }
154
+
155
+ // PascalCase a name (Support Ticket -> SupportTicket).
156
+ function toPascal(s) {
157
+ return words(s).map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase()).join("");
158
+ }
159
+
160
+ // kebab-case a name (Ticket Queue -> ticket-queue).
161
+ function toKebab(s) {
162
+ return words(s).map((w) => w.toLowerCase()).join("-");
163
+ }
164
+
165
+ // camelCase a name, preserving already-camel single tokens (created_at -> createdAt, slaMinutes -> slaMinutes).
166
+ function toCamel(s) {
167
+ const str = String(s).trim();
168
+ if (/^[A-Za-z0-9]+$/.test(str)) return str[0].toLowerCase() + str.slice(1);
169
+ const w = words(str);
170
+ if (!w.length) return "";
171
+ return w[0].toLowerCase() + w.slice(1).map((x) => x[0].toUpperCase() + x.slice(1).toLowerCase()).join("");
172
+ }
173
+
174
+ // Naive English pluralization sufficient for entity nouns.
175
+ function pluralize(word) {
176
+ const w = String(word);
177
+ if (/[^aeiou]y$/i.test(w)) return w.slice(0, -1) + "ies";
178
+ if (/(s|x|z|ch|sh)$/i.test(w)) return w + "es";
179
+ return w + "s";
180
+ }
181
+
182
+ // Capitalize the first character.
183
+ function cap(s) {
184
+ return s ? s[0].toUpperCase() + s.slice(1) : s;
185
+ }
186
+
187
+ // Canonical per-entity identifiers shared with generate-fixtures.mjs (module paths and export names must agree).
188
+ function entityNames(name) {
189
+ const arrayVar = pluralize(toCamel(name));
190
+ return {
191
+ iface: toPascal(name),
192
+ typeFile: toKebab(name),
193
+ fixtureFile: pluralize(toKebab(name)),
194
+ arrayVar,
195
+ emptyVar: "empty" + cap(arrayVar),
196
+ degradedVar: "degraded" + cap(arrayVar),
197
+ };
198
+ }
199
+
200
+ // Normalize a surface's declared states to the canonical loading/empty/error/degraded set.
201
+ function normalizeStates(arr) {
202
+ const alias = { partial: "degraded", degraded: "degraded", loading: "loading", empty: "empty", error: "error" };
203
+ const order = ["loading", "empty", "error", "degraded"];
204
+ const set = new Set();
205
+ for (const s of arr || []) {
206
+ const n = alias[String(s).trim().toLowerCase()];
207
+ if (n) set.add(n);
208
+ }
209
+ const picked = order.filter((s) => set.has(s));
210
+ return picked.length ? picked : order.slice();
211
+ }
212
+
213
+ // Read, parse, and validate docs/design/structure.md into a normalized model, refusing on malformed input.
214
+ function loadStructure(path, { requireSurfaces }) {
215
+ if (!existsSync(path)) refuse(`structure map not found at ${path}`, "pass the path to docs/design/structure.md as the first argument");
216
+ const body = extractStructureBlock(readFileSync(path, "utf8"));
217
+ if (body === null) {
218
+ refuse(
219
+ `no fenced \`\`\`yaml structure block in ${path}`,
220
+ "docs/design/structure.md must carry a fenced ```yaml structure block (see references/structure.md); run the structure phase to produce it",
221
+ );
222
+ }
223
+ const parsed = parseBlock(toLines(body), 0, toLines(body).length) || {};
224
+ const rawSurfaces = Array.isArray(parsed.surfaces) ? parsed.surfaces : [];
225
+ const surfaces = rawSurfaces.map((s, idx) => {
226
+ for (const key of ["name", "archetype", "route", "entity"]) {
227
+ if (!s || typeof s[key] !== "string" || s[key].trim() === "") {
228
+ refuse(
229
+ `surface #${idx + 1} is missing required field "${key}"`,
230
+ `every surfaces[] entry needs name, archetype, route, entity (structure.md schema)`,
231
+ );
232
+ }
233
+ }
234
+ const archetype = String(s.archetype).trim();
235
+ if (!ARCHETYPES.includes(archetype)) {
236
+ refuse(
237
+ `surface "${s.name}" has archetype "${archetype}", not one of the closed six`,
238
+ `set archetype to one of: ${ARCHETYPES.join(", ")} (surface-catalog.md; membership is closed)`,
239
+ );
240
+ }
241
+ let mod = 0;
242
+ if (s["density-modifier"] !== undefined && s["density-modifier"] !== null && String(s["density-modifier"]).trim() !== "") {
243
+ mod = Number.parseInt(String(s["density-modifier"]).trim(), 10);
244
+ if (Number.isNaN(mod)) {
245
+ refuse(
246
+ `surface "${s.name}" has a non-integer density-modifier "${s["density-modifier"]}"`,
247
+ "density-modifier is a signed integer (+1 denser / 0 / -1 looser) per surface-catalog.md",
248
+ );
249
+ }
250
+ }
251
+ return {
252
+ name: String(s.name).trim(),
253
+ archetype,
254
+ route: String(s.route).trim(),
255
+ entity: String(s.entity).trim(),
256
+ densityModifier: mod,
257
+ states: normalizeStates(s.states),
258
+ };
259
+ });
260
+ if (requireSurfaces && surfaces.length === 0) {
261
+ refuse(
262
+ "the structure map declares no surfaces",
263
+ "the object pass must assign at least one surface an archetype before scaffold opens (structure.md); an empty pass fails the structure gate",
264
+ );
265
+ }
266
+ return {
267
+ unitOfWork: typeof parsed["unit-of-work"] === "string" ? parsed["unit-of-work"].trim() : "",
268
+ coreLoop: typeof parsed["core-loop"] === "string" ? parsed["core-loop"].trim() : "",
269
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
270
+ temporal: parsed.temporal || {},
271
+ surfaces,
272
+ };
273
+ }
274
+
275
+ // Write a file under the project root, creating parent directories.
276
+ function emit(root, rel, content, written) {
277
+ const abs = join(root, rel);
278
+ mkdirSync(dirname(abs), { recursive: true });
279
+ writeFileSync(abs, content, "utf8");
280
+ written.push(rel);
281
+ }
282
+
283
+ // Render index.html carrying placeholder <html data-*> personality attributes (derive-personality --emit resolves them).
284
+ function indexHtml() {
285
+ return `<!doctype html>
286
+ <!-- Generated by scaffold.mjs. Personality state lives in these <html data-*> attributes (code is the cache).
287
+ The values below are placeholders; derive-personality.mjs --emit resolves them from the project's design.md. -->
288
+ <html
289
+ lang="en"
290
+ data-style="app"
291
+ data-density="standard"
292
+ data-verbosity="default"
293
+ data-feedback="subtle"
294
+ data-theme="light"
295
+ >
296
+ <head>
297
+ <meta charset="UTF-8" />
298
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
299
+ <title>Operator UI</title>
300
+ </head>
301
+ <body>
302
+ <div id="root"></div>
303
+ <script type="module" src="/src/main.tsx"></script>
304
+ </body>
305
+ </html>
306
+ `;
307
+ }
308
+
309
+ // Render src/main.tsx: mount the router app and load the emitted global stylesheet.
310
+ function mainTsx() {
311
+ return `// src/main.tsx — Vite entry: mounts the router app and loads the emitted global stylesheet (generated by scaffold.mjs; do not hand-edit).
312
+ import { StrictMode } from "react";
313
+ import { createRoot } from "react-dom/client";
314
+ import App from "./App";
315
+ import "./styles/globals.css";
316
+
317
+ const rootElement = document.getElementById("root");
318
+ if (!rootElement) {
319
+ throw new Error("Root element #root not found in index.html");
320
+ }
321
+
322
+ createRoot(rootElement).render(
323
+ <StrictMode>
324
+ <App />
325
+ </StrictMode>,
326
+ );
327
+ `;
328
+ }
329
+
330
+ // Render src/App.tsx: one route per surface, wrapped in the persistent operator shell.
331
+ function appTsx(surfaces) {
332
+ const imports = surfaces
333
+ .map((s) => `import { ${toPascal(s.name)} } from "./routes/${toKebab(s.name)}/${toPascal(s.name)}";`)
334
+ .join("\n");
335
+ const routes = surfaces
336
+ .map((s) => ` <Route path="${s.route}" element={<${toPascal(s.name)} />} />`)
337
+ .join("\n");
338
+ return `// src/App.tsx — application routes and layout wiring (generated from docs/design/structure.md; do not hand-edit).
339
+ import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
340
+ import { AppNav } from "./components/nav/AppNav";
341
+ ${imports}
342
+
343
+ // Persistent operator shell: the navigation rail beside the routed surface.
344
+ function AppLayout() {
345
+ return (
346
+ <div className="flex min-h-[100dvh] bg-background text-foreground">
347
+ <AppNav />
348
+ <main className="flex min-h-0 flex-1 flex-col overflow-auto">
349
+ <Outlet />
350
+ </main>
351
+ </div>
352
+ );
353
+ }
354
+
355
+ // Router: one route per surface in the entity/surface map.
356
+ export default function App() {
357
+ return (
358
+ <BrowserRouter>
359
+ <Routes>
360
+ <Route element={<AppLayout />}>
361
+ ${routes}
362
+ </Route>
363
+ </Routes>
364
+ </BrowserRouter>
365
+ );
366
+ }
367
+ `;
368
+ }
369
+
370
+ // Render src/components/nav/AppNav.tsx: top-level entries derived from the object model, secondary surfaces reached by drilling in.
371
+ function navTsx(surfaces) {
372
+ const navItems = surfaces.filter(
373
+ (s) => LANDING.has(s.archetype) && !s.route.includes(":") && !/\/new$/.test(s.route),
374
+ );
375
+ const items = navItems
376
+ .map((s) => ` { label: "${s.name}", route: "${s.route}" },`)
377
+ .join("\n");
378
+ return `// src/components/nav/AppNav.tsx — top-level navigation derived from the object model (generated; do not hand-edit).
379
+ import { NavLink } from "react-router-dom";
380
+
381
+ interface NavItem {
382
+ label: string;
383
+ route: string;
384
+ }
385
+
386
+ // Top-level entries are the primary entities' home surfaces; detail and create surfaces are reached by drilling in.
387
+ const NAV_ITEMS: NavItem[] = [
388
+ ${items}
389
+ ];
390
+
391
+ // Left navigation rail: one entry per top-level operator surface.
392
+ export function AppNav() {
393
+ return (
394
+ <nav
395
+ aria-label="Primary"
396
+ className="flex w-56 shrink-0 flex-col gap-1 border-r border-sidebar-border bg-sidebar p-3 text-sidebar-foreground"
397
+ >
398
+ {NAV_ITEMS.map((item) => (
399
+ <NavLink
400
+ key={item.route}
401
+ to={item.route}
402
+ end
403
+ className={({ isActive }) =>
404
+ [
405
+ "rounded-md px-3 py-2 text-sm font-medium transition-colors",
406
+ isActive
407
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
408
+ : "text-sidebar-foreground hover:bg-sidebar-accent/60",
409
+ ].join(" ")
410
+ }
411
+ >
412
+ {item.label}
413
+ </NavLink>
414
+ ))}
415
+ </nav>
416
+ );
417
+ }
418
+ `;
419
+ }
420
+
421
+ // Render one surface shell: typed props, fixture wiring, pre-created lifecycle state shells, and the density-step root.
422
+ function surfaceShell(surface) {
423
+ const comp = toPascal(surface.name);
424
+ const slug = toKebab(surface.name);
425
+ const en = entityNames(surface.entity);
426
+ const states = surface.states;
427
+ const hasEmpty = states.includes("empty");
428
+ const hasDegraded = states.includes("degraded");
429
+ const union = [...states, "ready"].map((s) => `"${s}"`).join(" | ");
430
+ const densityAttr =
431
+ surface.densityModifier > 0 ? ' data-density-step="tighter"' : surface.densityModifier < 0 ? ' data-density-step="looser"' : "";
432
+
433
+ const named = [en.arrayVar];
434
+ if (hasEmpty) named.push(en.emptyVar);
435
+ if (hasDegraded) named.push(en.degradedVar);
436
+
437
+ const branches = [];
438
+ if (states.includes("loading")) {
439
+ branches.push(` {state === "loading" && (
440
+ <div className="flex flex-1 items-center justify-center py-12" aria-busy="true">
441
+ <p className="text-sm text-muted-foreground">Loading ${surface.name}</p>
442
+ </div>
443
+ )}`);
444
+ }
445
+ if (hasEmpty) {
446
+ branches.push(` {state === "empty" && (
447
+ <div className="flex flex-1 flex-col items-center justify-center gap-1 py-12 text-center">
448
+ <p className="text-sm font-medium text-foreground">Nothing here yet</p>
449
+ <p className="max-w-sm text-sm text-muted-foreground">
450
+ ${surface.name} has no records yet. New records appear here as the operator creates them.
451
+ </p>
452
+ </div>
453
+ )}`);
454
+ }
455
+ if (states.includes("error")) {
456
+ branches.push(` {state === "error" && (
457
+ <div role="alert" className="flex flex-1 flex-col items-center justify-center gap-1 py-12 text-center">
458
+ <p className="text-sm font-medium text-destructive">Could not load ${surface.name}</p>
459
+ <p className="max-w-sm text-sm text-muted-foreground">The request failed. Retry to reload this surface.</p>
460
+ </div>
461
+ )}`);
462
+ }
463
+ if (hasDegraded) {
464
+ branches.push(` {state === "degraded" && (
465
+ <div className="flex flex-1 flex-col gap-2">
466
+ <p className="text-sm text-warning">Showing partial data. Some records are stale or failed to enrich.</p>
467
+ <${comp}Composition rows={rows} degraded />
468
+ </div>
469
+ )}`);
470
+ }
471
+ branches.push(` {state === "ready" && <${comp}Composition rows={rows} />}`);
472
+
473
+ const pickBody = [];
474
+ if (hasDegraded) pickBody.push(` if (state === "degraded") return ${en.degradedVar};`);
475
+ if (hasEmpty) pickBody.push(` if (state === "empty") return ${en.emptyVar};`);
476
+ pickBody.push(` return ${en.arrayVar};`);
477
+
478
+ const content = `// src/routes/${slug}/${comp}.tsx — ${surface.name} surface shell (${surface.archetype}; generated by scaffold.mjs; do not hand-edit).
479
+ import type { ${en.iface} } from "../../types/${en.typeFile}";
480
+ import { ${named.join(", ")} } from "../../fixtures/${en.fixtureFile}";
481
+
482
+ type SurfaceState = ${union};
483
+
484
+ interface ${comp}Props {
485
+ state?: SurfaceState;
486
+ }
487
+
488
+ // ${surface.name}: the ${surface.archetype} surface; scaffold pre-creates every lifecycle shell for the surfaces phase to fill.
489
+ export function ${comp}({ state = "ready" }: ${comp}Props) {
490
+ const rows: ${en.iface}[] = pickRows(state);
491
+ return (
492
+ <section${densityAttr} aria-label="${surface.name}" className="flex h-full min-h-0 flex-col gap-4 p-6">
493
+ <header className="flex items-center justify-between border-b border-border pb-3">
494
+ <div className="flex flex-col gap-1">
495
+ <h1 className="text-base font-semibold tracking-tight text-foreground">${surface.name}</h1>
496
+ <p className="text-xs text-muted-foreground">${surface.archetype} - ${surface.entity}</p>
497
+ </div>
498
+ <span className="text-xs text-muted-foreground tabular-nums">{rows.length} records</span>
499
+ </header>
500
+ ${branches.join("\n")}
501
+ </section>
502
+ );
503
+ }
504
+
505
+ // Composition region: the surfaces phase composes the ${surface.archetype} archetype here, following its exemplar and the emitted tokens.
506
+ function ${comp}Composition({ rows, degraded = false }: { rows: ${en.iface}[]; degraded?: boolean }) {
507
+ return (
508
+ <div className="flex flex-1 flex-col rounded-lg border border-border bg-card text-card-foreground">
509
+ <div className="flex items-center justify-between border-b border-border px-4 py-3">
510
+ <p className="text-sm font-medium text-foreground">${surface.name}</p>
511
+ <p className="text-xs text-muted-foreground tabular-nums">
512
+ {rows.length} records{degraded ? " (partial)" : ""}
513
+ </p>
514
+ </div>
515
+ <p className="px-4 py-6 text-sm text-muted-foreground">
516
+ Compose the ${surface.archetype} surface for ${surface.entity} here.
517
+ </p>
518
+ </div>
519
+ );
520
+ }
521
+
522
+ // Select the fixture set backing the requested lifecycle state.
523
+ function pickRows(state: SurfaceState): ${en.iface}[] {
524
+ ${pickBody.join("\n")}
525
+ }
526
+ `;
527
+ return { rel: `src/routes/${slug}/${comp}.tsx`, content };
528
+ }
529
+
530
+ // Entry point: load the map, emit the substrate, and report what was written.
531
+ function main() {
532
+ const args = parseArgs(process.argv.slice(2));
533
+ const structurePath = args._[0];
534
+ const out = typeof args.out === "string" ? args.out : null;
535
+ if (!structurePath || !out) {
536
+ refuse("missing arguments", "usage: scaffold.mjs <docs/design/structure.md> --out <project-root-dir>");
537
+ }
538
+ const model = loadStructure(structurePath, { requireSurfaces: true });
539
+ const written = [];
540
+ emit(out, "index.html", indexHtml(), written);
541
+ emit(out, "src/main.tsx", mainTsx(), written);
542
+ emit(out, "src/App.tsx", appTsx(model.surfaces), written);
543
+ emit(out, "src/components/nav/AppNav.tsx", navTsx(model.surfaces), written);
544
+ for (const surface of model.surfaces) {
545
+ const shell = surfaceShell(surface);
546
+ emit(out, shell.rel, shell.content, written);
547
+ }
548
+ process.stdout.write(`scaffold.mjs: wrote ${written.length} files under ${out}\n`);
549
+ for (const rel of written) process.stdout.write(` ${rel}\n`);
550
+ }
551
+
552
+ main();