@monoes/monodesign 1.2.20 → 1.2.23

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 (95) hide show
  1. package/README.md +2 -2
  2. package/cli/engine/cli/main.mjs +21 -1
  3. package/cli/engine/design-system-checks.mjs +381 -0
  4. package/cli/engine/design-system-parse.mjs +433 -0
  5. package/cli/engine/design-system.mjs +20 -736
  6. package/cli/engine/detect-antipatterns-browser.js +28 -5
  7. package/cli/engine/detect-antipatterns.mjs +1 -0
  8. package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
  9. package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
  10. package/cli/engine/engines/regex/detect-text.mjs +11 -523
  11. package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
  12. package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
  13. package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
  14. package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
  15. package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
  16. package/cli/engine/engines/static-html/detect-html.mjs +6 -1
  17. package/cli/engine/findings-group.mjs +94 -0
  18. package/cli/engine/fix/index.mjs +1 -26
  19. package/cli/engine/fix/unified-diff.mjs +107 -0
  20. package/cli/engine/rules/checks-background.mjs +17 -3
  21. package/cli/engine/rules/checks-element.mjs +2 -0
  22. package/cli/engine/rules/checks-pure.mjs +9 -2
  23. package/cli/lib/monodesign-config-consent.mjs +93 -0
  24. package/cli/lib/monodesign-config-core.mjs +37 -0
  25. package/cli/lib/monodesign-config-detection.mjs +133 -0
  26. package/cli/lib/monodesign-config-filter.mjs +198 -0
  27. package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
  28. package/cli/lib/monodesign-config.mjs +21 -623
  29. package/package.json +2 -2
  30. package/skill/scripts/context-fields.mjs +65 -0
  31. package/skill/scripts/context-resolve.mjs +425 -0
  32. package/skill/scripts/context-update.mjs +151 -0
  33. package/skill/scripts/context-utils.mjs +68 -0
  34. package/skill/scripts/context-workspace.mjs +245 -0
  35. package/skill/scripts/context.mjs +17 -891
  36. package/skill/scripts/hook-admin-config.mjs +152 -0
  37. package/skill/scripts/hook-admin-ignore.mjs +131 -0
  38. package/skill/scripts/hook-admin-manifest.mjs +292 -0
  39. package/skill/scripts/hook-admin.mjs +8 -541
  40. package/skill/scripts/hook-lib-cache.mjs +145 -0
  41. package/skill/scripts/hook-lib-config.mjs +292 -0
  42. package/skill/scripts/hook-lib-detector.mjs +72 -0
  43. package/skill/scripts/hook-lib-events.mjs +182 -0
  44. package/skill/scripts/hook-lib-findings.mjs +201 -0
  45. package/skill/scripts/hook-lib-ignore.mjs +205 -0
  46. package/skill/scripts/hook-lib-render.mjs +273 -0
  47. package/skill/scripts/hook-lib-run.mjs +281 -0
  48. package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
  49. package/skill/scripts/hook-lib.mjs +71 -1747
  50. package/skill/scripts/lib/design-parser-colors.mjs +182 -0
  51. package/skill/scripts/lib/design-parser-extract.mjs +333 -0
  52. package/skill/scripts/lib/design-parser-sections.mjs +214 -0
  53. package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
  54. package/skill/scripts/lib/design-parser.mjs +10 -783
  55. package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
  56. package/skill/scripts/live/manual-apply-compact.mjs +122 -0
  57. package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
  58. package/skill/scripts/live/manual-apply-log.mjs +179 -0
  59. package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
  60. package/skill/scripts/live/manual-apply.mjs +65 -640
  61. package/skill/scripts/live/svelte-component-accept.mjs +248 -0
  62. package/skill/scripts/live/svelte-component-css.mjs +230 -0
  63. package/skill/scripts/live/svelte-component-markup.mjs +104 -0
  64. package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
  65. package/skill/scripts/live/svelte-component-session.mjs +158 -0
  66. package/skill/scripts/live/svelte-component.mjs +32 -792
  67. package/skill/scripts/live-accept-mutate.mjs +152 -0
  68. package/skill/scripts/live-accept-parse.mjs +370 -0
  69. package/skill/scripts/live-accept-scrub.mjs +59 -0
  70. package/skill/scripts/live-accept-search.mjs +58 -0
  71. package/skill/scripts/live-accept.mjs +15 -631
  72. package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
  73. package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
  74. package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
  75. package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
  76. package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
  77. package/skill/scripts/live-commit-manual-edits.mjs +24 -881
  78. package/skill/scripts/live-copy-edit-agent.mjs +16 -551
  79. package/skill/scripts/live-copy-edit-compact.mjs +137 -0
  80. package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
  81. package/skill/scripts/live-copy-edit-runners.mjs +297 -0
  82. package/skill/scripts/live-inject-csp.mjs +132 -0
  83. package/skill/scripts/live-inject-files.mjs +229 -0
  84. package/skill/scripts/live-inject-tag.mjs +114 -0
  85. package/skill/scripts/live-inject.mjs +13 -440
  86. package/skill/scripts/live-server-poll.mjs +232 -0
  87. package/skill/scripts/live-server-routes.mjs +449 -0
  88. package/skill/scripts/live-server-state.mjs +361 -0
  89. package/skill/scripts/live-server.mjs +10 -946
  90. package/skill/scripts/live-wrap-css.mjs +132 -0
  91. package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
  92. package/skill/scripts/live-wrap-search.mjs +247 -0
  93. package/skill/scripts/live-wrap.mjs +21 -484
  94. package/skill/scripts/palette-seeds.mjs +401 -0
  95. package/skill/scripts/palette.mjs +1 -395
@@ -0,0 +1,182 @@
1
+ // DESIGN.md "Colors" and "Overview" section extraction. Split out of
2
+ // design-parser.mjs (file-size sweep — pure move, no behaviour change).
3
+
4
+ import {
5
+ HEX_RE,
6
+ OKLCH_RE,
7
+ collectParagraphs,
8
+ collectBullets,
9
+ stripBold,
10
+ splitSubsections,
11
+ extractNamedRules,
12
+ } from './design-parser-sections.mjs';
13
+
14
+ function extractOverview(section) {
15
+ if (!section) return null;
16
+ const text = section.lines.join('\n');
17
+ const northStar = text.match(/\*\*Creative North Star:\s*"([^"]+)"\*\*/);
18
+ const keyChars = [];
19
+ const keyCharMatch = text.match(/\*\*Key Characteristics:\*\*\s*\n([\s\S]+?)(?:\n##|\n###|$)/);
20
+ if (keyCharMatch) {
21
+ for (const line of keyCharMatch[1].split('\n')) {
22
+ const m = line.match(/^\s*[-*]\s+(.+)$/);
23
+ if (m) keyChars.push(stripBold(m[1].trim()));
24
+ }
25
+ }
26
+
27
+ // Philosophy paragraphs: everything that isn't a rule header or key-char block
28
+ const paragraphs = collectParagraphs(section.lines).filter(
29
+ (p) =>
30
+ !p.startsWith('**Creative North Star') &&
31
+ !p.startsWith('**Key Characteristics')
32
+ );
33
+
34
+ return {
35
+ subtitle: section.subtitle,
36
+ creativeNorthStar: northStar ? northStar[1] : null,
37
+ philosophy: paragraphs,
38
+ keyCharacteristics: keyChars,
39
+ };
40
+ }
41
+
42
+ function extractColors(section) {
43
+ if (!section) return null;
44
+ const subs = splitSubsections(section.lines);
45
+
46
+ const description = collectParagraphs(subs[0].lines).join(' ');
47
+ const groups = [];
48
+ const ROLE_KEYWORDS = /^(primary|secondary|tertiary|neutral|accent)\b/i;
49
+
50
+ for (const sub of subs.slice(1)) {
51
+ if (!sub.name || /Named Rules?/i.test(sub.name) || /^The\s/i.test(sub.name)) continue;
52
+
53
+ const bullets = collectBullets(sub.lines);
54
+ const parsed = bullets.map((b) => parseColorBullet(b)).filter(Boolean);
55
+ if (parsed.length === 0) continue;
56
+
57
+ // If every bullet starts with a role keyword (Primary/Secondary/...), promote
58
+ // each bullet to its own group. Otherwise keep the subsection as the group.
59
+ const allRoleBullets =
60
+ parsed.length > 0 && parsed.every((p) => p.name && ROLE_KEYWORDS.test(p.name));
61
+
62
+ if (allRoleBullets) {
63
+ for (const p of parsed) {
64
+ groups.push({ role: p.name, colors: [p] });
65
+ }
66
+ } else {
67
+ groups.push({ role: sub.name, colors: parsed });
68
+ }
69
+ }
70
+
71
+ // If the Colors section has no subsections at all (unlikely), fall back to
72
+ // scanning the whole section as a flat bullet list.
73
+ if (groups.length === 0) {
74
+ const flat = collectBullets(section.lines)
75
+ .map((b) => parseColorBullet(b))
76
+ .filter(Boolean);
77
+ if (flat.length) {
78
+ for (const p of flat) {
79
+ if (p.name && ROLE_KEYWORDS.test(p.name)) {
80
+ groups.push({ role: p.name, colors: [p] });
81
+ } else {
82
+ const fallback = groups.find((g) => g.role === 'Palette');
83
+ if (fallback) fallback.colors.push(p);
84
+ else groups.push({ role: 'Palette', colors: [p] });
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ return {
91
+ subtitle: section.subtitle,
92
+ description: description || null,
93
+ groups,
94
+ rules: extractNamedRules(section.lines),
95
+ };
96
+ }
97
+
98
+ function parseColorBullet(bullet) {
99
+ const text = bullet.trim();
100
+
101
+ // Case 1 (Monodesign): **Name** (value-with-maybe-nested-parens): description
102
+ const bold = text.match(/^\*\*(.+?)\*\*\s*(.*)$/);
103
+ if (bold?.[2].startsWith('(')) {
104
+ const value = extractParenGroup(bold[2]);
105
+ if (value !== null) {
106
+ const after = bold[2].slice(value.length + 2).trimStart();
107
+ if (after.startsWith(':')) {
108
+ return buildColor(bold[1], value, after.slice(1).trim());
109
+ }
110
+ }
111
+ }
112
+
113
+ // Case 2 (Stitch): **Name (values):** description — value embedded in bold.
114
+ const stitch = text.match(/^\*\*([^*]+?)\s*\(([^)]+)\):\*\*\s*(.*)$/);
115
+ if (stitch) {
116
+ return buildColor(stitch[1].trim(), stitch[2], stitch[3]);
117
+ }
118
+
119
+ // Case 3: bullet without bold, just hex/oklch inside.
120
+ const values = collectColorValues(text);
121
+ if (values.length) {
122
+ return buildColor(null, values.join(' to '), text);
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function extractParenGroup(s) {
128
+ if (s[0] !== '(') return null;
129
+ let depth = 0;
130
+ for (let i = 0; i < s.length; i++) {
131
+ if (s[i] === '(') depth++;
132
+ else if (s[i] === ')') {
133
+ depth--;
134
+ if (depth === 0) return s.slice(1, i);
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function buildColor(name, rawValue, description) {
141
+ const values = collectColorValues(rawValue);
142
+ const primary = values[0] ?? rawValue.trim();
143
+ return {
144
+ name: name ? stripBold(name).trim() : null,
145
+ value: primary,
146
+ valueRange: values.length > 1 ? values : null,
147
+ format: detectFormat(primary),
148
+ description: stripBold(description || '').trim() || null,
149
+ };
150
+ }
151
+
152
+ function collectColorValues(s) {
153
+ const out = [];
154
+ s.replace(HEX_RE, (v) => {
155
+ out.push(v);
156
+ return v;
157
+ });
158
+ s.replace(OKLCH_RE, (v) => {
159
+ out.push(v);
160
+ return v;
161
+ });
162
+ return out;
163
+ }
164
+
165
+ function detectFormat(v) {
166
+ if (!v) return 'unknown';
167
+ if (v.startsWith('#')) return 'hex';
168
+ if (/^oklch/i.test(v)) return 'oklch';
169
+ if (/^rgb/i.test(v)) return 'rgb';
170
+ return 'unknown';
171
+ }
172
+
173
+
174
+ export {
175
+ extractOverview,
176
+ extractColors,
177
+ parseColorBullet,
178
+ extractParenGroup,
179
+ buildColor,
180
+ collectColorValues,
181
+ detectFormat,
182
+ };
@@ -0,0 +1,333 @@
1
+ // DESIGN.md "Typography", "Elevation", "Components", "Do's and Don'ts"
2
+ // section extraction, plus frontmatter/prose coverage scoring. Split out of
3
+ // design-parser.mjs (file-size sweep — pure move, no behaviour change).
4
+
5
+ import {
6
+ collectParagraphs,
7
+ collectBullets,
8
+ stripBold,
9
+ splitSubsections,
10
+ extractNamedRules,
11
+ normalizeApostrophes,
12
+ } from './design-parser-sections.mjs';
13
+
14
+ function extractTypography(section) {
15
+ if (!section) return null;
16
+ const text = section.lines.join('\n');
17
+
18
+ const fonts = {};
19
+ // Pattern A: **Display Font:** Family (with fallback)
20
+ const fontLineRe = /\*\*([\w\s/]+?)Font:\*\*\s*([^\n(]+?)(?:\s*\(with\s+([^)]+)\))?\s*$/gm;
21
+ let fm;
22
+ while ((fm = fontLineRe.exec(text)) !== null) {
23
+ const rawRole = fm[1].trim().toLowerCase().replace(/\s+/g, '-');
24
+ const role = normalizeFontRole(rawRole) || 'display';
25
+ fonts[role] = {
26
+ family: fm[2].trim(),
27
+ fallback: fm[3] ? fm[3].trim() : null,
28
+ };
29
+ }
30
+
31
+ // Pattern B (Stitch): * **Display & Headlines (Noto Serif):** description
32
+ if (Object.keys(fonts).length === 0) {
33
+ const stitchRe = /\*\*([\w\s&/]+?)\s*\(([^)]+)\):\*\*\s*(.+)/g;
34
+ let sm;
35
+ while ((sm = stitchRe.exec(text)) !== null) {
36
+ const rawRole = sm[1]
37
+ .trim()
38
+ .toLowerCase()
39
+ .replace(/\s*&\s*/g, '-')
40
+ .replace(/\s+/g, '-');
41
+ const role = normalizeFontRole(rawRole) || rawRole;
42
+ fonts[role] = { family: sm[2].trim(), fallback: null, purpose: sm[3].trim() };
43
+ }
44
+ }
45
+
46
+ // Character paragraph — either a **Character:** label, or fall back to the
47
+ // first free paragraph under the section header (Stitch style).
48
+ const characterMatch = text.match(/\*\*Character:\*\*\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\n|\n###|\n##|$)/);
49
+ let character = characterMatch ? characterMatch[1].replace(/\n/g, ' ').trim() : null;
50
+ if (!character) {
51
+ const paragraphs = collectParagraphs(section.lines).filter(
52
+ (p) => !/^\*\*[\w\s/&]+Font/i.test(p) && !/^\*\*[\w\s/&]+\([^)]+\)/.test(p)
53
+ );
54
+ if (paragraphs.length) character = paragraphs[0];
55
+ }
56
+
57
+ // Hierarchy bullets under ### Hierarchy
58
+ const subs = splitSubsections(section.lines);
59
+ let hierarchy = [];
60
+ const hierSub = subs.find((s) => s.name && /hierarch/i.test(s.name));
61
+ if (hierSub) {
62
+ const bullets = collectBullets(hierSub.lines);
63
+ hierarchy = bullets.map(parseTypeBullet).filter(Boolean);
64
+ }
65
+
66
+ return {
67
+ subtitle: section.subtitle,
68
+ fonts,
69
+ character,
70
+ hierarchy,
71
+ rules: extractNamedRules(section.lines),
72
+ };
73
+ }
74
+
75
+ function normalizeFontRole(raw) {
76
+ // Canonical roles the panel cares about: display, body, label, mono.
77
+ // Stitch often writes compound roles like "display-&-headlines" or "ui-&-body"
78
+ // — collapse them to the first canonical role present.
79
+ const tokens = raw.split(/[-/&\s]+/).filter(Boolean);
80
+ const priority = ['display', 'headline', 'body', 'ui', 'label', 'mono'];
81
+ const canonical = { headline: 'display', ui: 'body' };
82
+ for (const p of priority) {
83
+ if (tokens.includes(p)) return canonical[p] || p;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function parseTypeBullet(bullet) {
89
+ // - **Display** (family, weight 300, italic, clamp(...), line-height 1): purpose
90
+ const m = bullet.match(/^\*\*(.+?)\*\*\s*\(([^)]+)\):\s*(.*)$/);
91
+ if (!m) return null;
92
+ const name = m[1].trim();
93
+ const specs = m[2].split(',').map((s) => s.trim());
94
+ return {
95
+ name,
96
+ specs,
97
+ purpose: stripBold(m[3] || '').trim() || null,
98
+ };
99
+ }
100
+
101
+ function extractElevation(section) {
102
+ if (!section) return null;
103
+ const subs = splitSubsections(section.lines);
104
+
105
+ const description = collectParagraphs(subs[0].lines).join(' ') || null;
106
+
107
+ const shadows = [];
108
+ const seen = new Set();
109
+ const dedupe = (entry) => {
110
+ const key = `${entry.name || ''}::${entry.value}`;
111
+ if (seen.has(key)) return;
112
+ seen.add(key);
113
+ shadows.push(entry);
114
+ };
115
+
116
+ for (const b of collectBullets(section.lines)) {
117
+ const parsed = parseShadowBullet(b);
118
+ if (parsed) dedupe(parsed);
119
+ }
120
+
121
+ // Fallback: extract shadows written inline in prose. Stitch style is
122
+ // "...use an extra-diffused shadow: `box-shadow: 0 12px 40px rgba(...)`."
123
+ for (const p of collectParagraphs(section.lines)) {
124
+ for (const inline of extractInlineShadows(p)) dedupe(inline);
125
+ }
126
+ for (const b of collectBullets(section.lines)) {
127
+ for (const inline of extractInlineShadows(b)) dedupe(inline);
128
+ }
129
+
130
+ return {
131
+ subtitle: section.subtitle,
132
+ description,
133
+ shadows,
134
+ rules: extractNamedRules(section.lines),
135
+ };
136
+ }
137
+
138
+ function extractInlineShadows(text) {
139
+ // Find `box-shadow: ...` anywhere in prose and capture the value. Work on the
140
+ // raw string so it handles both backtick-fenced and unfenced variants.
141
+ const out = [];
142
+ const re = /box-shadow\s*:\s*([^`;\n]+)/gi;
143
+ let m;
144
+ while ((m = re.exec(text)) !== null) {
145
+ const value = m[1].replace(/[`.)]+$/, '').trim();
146
+ if (!value) continue;
147
+ // Name heuristic: the noun immediately before the shadow phrase.
148
+ // e.g. "an extra-diffused shadow: ..." -> "extra-diffused shadow"
149
+ const before = text.slice(0, m.index);
150
+ const nameMatch = before.match(/\b([A-Za-z][A-Za-z\- ]{2,40})\s+shadow\b[^A-Za-z0-9]*$/i);
151
+ let name = null;
152
+ if (nameMatch) {
153
+ const stripped = nameMatch[1]
154
+ .replace(/^(?:use|using|apply|applying|is|are|looks? like)\s+/i, '')
155
+ .replace(/^(?:a|an|the)\s+/i, '')
156
+ .trim();
157
+ if (stripped) {
158
+ name =
159
+ `${stripped.charAt(0).toUpperCase() + stripped.slice(1)} shadow`;
160
+ }
161
+ }
162
+ out.push({
163
+ name,
164
+ value,
165
+ purpose: null,
166
+ });
167
+ }
168
+ return out;
169
+ }
170
+
171
+ function parseShadowBullet(bullet) {
172
+ // - **Name** (`box-shadow: value`): purpose
173
+ // - **Name** (`value`): purpose
174
+ // Only accept if the paren content looks like a shadow value (contains px,
175
+ // rem, rgba, or box-shadow). This filters out `**Rule Name:**` bullets.
176
+ const m = bullet.match(/^\*\*(.+?)\*\*\s*\(`?([^`]+?)`?\):\s*(.*)$/);
177
+ if (!m) return null;
178
+ const rawValue = m[2].replace(/^box-shadow:\s*/i, '').trim();
179
+ const looksLikeShadow =
180
+ /box-shadow|rgba?\(|\bpx\b|\brem\b|^-?\d+\s/i.test(rawValue) &&
181
+ /\d/.test(rawValue);
182
+ if (!looksLikeShadow) return null;
183
+ const name = stripBold(m[1]).trim();
184
+ return {
185
+ name,
186
+ value: rawValue,
187
+ purpose: stripBold(m[3] || '').trim() || null,
188
+ };
189
+ }
190
+
191
+ function extractComponents(section) {
192
+ if (!section) return null;
193
+ const subs = splitSubsections(section.lines);
194
+ const components = [];
195
+
196
+ for (const sub of subs.slice(1)) {
197
+ if (!sub.name) continue;
198
+
199
+ const bullets = collectBullets(sub.lines);
200
+ const paragraphs = collectParagraphs(sub.lines);
201
+
202
+ const variants = [];
203
+ const properties = {};
204
+
205
+ for (const b of bullets) {
206
+ // - **Key:** value
207
+ const m = b.match(/^\*\*(.+?):?\*\*:?\s*(.+)$/);
208
+ if (m) {
209
+ const key = stripBold(m[1]).trim();
210
+ const value = stripBold(m[2]).trim();
211
+ // Heuristic: "Primary", "Secondary", "Hover", "Focus" etc are variants;
212
+ // "Shape", "Background", "Padding" are properties.
213
+ if (/^(primary|secondary|tertiary|ghost|hover|focus|active|disabled|default|error|selected|unselected|state)$/i.test(key.split(/[\s/]/)[0])) {
214
+ variants.push({ name: key, description: value });
215
+ } else {
216
+ properties[key.toLowerCase()] = value;
217
+ }
218
+ }
219
+ }
220
+
221
+ components.push({
222
+ name: sub.name,
223
+ description: paragraphs.join(' ') || null,
224
+ properties,
225
+ variants,
226
+ });
227
+ }
228
+
229
+ return {
230
+ subtitle: section.subtitle,
231
+ components,
232
+ };
233
+ }
234
+
235
+ function extractDosDonts(section) {
236
+ if (!section) return null;
237
+ const subs = splitSubsections(section.lines);
238
+ const dos = [];
239
+ const donts = [];
240
+
241
+ for (const sub of subs.slice(1)) {
242
+ if (!sub.name) continue;
243
+ const subName = normalizeApostrophes(sub.name);
244
+ const bullets = collectBullets(sub.lines).map((b) => stripBold(b).trim());
245
+ if (/^do'?t?:?$/i.test(subName) || /^do:?$/i.test(subName)) {
246
+ dos.push(...bullets);
247
+ } else if (/^don'?t:?$/i.test(subName)) {
248
+ donts.push(...bullets);
249
+ }
250
+ }
251
+
252
+ // Classify by bullet prefix as a backup (catches loose bullets outside H3 wrappers)
253
+ for (const b of collectBullets(section.lines)) {
254
+ const stripped = normalizeApostrophes(stripBold(b).trim());
255
+ if (/^don'?t\b/i.test(stripped)) {
256
+ if (!donts.some((d) => normalizeApostrophes(d) === stripped)) donts.push(stripped);
257
+ } else if (/^do\b/i.test(stripped)) {
258
+ if (!dos.some((d) => normalizeApostrophes(d) === stripped)) dos.push(stripped);
259
+ }
260
+ }
261
+
262
+ return { dos, donts };
263
+ }
264
+
265
+ // ---------- Coverage assessment ----------
266
+
267
+ function assessCoverage(model) {
268
+ const report = {};
269
+
270
+ report.overview = model.overview
271
+ ? {
272
+ northStar: Boolean(model.overview.creativeNorthStar),
273
+ philosophy: model.overview.philosophy.length > 0,
274
+ keyCharacteristics: model.overview.keyCharacteristics.length,
275
+ }
276
+ : 'missing';
277
+
278
+ report.colors = model.colors
279
+ ? {
280
+ groups: model.colors.groups.length,
281
+ totalColors: model.colors.groups.reduce((n, g) => n + g.colors.length, 0),
282
+ rules: model.colors.rules.length,
283
+ }
284
+ : 'missing';
285
+
286
+ report.typography = model.typography
287
+ ? {
288
+ fonts: Object.keys(model.typography.fonts).length,
289
+ hierarchyEntries: model.typography.hierarchy.length,
290
+ character: Boolean(model.typography.character),
291
+ rules: model.typography.rules.length,
292
+ }
293
+ : 'missing';
294
+
295
+ report.elevation = model.elevation
296
+ ? {
297
+ shadows: model.elevation.shadows.length,
298
+ rules: model.elevation.rules.length,
299
+ description: Boolean(model.elevation.description),
300
+ }
301
+ : 'missing';
302
+
303
+ report.components = model.components
304
+ ? {
305
+ count: model.components.components.length,
306
+ variantTotal: model.components.components.reduce((n, c) => n + c.variants.length, 0),
307
+ }
308
+ : 'missing';
309
+
310
+ report.dosDonts = model.dosDonts
311
+ ? {
312
+ dos: model.dosDonts.dos.length,
313
+ donts: model.dosDonts.donts.length,
314
+ }
315
+ : 'missing';
316
+
317
+ return report;
318
+ }
319
+
320
+ // ---------- Main ----------
321
+
322
+
323
+ export {
324
+ extractTypography,
325
+ normalizeFontRole,
326
+ parseTypeBullet,
327
+ extractElevation,
328
+ extractInlineShadows,
329
+ parseShadowBullet,
330
+ extractComponents,
331
+ extractDosDonts,
332
+ assessCoverage,
333
+ };