quiver-cli 1.2.0 → 1.3.1

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 (121) hide show
  1. package/dist/cli.js +237 -10
  2. package/package.json +1 -1
  3. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  4. package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
  5. package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
  6. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
  7. package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
  8. package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
  9. package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
  10. package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
  11. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
  12. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
  13. package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
  14. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
  15. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
  16. package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
  17. package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
  18. package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
  19. package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
  21. package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
  22. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
  23. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
  24. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  25. package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
  26. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  27. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  28. package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
  29. package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
  30. package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
  31. package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
  32. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  33. package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
  34. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  35. package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
  36. package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
  37. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
  38. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
  39. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
  40. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
  41. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
  42. package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
  43. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
  44. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
  45. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
  46. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
  47. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
  48. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
  49. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
  50. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
  51. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  52. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
  53. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
  54. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
  55. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
  56. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
  57. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
  58. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
  59. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
  60. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
  61. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
  62. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
  63. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
  64. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
  65. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
  66. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
  67. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
  68. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
  69. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
  70. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
  71. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
  72. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
  73. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
  74. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
  76. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
  77. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
  78. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
  79. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
  85. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
  86. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
  88. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
  89. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
  90. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
  91. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  92. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
  93. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
  95. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
  96. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  97. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
  98. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
  99. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
  100. package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
  101. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
  102. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
  103. package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
  104. package/template/.agents/skills/hono/SKILL.md +17 -3
  105. package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
  106. package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
  107. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
  108. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
  109. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
  110. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
  111. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
  112. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
  113. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
  114. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
  115. package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
  116. package/template/.agents/skills/supabase/SKILL.md +5 -1
  117. package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
  118. package/template/.agents/skills/writing/humanizer/README.md +209 -0
  119. package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
  120. package/template/.agents/upstreams.json +21 -22
  121. package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
@@ -2,15 +2,17 @@
2
2
  /**
3
3
  * Impeccable design hook — PostToolUse + Stop entry point.
4
4
  *
5
- * Reads the Claude Code / Codex / Cursor hook event from stdin and routes by
6
- * `hook_event_name`:
5
+ * Reads the Claude Code / Codex / Cursor / Grok Build hook event from stdin
6
+ * and routes by Stop vs everything else. Claude uses `hook_event_name:
7
+ * "Stop"`; Grok uses `hookEventName: "stop"`.
7
8
  *
8
9
  * - PostToolUse: runs the immediate-tier detector rules against the touched
9
10
  * file and emits a system reminder via
10
- * `hookSpecificOutput.additionalContext` when findings exist.
11
+ * `hookSpecificOutput.additionalContext` when findings exist. Grok
12
+ * discards that stdout; the scan still warms the session cache for Stop.
11
13
  * - Stop: runs the FULL detector rule set over every UI file touched this
12
14
  * session (the deep pass), deduped against what the per-edit pass already
13
- * surfaced, and emits once via the Stop additionalContext channel.
15
+ * surfaced, and emits once via the harness-specific continuation channel.
14
16
  *
15
17
  * Contract: never break a turn. Always exit 0. Clean files emit a small ack
16
18
  * unless quiet mode is enabled; a clean Stop pass is silent.
@@ -19,7 +21,7 @@
19
21
  * subprocess. This file is the thin stdin/stdout adapter.
20
22
  */
21
23
 
22
- import { runHook, runStopHook, writeAuditLog } from './hook-lib.mjs';
24
+ import { runHook, runStopHook, writeAuditLog, isStopEvent } from './hook-lib.mjs';
23
25
 
24
26
  async function readStdin() {
25
27
  if (process.stdin.isTTY) return '';
@@ -28,10 +30,9 @@ async function readStdin() {
28
30
  return Buffer.concat(chunks).toString('utf-8');
29
31
  }
30
32
 
31
- function isStopEvent(stdinJson) {
33
+ function stdinIsStop(stdinJson) {
32
34
  try {
33
- const event = JSON.parse(stdinJson);
34
- return event && typeof event === 'object' && event.hook_event_name === 'Stop';
35
+ return isStopEvent(JSON.parse(stdinJson));
35
36
  } catch {
36
37
  // Malformed stdin falls through to runHook, which audits the skip.
37
38
  return false;
@@ -48,7 +49,7 @@ async function main() {
48
49
  let stdinJson = '';
49
50
  try { stdinJson = await readStdin(); } catch { /* fall through */ }
50
51
 
51
- const run = isStopEvent(stdinJson) ? runStopHook : runHook;
52
+ const run = stdinIsStop(stdinJson) ? runStopHook : runHook;
52
53
  const result = await run({
53
54
  stdinJson,
54
55
  env: inheritedEnv,
@@ -50,9 +50,36 @@ export function normalizeConceptForm(value) {
50
50
  .trim();
51
51
  }
52
52
 
53
- export function validateConceptEntry(concept, { existingForms = new Map() } = {}) {
53
+ export function validateConceptEntry(concept, { existingForms = new Map(), axes = null } = {}) {
54
54
  const errors = [];
55
55
  const id = concept?.id || '(unknown)';
56
+
57
+ // Recorded aesthetic axis values. Optional, and absent means the value is
58
+ // inferred from the system rules instead. Some axes cannot be inferred at all:
59
+ // depth's keyword probe matched worlds that said "no cast shadow anywhere",
60
+ // and motion and colour strategy describe properties the rules never state, so
61
+ // a wave that assigns those has to record them or the assignment is lost.
62
+ // Validated against the axes definition when the caller supplies it, because a
63
+ // typo would read as "unrecorded" and silently fall back to a probe that is
64
+ // known not to work.
65
+ if (concept?.axes !== undefined && concept.axes !== null) {
66
+ if (typeof concept.axes !== 'object' || Array.isArray(concept.axes)) {
67
+ errors.push(`concept ${id} axes must be an object of axis id to value id`);
68
+ } else if (axes) {
69
+ const byId = new Map((axes.axes || []).map(axis => [axis.id, axis]));
70
+ for (const [axisId, valueId] of Object.entries(concept.axes)) {
71
+ const axis = byId.get(axisId);
72
+ if (!axis) {
73
+ errors.push(`concept ${id} names unknown axis "${axisId}"`);
74
+ } else if (!(axis.values || []).some(value => value.id === valueId)) {
75
+ errors.push(
76
+ `concept ${id} axis "${axisId}" has unknown value "${valueId}" `
77
+ + `(expected one of ${(axis.values || []).map(v => v.id).join(', ')})`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ }
56
83
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(concept?.id || '')) {
57
84
  errors.push(`invalid concept id: ${String(concept?.id)}`);
58
85
  }
@@ -82,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map() } = {}
82
109
  || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
83
110
  errors.push(`concept ${id} must have exactly three structural tags`);
84
111
  }
112
+ // The slop this world in particular is at risk of. Optional, because 541
113
+ // entries predate it and none of them are wrong for lacking it. A world built
114
+ // from posters is at risk of shouting and one built from instruments is at
115
+ // risk of dead greys; a global detector cannot know which, and the author can.
116
+ if (concept?.avoid !== undefined) {
117
+ if (!Array.isArray(concept.avoid)
118
+ || concept.avoid.length < 2
119
+ || concept.avoid.length > 3
120
+ || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
121
+ errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
122
+ }
123
+ }
85
124
  if (!Array.isArray(concept?.system)
86
125
  || concept.system.length !== SYSTEM_PREFIXES.length
87
126
  || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
@@ -2,15 +2,20 @@
2
2
  // the live-mode design-system panel can render. Deterministic, dependency-free.
3
3
  //
4
4
  // Two-layer: YAML frontmatter (machine-readable tokens) + markdown body
5
- // (prose with six canonical H2 sections). When frontmatter is present, it's
5
+ // (prose with eight canonical H2 sections). When frontmatter is present, it's
6
6
  // exposed on `model.frontmatter` alongside the prose-scraped sections;
7
7
  // consumers can prefer frontmatter values and fall back to prose.
8
8
 
9
+ // Array order is also match precedence: matchCanonicalSection's keyword-contained
10
+ // pass returns the first entry a heading contains, so reordering this changes
11
+ // which section an ambiguous heading resolves to.
9
12
  const CANONICAL_SECTIONS = [
10
13
  'Overview',
11
14
  'Colors',
12
15
  'Typography',
16
+ 'Layout',
13
17
  'Elevation',
18
+ 'Shapes',
14
19
  'Components',
15
20
  "Do's and Don'ts",
16
21
  ];
@@ -115,10 +120,71 @@ function stripInlineYamlComment(s) {
115
120
  return s;
116
121
  }
117
122
 
123
+ // YAML double-quoted scalars process backslash escapes. Stripping the outer
124
+ // quotes without unescaping leaves them in place, so a nested font family like
125
+ // fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif"
126
+ // keeps its literal backslashes and never matches the same family in CSS.
127
+ // The full YAML 1.2 double-quote escape set (spec section 5.7).
128
+ const YAML_SIMPLE_ESCAPES = {
129
+ '0': '\0',
130
+ a: '\x07',
131
+ b: '\b',
132
+ t: '\t',
133
+ n: '\n',
134
+ v: '\v',
135
+ f: '\f',
136
+ r: '\r',
137
+ e: '\x1b',
138
+ ' ': ' ',
139
+ '"': '"',
140
+ '/': '/',
141
+ '\\': '\\',
142
+ N: '\u0085',
143
+ _: '\u00a0',
144
+ L: '\u2028',
145
+ P: '\u2029',
146
+ };
147
+ const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 };
148
+
149
+ function unescapeYamlDoubleQuoted(body) {
150
+ let out = '';
151
+ for (let i = 0; i < body.length; i++) {
152
+ const ch = body[i];
153
+ if (ch !== '\\' || i === body.length - 1) {
154
+ out += ch;
155
+ continue;
156
+ }
157
+ const next = body[i + 1];
158
+ if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) {
159
+ out += YAML_SIMPLE_ESCAPES[next];
160
+ i++;
161
+ continue;
162
+ }
163
+ // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay
164
+ // literal rather than corrupting the rest of the scalar.
165
+ const hexLen = YAML_HEX_ESCAPE_LENGTHS[next];
166
+ if (hexLen) {
167
+ const hex = body.slice(i + 2, i + 2 + hexLen);
168
+ const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1;
169
+ if (codePoint >= 0 && codePoint <= 0x10ffff) {
170
+ out += String.fromCodePoint(codePoint);
171
+ i += 1 + hexLen;
172
+ continue;
173
+ }
174
+ }
175
+ out += ch;
176
+ }
177
+ return out;
178
+ }
179
+
118
180
  function parseScalar(raw) {
119
181
  const s = raw.trim();
120
- if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
121
- return s.slice(1, -1);
182
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
183
+ return unescapeYamlDoubleQuoted(s.slice(1, -1));
184
+ }
185
+ // Single-quoted YAML escapes only the quote itself, by doubling it.
186
+ if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) {
187
+ return s.slice(1, -1).split("''").join("'");
122
188
  }
123
189
  if (s === 'true') return true;
124
190
  if (s === 'false') return false;
@@ -130,9 +196,6 @@ function parseScalar(raw) {
130
196
 
131
197
  const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
132
198
  const OKLCH_RE = /oklch\([^)]+\)/gi;
133
- const RGBA_RE = /rgba?\([^)]+\)/gi;
134
- const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
135
- const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
136
199
 
137
200
  // ---------- Section splitting ----------
138
201
 
@@ -266,47 +329,37 @@ function stripBold(s) {
266
329
  function extractNamedRules(lines) {
267
330
  const rules = [];
268
331
  const seen = new Set();
332
+ const addRule = (name, body, { allowDuplicate = false } = {}) => {
333
+ const key = name.toLowerCase();
334
+ if (!allowDuplicate && seen.has(key)) return;
335
+ seen.add(key);
336
+ rules.push({ name, body });
337
+ };
269
338
 
270
339
  // Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
271
340
  const joined = lines.join('\n');
272
- const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
273
- const inlineMatches = [];
274
- let m;
275
- while ((m = inlineStart.exec(joined)) !== null) {
276
- inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
277
- }
341
+ const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
278
342
  for (let i = 0; i < inlineMatches.length; i++) {
279
- const mm = inlineMatches[i];
280
- const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
343
+ const match = inlineMatches[i];
344
+ const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
281
345
  const body = joined
282
- .slice(mm.end, bodyEnd)
346
+ .slice(match.index + match[0].length, bodyEnd)
283
347
  .replace(/\n##[^\n]*$/s, '')
284
348
  .replace(/\n###[^\n]*$/s, '')
285
349
  .trim();
286
- const name = stripBold(mm.name).trim();
287
- seen.add(name.toLowerCase());
288
- rules.push({ name, body: stripBold(body) });
350
+ // Preserve the inline format's historical behavior: repeated inline rules
351
+ // remain visible, while the later heading and bullet formats dedupe.
352
+ addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
289
353
  }
290
354
 
291
355
  // Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
292
356
  // bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
293
- for (let i = 0; i < lines.length; i++) {
294
- const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
295
- if (!h3) continue;
296
- const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
357
+ for (const subsection of splitSubsections(lines).slice(1)) {
358
+ const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
297
359
  if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
298
- if (seen.has(headerName.toLowerCase())) continue;
299
360
 
300
- const bodyLines = [];
301
- for (let j = i + 1; j < lines.length; j++) {
302
- if (/^##\s|^###\s/.test(lines[j])) break;
303
- bodyLines.push(lines[j]);
304
- }
305
- const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
306
- if (body) {
307
- seen.add(headerName.toLowerCase());
308
- rules.push({ name: headerName, body });
309
- }
361
+ const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
362
+ if (body) addRule(headerName, body);
310
363
  }
311
364
 
312
365
  // Style C (Stitch bullet form): "* **The Layering Principle:** body"
@@ -316,9 +369,7 @@ function extractNamedRules(lines) {
316
369
  if (!mm) continue;
317
370
  const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
318
371
  if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
319
- if (seen.has(nameRaw.toLowerCase())) continue;
320
- seen.add(nameRaw.toLowerCase());
321
- rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
372
+ addRule(nameRaw, stripBold(mm[2]).trim());
322
373
  }
323
374
 
324
375
  return rules;
@@ -330,17 +381,16 @@ function extractOverview(section) {
330
381
  if (!section) return null;
331
382
  const text = section.lines.join('\n');
332
383
  const northStar = text.match(/\*\*Creative North Star:\s*"([^"]+)"\*\*/);
333
- const keyChars = [];
334
384
  const keyCharMatch = text.match(/\*\*Key Characteristics:\*\*\s*\n([\s\S]+?)(?:\n##|\n###|$)/);
335
- if (keyCharMatch) {
336
- for (const line of keyCharMatch[1].split('\n')) {
337
- const m = line.match(/^\s*[-*]\s+(.+)$/);
338
- if (m) keyChars.push(stripBold(m[1].trim()));
339
- }
340
- }
385
+ const keyChars = keyCharMatch
386
+ ? collectBullets(keyCharMatch[1].split('\n')).map((bullet) => stripBold(bullet.trim()))
387
+ : [];
388
+ const prose = keyCharMatch
389
+ ? text.slice(0, keyCharMatch.index) + text.slice(keyCharMatch.index + keyCharMatch[0].length)
390
+ : text;
341
391
 
342
392
  // Philosophy paragraphs: everything that isn't a rule header or key-char block
343
- const paragraphs = collectParagraphs(section.lines).filter(
393
+ const paragraphs = collectParagraphs(prose.split('\n')).filter(
344
394
  (p) =>
345
395
  !p.startsWith('**Creative North Star') &&
346
396
  !p.startsWith('**Key Characteristics')
@@ -485,36 +535,6 @@ function detectFormat(v) {
485
535
  return 'unknown';
486
536
  }
487
537
 
488
- function scanInlineColors(lines) {
489
- const out = [];
490
- for (const line of lines) {
491
- if (!/^\s*[-*]\s/.test(line)) continue;
492
- const trimmed = line.replace(/^\s*[-*]\s+/, '');
493
- const color = parseColorBullet(trimmed);
494
- if (color) out.push(color);
495
- }
496
- return out;
497
- }
498
-
499
- function parseStitchInlineGroups(lines) {
500
- // Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
501
- // Each bullet IS its own role. Group them under the spoken role name.
502
- const out = [];
503
- for (const line of lines) {
504
- if (!/^\s*[-*]\s/.test(line)) continue;
505
- const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
506
- const m = trimmed.match(
507
- /^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
508
- );
509
- if (m) {
510
- const role = m[1];
511
- const color = buildColor(role, m[2], m[3]);
512
- out.push({ role, colors: [color] });
513
- }
514
- }
515
- return out;
516
- }
517
-
518
538
  function extractTypography(section) {
519
539
  if (!section) return null;
520
540
  const text = section.lines.join('\n');
@@ -602,11 +622,19 @@ function parseTypeBullet(bullet) {
602
622
  };
603
623
  }
604
624
 
605
- function extractElevation(section) {
625
+ function extractGuidance(section) {
606
626
  if (!section) return null;
607
627
  const subs = splitSubsections(section.lines);
628
+ return {
629
+ subtitle: section.subtitle,
630
+ description: collectParagraphs(subs[0].lines).join(' ') || null,
631
+ rules: extractNamedRules(section.lines),
632
+ };
633
+ }
608
634
 
609
- const description = collectParagraphs(subs[0].lines).join(' ') || null;
635
+ function extractElevation(section) {
636
+ const guidance = extractGuidance(section);
637
+ if (!guidance) return null;
610
638
 
611
639
  const shadows = [];
612
640
  const seen = new Set();
@@ -631,12 +659,7 @@ function extractElevation(section) {
631
659
  for (const inline of extractInlineShadows(b)) dedupe(inline);
632
660
  }
633
661
 
634
- return {
635
- subtitle: section.subtitle,
636
- description,
637
- shadows,
638
- rules: extractNamedRules(section.lines),
639
- };
662
+ return { ...guidance, shadows };
640
663
  }
641
664
 
642
665
  function extractInlineShadows(text) {
@@ -768,6 +791,15 @@ function extractDosDonts(section) {
768
791
 
769
792
  // ---------- Coverage assessment ----------
770
793
 
794
+ // Sections whose model is description-plus-rules only (see extractGuidance).
795
+ const guidanceCoverage = (guidance) =>
796
+ guidance
797
+ ? {
798
+ description: Boolean(guidance.description),
799
+ rules: guidance.rules.length,
800
+ }
801
+ : 'missing';
802
+
771
803
  function assessCoverage(model) {
772
804
  const report = {};
773
805
 
@@ -796,6 +828,8 @@ function assessCoverage(model) {
796
828
  }
797
829
  : 'missing';
798
830
 
831
+ report.layout = guidanceCoverage(model.layout);
832
+
799
833
  report.elevation = model.elevation
800
834
  ? {
801
835
  shadows: model.elevation.shadows.length,
@@ -804,6 +838,8 @@ function assessCoverage(model) {
804
838
  }
805
839
  : 'missing';
806
840
 
841
+ report.shapes = guidanceCoverage(model.shapes);
842
+
807
843
  report.components = model.components
808
844
  ? {
809
845
  count: model.components.components.length,
@@ -833,7 +869,9 @@ export function parseDesignMd(md) {
833
869
  overview: extractOverview(sections['Overview']),
834
870
  colors: extractColors(sections['Colors']),
835
871
  typography: extractTypography(sections['Typography']),
872
+ layout: extractGuidance(sections['Layout']),
836
873
  elevation: extractElevation(sections['Elevation']),
874
+ shapes: extractGuidance(sections['Shapes']),
837
875
  components: extractComponents(sections['Components']),
838
876
  dosDonts: extractDosDonts(sections["Do's and Don'ts"]),
839
877
  };