quiver-cli 1.3.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 (120) hide show
  1. package/package.json +1 -1
  2. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  3. package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
  4. package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
  5. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
  6. package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
  7. package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
  8. package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
  9. package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
  10. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
  11. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
  12. package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
  13. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
  14. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
  15. package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
  16. package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
  17. package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
  18. package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
  19. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
  20. package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
  21. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
  22. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
  23. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  24. package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
  25. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  26. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  27. package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
  28. package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
  29. package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
  30. package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
  31. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  32. package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
  33. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  34. package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
  35. package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
  36. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
  37. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
  38. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
  39. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
  40. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
  41. package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
  42. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
  43. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
  44. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
  45. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
  46. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
  47. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
  48. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
  49. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
  50. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  51. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
  52. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
  53. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
  54. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
  55. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
  56. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
  57. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
  58. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
  59. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
  60. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
  61. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
  62. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
  63. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
  64. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
  65. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
  66. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
  67. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
  68. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
  69. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
  70. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
  71. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
  72. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
  73. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
  74. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
  75. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
  76. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
  77. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
  78. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
  79. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
  81. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
  83. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
  84. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
  85. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
  86. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
  87. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
  88. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
  89. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
  90. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  91. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
  92. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
  93. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
  94. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
  95. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  96. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
  97. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
  98. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
  99. package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
  100. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
  101. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
  102. package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
  103. package/template/.agents/skills/hono/SKILL.md +17 -3
  104. package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
  105. package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
  106. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
  107. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
  108. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
  109. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
  110. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
  111. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
  112. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
  113. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
  114. package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
  115. package/template/.agents/skills/supabase/SKILL.md +5 -1
  116. package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
  117. package/template/.agents/skills/writing/humanizer/README.md +209 -0
  118. package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
  119. package/template/.agents/upstreams.json +21 -22
  120. package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
@@ -71,14 +71,54 @@ function contrastRatio(c1, c2) {
71
71
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
72
72
  }
73
73
 
74
+ // The CSS color functions worth pulling out of a longer declaration. The set
75
+ // is deliberately closed: `linear-gradient(` and `url(` also look like
76
+ // `name(` and must not be read as colors.
77
+ const COLOR_FUNCTION_NAMES = new Set([
78
+ 'rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'oklch', 'oklab', 'lch', 'lab', 'color', 'color-mix',
79
+ ]);
80
+
81
+ // Pull every color-function token out of a value, with balanced-paren capture
82
+ // so nested forms (`color-mix(in oklab, oklch(...) 20%, transparent)`) survive
83
+ // whole. Returns the raw substrings in source order.
84
+ function extractColorFunctionTokens(value) {
85
+ const str = String(value || '');
86
+ const tokens = [];
87
+ const re = /([a-z][a-z-]*)\(/gi;
88
+ let m;
89
+ while ((m = re.exec(str)) !== null) {
90
+ if (!COLOR_FUNCTION_NAMES.has(m[1].toLowerCase())) continue;
91
+ let depth = 0, end = -1;
92
+ for (let i = m.index + m[0].length - 1; i < str.length; i++) {
93
+ if (str[i] === '(') depth++;
94
+ else if (str[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
95
+ }
96
+ if (end < 0) break;
97
+ tokens.push(str.slice(m.index, end + 1));
98
+ re.lastIndex = end + 1;
99
+ }
100
+ return tokens;
101
+ }
102
+
74
103
  function parseGradientColors(bgImage) {
75
104
  if (!bgImage || !bgImage.includes('gradient')) return [];
76
105
  const colors = [];
77
- for (const m of bgImage.matchAll(/rgba?\([^)]+\)/g)) {
78
- const c = parseRgb(m[0]);
106
+ const tokenSpans = [];
107
+ let from = 0;
108
+ // Stops arrive in whatever syntax the author wrote and the browser kept.
109
+ // A dark ground painted as `linear-gradient(oklch(...), oklch(...))` used
110
+ // to read as a gradient with no stops at all.
111
+ for (const token of extractColorFunctionTokens(bgImage)) {
112
+ const start = bgImage.indexOf(token, from);
113
+ if (start < 0) break;
114
+ tokenSpans.push({ start, end: start + token.length });
115
+ from = start + token.length;
116
+ const c = parseAnyColor(token);
79
117
  if (c) colors.push(c);
80
118
  }
81
119
  for (const m of bgImage.matchAll(/#([0-9a-f]{6}|[0-9a-f]{3})\b/gi)) {
120
+ // Nested hex inside color-mix is an ingredient, not a stop (issue #578).
121
+ if (tokenSpans.some(s => m.index >= s.start && m.index < s.end)) continue;
82
122
  const h = m[1];
83
123
  if (h.length === 6) {
84
124
  colors.push({ r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16), a: 1 });
@@ -112,13 +152,445 @@ function colorToHex(c) {
112
152
  return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('');
113
153
  }
114
154
 
155
+ // ─── Color-space conversions ────────────────────────────────────────────────
156
+ //
157
+ // Every function here lands on 8-bit sRGB, clamped to gamut. Chrome, Safari,
158
+ // and Firefox all keep the authored color space in getComputedStyle output
159
+ // (`oklch(0.84 0.19 80.46)`, `lch(20 5 60)`, `color(srgb 1.04 0.72 -0.21)`),
160
+ // so a detector that only reads rgb() is blind on any modern palette. The
161
+ // expected outputs are pinned in tests/detect-antipatterns.test.js against
162
+ // what Chrome itself paints for the same strings.
163
+
164
+ function clamp01(x) {
165
+ return Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : 0;
166
+ }
167
+
168
+ // Linear-light sRGB channel to the encoded 0-255 value.
169
+ function encodeSrgbChannel(x) {
170
+ const c = clamp01(x);
171
+ return Math.round((c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055) * 255);
172
+ }
173
+
174
+ function decodeSrgbChannel(x) {
175
+ const c = Number.isFinite(x) ? x : 0;
176
+ const sign = c < 0 ? -1 : 1;
177
+ const abs = Math.abs(c);
178
+ return sign * (abs <= 0.04045 ? abs / 12.92 : Math.pow((abs + 0.055) / 1.055, 2.4));
179
+ }
180
+
181
+ function linearSrgbToColor(r, g, b, a = 1) {
182
+ return { r: encodeSrgbChannel(r), g: encodeSrgbChannel(g), b: encodeSrgbChannel(b), a };
183
+ }
184
+
185
+ // OKLab to sRGB (Björn Ottosson's matrices). L in 0..1, a/b are signed axes.
186
+ function oklabToRgb(L, a, b) {
187
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
188
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
189
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
190
+ const lc = l_ * l_ * l_, mc = m_ * m_ * m_, sc = s_ * s_ * s_;
191
+ return linearSrgbToColor(
192
+ 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc,
193
+ -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc,
194
+ -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc,
195
+ );
196
+ }
197
+
198
+ // OKLCH to sRGB. L in 0..1, C in 0..~0.4 typical, H in degrees. Chroma past
199
+ // the sRGB gamut clamps per channel rather than producing NaN.
200
+ function oklchToRgb(L, C, H) {
201
+ const hRad = (H * Math.PI) / 180;
202
+ return oklabToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
203
+ }
204
+
205
+ // CIE Lab to sRGB. CSS lab()/lch() use the D50 white point; the matrix below
206
+ // is the Bradford-adapted XYZ-D50 to linear-sRGB transform from CSS Color 4.
207
+ function labToRgb(L, a, b) {
208
+ const kappa = 24389 / 27, epsilon = 216 / 24389;
209
+ const fy = (L + 16) / 116, fx = fy + a / 500, fz = fy - b / 200;
210
+ const invert = (t) => (t * t * t > epsilon ? t * t * t : (116 * t - 16) / kappa);
211
+ const yr = L > kappa * epsilon ? Math.pow((L + 16) / 116, 3) : L / kappa;
212
+ const Xn = 0.3457 / 0.3585, Zn = (1 - 0.3457 - 0.3585) / 0.3585;
213
+ const x = invert(fx) * Xn, y = yr, z = invert(fz) * Zn;
214
+ return linearSrgbToColor(
215
+ 3.1341359569958707 * x - 1.6173863321612538 * y - 0.4906619460083532 * z,
216
+ -0.9787955029120890 * x + 1.9162545672595240 * y + 0.0334427311613195 * z,
217
+ 0.0719553798841168 * x - 0.2289768264158322 * y + 1.4053860583241250 * z,
218
+ );
219
+ }
220
+
221
+ function lchToRgb(L, C, H) {
222
+ const hRad = (H * Math.PI) / 180;
223
+ return labToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
224
+ }
225
+
226
+ // color(<space> c1 c2 c3) for the spaces that turn up in real stylesheets.
227
+ // `srgb` is what Chrome serializes most color-mix() results into, routinely
228
+ // with channels outside 0..1. Spaces we do not model return null so callers
229
+ // abstain instead of measuring against a color we invented.
230
+ function colorFunctionToRgb(space, c1, c2, c3) {
231
+ switch (space) {
232
+ case 'srgb':
233
+ return { r: Math.round(clamp01(c1) * 255), g: Math.round(clamp01(c2) * 255), b: Math.round(clamp01(c3) * 255), a: 1 };
234
+ case 'srgb-linear':
235
+ return linearSrgbToColor(c1, c2, c3);
236
+ case 'display-p3': {
237
+ const [R, G, B] = [decodeSrgbChannel(c1), decodeSrgbChannel(c2), decodeSrgbChannel(c3)];
238
+ return linearSrgbToColor(
239
+ 1.2249401762805587 * R - 0.2249404646817506 * G + 0.0000002884022551 * B,
240
+ -0.0420569547096138 * R + 1.0420571661298634 * G - 0.0000002113202247 * B,
241
+ -0.0196375587040044 * R - 0.0786360772174755 * G + 1.0982736359214800 * B,
242
+ );
243
+ }
244
+ default:
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function hslToRgb(h, s, l) {
250
+ h = ((h % 360) + 360) % 360;
251
+ const c = (1 - Math.abs(2 * l - 1)) * s;
252
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
253
+ const m0 = l - c / 2;
254
+ const [r, g, b] =
255
+ h < 60 ? [c, x, 0] :
256
+ h < 120 ? [x, c, 0] :
257
+ h < 180 ? [0, c, x] :
258
+ h < 240 ? [0, x, c] :
259
+ h < 300 ? [x, 0, c] : [c, 0, x];
260
+ return {
261
+ r: Math.round((r + m0) * 255),
262
+ g: Math.round((g + m0) * 255),
263
+ b: Math.round((b + m0) * 255),
264
+ a: 1,
265
+ };
266
+ }
267
+
268
+ function hwbToRgb(h, w, bl) {
269
+ if (w + bl >= 1) {
270
+ const g = Math.round((w / (w + bl)) * 255);
271
+ return { r: g, g, b: g, a: 1 };
272
+ }
273
+ const base = hslToRgb(h, 1, 0.5);
274
+ const mix = (c) => Math.round(((c / 255) * (1 - w - bl) + w) * 255);
275
+ return { r: mix(base.r), g: mix(base.g), b: mix(base.b), a: 1 };
276
+ }
277
+
278
+ // Common CSS named colors — the handful that actually show up in generated
279
+ // UIs, not the full 148-name spec list. Includes the achromatic names so a
280
+ // named gray parses (and correctly reads as no-chroma) instead of being
281
+ // treated as an unknown color.
282
+ const CSS_NAMED_COLORS = {
283
+ black: { r: 0, g: 0, b: 0 },
284
+ white: { r: 255, g: 255, b: 255 },
285
+ gray: { r: 128, g: 128, b: 128 },
286
+ grey: { r: 128, g: 128, b: 128 },
287
+ silver: { r: 192, g: 192, b: 192 },
288
+ dimgray: { r: 105, g: 105, b: 105 },
289
+ darkgray: { r: 169, g: 169, b: 169 },
290
+ lightgray: { r: 211, g: 211, b: 211 },
291
+ gainsboro: { r: 220, g: 220, b: 220 },
292
+ whitesmoke: { r: 245, g: 245, b: 245 },
293
+ red: { r: 255, g: 0, b: 0 },
294
+ crimson: { r: 220, g: 20, b: 60 },
295
+ tomato: { r: 255, g: 99, b: 71 },
296
+ coral: { r: 255, g: 127, b: 80 },
297
+ salmon: { r: 250, g: 128, b: 114 },
298
+ orange: { r: 255, g: 165, b: 0 },
299
+ gold: { r: 255, g: 215, b: 0 },
300
+ yellow: { r: 255, g: 255, b: 0 },
301
+ olive: { r: 128, g: 128, b: 0 },
302
+ lime: { r: 0, g: 255, b: 0 },
303
+ green: { r: 0, g: 128, b: 0 },
304
+ teal: { r: 0, g: 128, b: 128 },
305
+ turquoise: { r: 64, g: 224, b: 208 },
306
+ cyan: { r: 0, g: 255, b: 255 },
307
+ aqua: { r: 0, g: 255, b: 255 },
308
+ skyblue: { r: 135, g: 206, b: 235 },
309
+ dodgerblue: { r: 30, g: 144, b: 255 },
310
+ blue: { r: 0, g: 0, b: 255 },
311
+ navy: { r: 0, g: 0, b: 128 },
312
+ indigo: { r: 75, g: 0, b: 130 },
313
+ rebeccapurple: { r: 102, g: 51, b: 153 },
314
+ purple: { r: 128, g: 0, b: 128 },
315
+ violet: { r: 238, g: 130, b: 238 },
316
+ orchid: { r: 218, g: 112, b: 214 },
317
+ magenta: { r: 255, g: 0, b: 255 },
318
+ fuchsia: { r: 255, g: 0, b: 255 },
319
+ hotpink: { r: 255, g: 105, b: 180 },
320
+ pink: { r: 255, g: 192, b: 203 },
321
+ maroon: { r: 128, g: 0, b: 0 },
322
+ };
323
+
324
+ // Split a string on top-level commas (ignoring commas nested in parens).
325
+ function splitTopLevelCommas(str) {
326
+ const parts = [];
327
+ let depth = 0, start = 0;
328
+ for (let i = 0; i < str.length; i++) {
329
+ const ch = str[i];
330
+ if (ch === '(') depth++;
331
+ else if (ch === ')') depth = Math.max(0, depth - 1);
332
+ else if (ch === ',' && depth === 0) {
333
+ parts.push(str.slice(start, i).trim());
334
+ start = i + 1;
335
+ }
336
+ }
337
+ const tail = str.slice(start).trim();
338
+ if (tail) parts.push(tail);
339
+ return parts;
340
+ }
341
+
342
+ // Evaluate a CSS color-mix() expression to {r,g,b,a}. Returns null when
343
+ // the expression can't be resolved (unresolved var(), unknown colors).
344
+ //
345
+ // Mixing is done with premultiplied alpha in sRGB regardless of the
346
+ // declared interpolation space. That is exact for the dominant generated-UI
347
+ // pattern — `color-mix(in oklab, <color> N%, transparent)` — where the
348
+ // result is simply <color> at alpha N% in ANY rectangular space, and a
349
+ // close-enough approximation for opaque-opaque mixes (the detector only
350
+ // consumes these values for contrast/chroma thresholds, not for display).
351
+ function parseColorMix(str) {
352
+ const m = String(str).trim().match(/^color-mix\(/i);
353
+ if (!m) return null;
354
+ // Balanced-paren capture of the arguments.
355
+ let depth = 0, end = -1;
356
+ const open = str.indexOf('(');
357
+ for (let i = open; i < str.length; i++) {
358
+ if (str[i] === '(') depth++;
359
+ else if (str[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
360
+ }
361
+ if (end < 0) return null;
362
+ const args = splitTopLevelCommas(str.slice(open + 1, end));
363
+ if (args.length !== 3 || !/^in\s/i.test(args[0])) return null;
364
+
365
+ const parseComponent = (component) => {
366
+ // Percentage may lead or trail the color per spec.
367
+ let pct = null;
368
+ let colorStr = component;
369
+ const trail = component.match(/\s+([\d.]+)%$/);
370
+ const lead = component.match(/^([\d.]+)%\s+/);
371
+ if (trail) { pct = parseFloat(trail[1]); colorStr = component.slice(0, trail.index).trim(); }
372
+ else if (lead) { pct = parseFloat(lead[1]); colorStr = component.slice(lead[0].length).trim(); }
373
+ let color;
374
+ if (/^transparent$/i.test(colorStr)) color = { r: 0, g: 0, b: 0, a: 0 };
375
+ else color = parseAnyColor(colorStr);
376
+ if (!color) return null;
377
+ return { color, pct };
378
+ };
379
+
380
+ const c1 = parseComponent(args[1]);
381
+ const c2 = parseComponent(args[2]);
382
+ if (!c1 || !c2) return null;
383
+ let p1 = c1.pct, p2 = c2.pct;
384
+ if (p1 == null && p2 == null) { p1 = 50; p2 = 50; }
385
+ else if (p1 == null) p1 = 100 - p2;
386
+ else if (p2 == null) p2 = 100 - p1;
387
+ const sum = p1 + p2;
388
+ if (sum <= 0) return null;
389
+ // Per spec: weights normalize to sum; when sum < 100 the result alpha is
390
+ // additionally scaled by sum/100.
391
+ const w1 = p1 / sum, w2 = p2 / sum;
392
+ const alphaScale = sum < 100 ? sum / 100 : 1;
393
+ const a1 = c1.color.a ?? 1, a2 = c2.color.a ?? 1;
394
+ const a = (a1 * w1 + a2 * w2) * alphaScale;
395
+ if (a <= 0) return { r: 0, g: 0, b: 0, a: 0 };
396
+ const mix = (ch) => Math.round((c1.color[ch] * a1 * w1 + c2.color[ch] * a2 * w2) / (a1 * w1 + a2 * w2));
397
+ return { r: mix('r'), g: mix('g'), b: mix('b'), a: Math.min(1, a) };
398
+ }
399
+
400
+ // Composite a translucent color over an opaque(ish) base (simple
401
+ // source-over in sRGB). Returns an opaque {r,g,b,a:1}.
402
+ function compositeColorOver(top, base) {
403
+ const a = top.a ?? 1;
404
+ return {
405
+ r: Math.round(top.r * a + base.r * (1 - a)),
406
+ g: Math.round(top.g * a + base.g * (1 - a)),
407
+ b: Math.round(top.b * a + base.b * (1 - a)),
408
+ a: 1,
409
+ };
410
+ }
411
+
412
+ // A color() / lab() / lch() component: a bare number, a percentage against
413
+ // `scale`, or the `none` keyword (which resolves to zero for our purposes).
414
+ function parseColorComponent(token, scale = 1) {
415
+ if (token == null) return null;
416
+ const t = String(token).trim();
417
+ if (/^none$/i.test(t)) return 0;
418
+ const num = parseFloat(t);
419
+ if (!Number.isFinite(num)) return null;
420
+ return t.endsWith('%') ? (num / 100) * scale : num;
421
+ }
422
+
423
+ function parseAlphaToken(token) {
424
+ if (token == null) return 1;
425
+ const t = String(token).trim();
426
+ if (/^none$/i.test(t)) return 1;
427
+ const num = parseFloat(t);
428
+ if (!Number.isFinite(num)) return 1;
429
+ return t.endsWith('%') ? num / 100 : num;
430
+ }
431
+
432
+ // Extended color parser: rgb/rgba/hex/oklch/oklab/lch/lab/hsl/hwb/color()/
433
+ // color-mix/common named colors. Returns null on no match. Use this when the
434
+ // input might be any CSS color form; use plain parseRgb when you only expect
435
+ // computed rgb() values from real browsers.
436
+ function parseAnyColor(s) {
437
+ if (!s || typeof s !== 'string') return null;
438
+ const str = s.trim();
439
+ if (str === 'transparent' || str === 'currentcolor' || str === 'inherit') return null;
440
+ if (/^color-mix\(/i.test(str)) return parseColorMix(str);
441
+ let m;
442
+ m = str.match(/rgba?\(\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)(?:\s*[,/]\s*([\d.]+)(%)?)?\s*\)/);
443
+ if (m) {
444
+ const c = { r: Math.round(+m[1]), g: Math.round(+m[2]), b: Math.round(+m[3]), a: 1 };
445
+ if (m[4] !== undefined) c.a = m[5] === '%' ? parseFloat(m[4]) / 100 : +m[4];
446
+ return c;
447
+ }
448
+ m = str.match(/^#([0-9a-f]{3,8})$/i);
449
+ if (m) {
450
+ const h = m[1];
451
+ if (h.length === 3 || h.length === 4) {
452
+ return {
453
+ r: parseInt(h[0] + h[0], 16),
454
+ g: parseInt(h[1] + h[1], 16),
455
+ b: parseInt(h[2] + h[2], 16),
456
+ a: h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1,
457
+ };
458
+ }
459
+ if (h.length === 6 || h.length === 8) {
460
+ return {
461
+ r: parseInt(h.slice(0, 2), 16),
462
+ g: parseInt(h.slice(2, 4), 16),
463
+ b: parseInt(h.slice(4, 6), 16),
464
+ a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
465
+ };
466
+ }
467
+ }
468
+ // OKLCH parser. Tailwind v4's CSS minifier squishes the space after
469
+ // `%` ("21.5%.02 50"), so the separator between L and C may be absent.
470
+ // Match L (with optional %), then C and H separated permissively.
471
+ m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
472
+ if (m) {
473
+ const Lnum = parseFloat(m[1]);
474
+ const L = m[2] === '%' ? Lnum / 100 : Lnum;
475
+ const rgb = oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
476
+ if (m[5] !== undefined) {
477
+ const alpha = parseFloat(m[5]);
478
+ rgb.a = m[6] === '%' ? alpha / 100 : alpha;
479
+ }
480
+ return rgb;
481
+ }
482
+ // OKLAB — a/b are signed axes; percentages map 100% → 0.4.
483
+ m = str.match(/oklab\(\s*([\d.]+)(%?)\s+(-?[\d.]+)(%?)\s+(-?[\d.]+)(%?)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
484
+ if (m) {
485
+ const L = m[2] === '%' ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
486
+ const a = m[4] === '%' ? parseFloat(m[3]) * 0.004 : parseFloat(m[3]);
487
+ const b = m[6] === '%' ? parseFloat(m[5]) * 0.004 : parseFloat(m[5]);
488
+ const rgb = oklabToRgb(L, a, b);
489
+ if (m[7] !== undefined) {
490
+ const alpha = parseFloat(m[7]);
491
+ rgb.a = m[8] === '%' ? alpha / 100 : alpha;
492
+ }
493
+ return rgb;
494
+ }
495
+ // LCH / LAB — CIE, D50 white point. Chrome serializes lch(20% 5 60) as
496
+ // `lch(20 5 60)`, so L arrives with or without its percent sign. In both
497
+ // spaces L runs 0..100 and 100% means 100.
498
+ m = str.match(/^lch\(\s*([\d.]+%?|none)\s+([\d.]+%?|none)\s+(-?[\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?|none))?\s*\)$/i);
499
+ if (m) {
500
+ const L = parseColorComponent(m[1], 100);
501
+ const C = parseColorComponent(m[2], 150);
502
+ const H = parseFloat(m[3]);
503
+ if (L == null || C == null || !Number.isFinite(H)) return null;
504
+ const rgb = lchToRgb(L, C, H);
505
+ rgb.a = parseAlphaToken(m[4]);
506
+ return rgb;
507
+ }
508
+ m = str.match(/^lab\(\s*([\d.]+%?|none)\s+(-?[\d.]+%?|none)\s+(-?[\d.]+%?|none)(?:\s*\/\s*([\d.]+%?|none))?\s*\)$/i);
509
+ if (m) {
510
+ const L = parseColorComponent(m[1], 100);
511
+ const a = parseColorComponent(m[2], 125);
512
+ const b = parseColorComponent(m[3], 125);
513
+ if (L == null || a == null || b == null) return null;
514
+ const rgb = labToRgb(L, a, b);
515
+ rgb.a = parseAlphaToken(m[4]);
516
+ return rgb;
517
+ }
518
+ // color(<space> c1 c2 c3 [/ alpha]) — what Chrome hands back for most
519
+ // color-mix() results and for any wide-gamut color an author wrote.
520
+ m = str.match(/^color\(\s*([a-z0-9-]+)\s+(-?[\d.eE+-]+%?|none)\s+(-?[\d.eE+-]+%?|none)\s+(-?[\d.eE+-]+%?|none)(?:\s*\/\s*([\d.]+%?|none))?\s*\)$/i);
521
+ if (m) {
522
+ const c1 = parseColorComponent(m[2]);
523
+ const c2 = parseColorComponent(m[3]);
524
+ const c3 = parseColorComponent(m[4]);
525
+ if (c1 == null || c2 == null || c3 == null) return null;
526
+ const rgb = colorFunctionToRgb(m[1].toLowerCase(), c1, c2, c3);
527
+ if (!rgb) return null;
528
+ rgb.a = parseAlphaToken(m[5]);
529
+ return rgb;
530
+ }
531
+ // HSL/HSLA — comma or space syntax, optional deg on hue.
532
+ m = str.match(/hsla?\(\s*(-?[\d.]+)(?:deg)?\s*[,\s]\s*([\d.]+)%\s*[,\s]\s*([\d.]+)%(?:\s*[,/]\s*([\d.]+)(%)?)?\s*\)/i);
533
+ if (m) {
534
+ const rgb = hslToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
535
+ if (m[4] !== undefined) {
536
+ const alpha = parseFloat(m[4]);
537
+ rgb.a = m[5] === '%' ? alpha / 100 : alpha;
538
+ }
539
+ return rgb;
540
+ }
541
+ // HWB — hue whiteness% blackness%.
542
+ m = str.match(/hwb\(\s*(-?[\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
543
+ if (m) {
544
+ const rgb = hwbToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
545
+ if (m[4] !== undefined) {
546
+ const alpha = parseFloat(m[4]);
547
+ rgb.a = m[5] === '%' ? alpha / 100 : alpha;
548
+ }
549
+ return rgb;
550
+ }
551
+ const named = CSS_NAMED_COLORS[str.toLowerCase()];
552
+ if (named) return { ...named, a: 1 };
553
+ return null;
554
+ }
555
+
556
+ // True when a computed background-color string names no paint at all. Used to
557
+ // tell "this layer is see-through" (walk on to the ancestor) apart from "this
558
+ // layer has a color we could not read" (stop and abstain).
559
+ //
560
+ // `inherit` belongs here even though it is not literally see-through: it means
561
+ // "paint with the parent's background-color", and walking on to the parent IS
562
+ // that resolution. Real browsers resolve the keyword before getComputedStyle
563
+ // output; only jsdom's partial cascade hands it through verbatim, and treating
564
+ // it as unreadable would make the walk abstain on a surface it can know.
565
+ // (`currentcolor` is NOT here — it is real paint in the element's own text
566
+ // color; resolveBackgroundInfo substitutes the computed color for it.)
567
+ function isNoPaintColorValue(value) {
568
+ const v = String(value || '').trim().toLowerCase();
569
+ if (!v) return true;
570
+ return v === 'transparent' || v === 'none' || v === 'initial' || v === 'inherit' || v === 'unset' || v === 'revert' || v === 'revert-layer';
571
+ }
572
+
115
573
  export {
116
574
  isNeutralColor,
117
575
  parseRgb,
118
576
  relativeLuminance,
119
577
  contrastRatio,
120
578
  parseGradientColors,
579
+ extractColorFunctionTokens,
121
580
  hasChroma,
122
581
  getHue,
123
582
  colorToHex,
583
+ oklabToRgb,
584
+ oklchToRgb,
585
+ labToRgb,
586
+ lchToRgb,
587
+ colorFunctionToRgb,
588
+ hslToRgb,
589
+ hwbToRgb,
590
+ CSS_NAMED_COLORS,
591
+ splitTopLevelCommas,
592
+ parseColorMix,
593
+ parseAnyColor,
594
+ compositeColorOver,
595
+ isNoPaintColorValue,
124
596
  };
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
56
56
  return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
57
57
  }
58
58
 
59
- const GENERIC_FONTS = new Set([
59
+ // Overused-font primary selection skips only CSS generics so a system stack
60
+ // keeps the system face as primary; GENERIC_FONTS still includes platform
61
+ // faces for design-system/serif resolution.
62
+ const CSS_GENERIC_FONTS = new Set([
60
63
  'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
64
+ 'inherit', 'initial', 'unset', 'revert',
65
+ ]);
66
+
67
+ const GENERIC_FONTS = new Set([
68
+ ...CSS_GENERIC_FONTS,
61
69
  'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
62
70
  '-apple-system', 'blinkmacsystemfont', 'segoe ui',
63
- 'inherit', 'initial', 'unset', 'revert',
64
71
  ]);
65
72
 
73
+ function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
74
+ return String(fontFamily || '')
75
+ .split(',')
76
+ .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
77
+ .find(f => f && !skip.has(f)) || null;
78
+ }
79
+
66
80
  // WCAG large text thresholds are defined in points: 18pt normal text and
67
81
  // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
68
82
  const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
104
118
  BRAND_FONT_DOMAINS,
105
119
  isBrandFontOnOwnDomain,
106
120
  GENERIC_FONTS,
121
+ primaryFontFace,
107
122
  WCAG_LARGE_TEXT_PX,
108
123
  WCAG_LARGE_BOLD_TEXT_PX,
109
124
  EM_DASH_FLOOR,
@@ -33,12 +33,8 @@ import {
33
33
  stampProductSchema,
34
34
  } from './lib/artifact-schema.mjs';
35
35
  import {
36
- checkConfig,
37
- checkDesignSidecar,
36
+ collectBootFindingGroups,
38
37
  checkNativePlatformEvidence,
39
- checkProduct,
40
- checkProjectRoots,
41
- checkSurfaceBriefs,
42
38
  designSidecarCandidatesFor,
43
39
  } from './lib/staleness.mjs';
44
40
  import {
@@ -105,33 +101,30 @@ async function collect(cwd, targetOptions) {
105
101
  extractPlatform,
106
102
  readFile: safeRead,
107
103
  });
104
+ const bootFindings = collectBootFindingGroups(ctx, {
105
+ absDesignPath,
106
+ sidecarCandidates,
107
+ projectRootPatterns: readProjectRootPatterns(ctx.repoRoot),
108
+ targetCandidates: workspaceCandidates,
109
+ });
108
110
 
109
111
  const findings = [
110
- ...checkProduct(ctx.product, ctx.productPath || 'PRODUCT.md'),
111
- ...(ctx.product
112
- ? checkNativePlatformEvidence({
113
- projectRoot,
114
- platform: ctx.platform,
115
- product: ctx.product,
116
- productPath: ctx.productPath,
117
- })
118
- : []),
119
- ...checkDesignSidecar({ designPath: absDesignPath, sidecarCandidates, projectRoot }),
112
+ ...bootFindings.product,
113
+ ...bootFindings.nativePlatform,
114
+ ...bootFindings.designSidecar,
120
115
  ...checkDesignDrift({ designPath: absDesignPath, projectRoot }),
121
116
  ...checkDesignCoverage({ design: ctx.design, designPath: ctx.designPath, parseDesignMd }),
122
- ...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
117
+ ...bootFindings.config,
118
+ ...bootFindings.buildPath,
123
119
  ...checkDetectorIgnores({ projectRoot, knownRuleIds }),
124
- ...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
120
+ ...bootFindings.surfaceBriefs,
125
121
  ...checkHookInstallation({
126
122
  projectRoot,
127
123
  repoRoot: ctx.repoRoot,
128
124
  providerId: IMPECCABLE_PROVIDER_ID,
129
125
  }),
130
126
  ...checkLegacyLiveState({ projectRoot }),
131
- ...checkProjectRoots({
132
- patterns: readProjectRootPatterns(ctx.repoRoot),
133
- candidates: workspaceCandidates,
134
- }),
127
+ ...bootFindings.projectRoots,
135
128
  ...workspaceResult.findings,
136
129
  ];
137
130