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.
- package/dist/cli.js +237 -10
- package/package.json +1 -1
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
- package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
- package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
- package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
- package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
- package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
- package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
- package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
- package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
- package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
- package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
- package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
- package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
- package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
- package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
- package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
- package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
- package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
- package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
- package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
- package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
- package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
- package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
- package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
- package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
- package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
- package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
- package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
- package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
- package/template/.agents/skills/hono/SKILL.md +17 -3
- package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
- package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
- package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
- package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
- package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
- package/template/.agents/skills/supabase/SKILL.md +5 -1
- package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
- package/template/.agents/skills/writing/humanizer/README.md +209 -0
- package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
- package/template/.agents/upstreams.json +21 -22
- 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
|
|
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
|
|
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
|
|
33
|
+
function stdinIsStop(stdinJson) {
|
|
32
34
|
try {
|
|
33
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
280
|
-
const bodyEnd =
|
|
343
|
+
const match = inlineMatches[i];
|
|
344
|
+
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
|
281
345
|
const body = joined
|
|
282
|
-
.slice(
|
|
346
|
+
.slice(match.index + match[0].length, bodyEnd)
|
|
283
347
|
.replace(/\n##[^\n]*$/s, '')
|
|
284
348
|
.replace(/\n###[^\n]*$/s, '')
|
|
285
349
|
.trim();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 (
|
|
294
|
-
const
|
|
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
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
};
|