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.
- 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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perceptual measures for comparing a comp with a build screenshot. Pure
|
|
3
|
+
* functions over `{ width, height, data }` RGBA images; no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Three families, because a build fails a comp in three separable ways:
|
|
6
|
+
*
|
|
7
|
+
* - structure: is the composition the same? Measured as SSIM over a blurred
|
|
8
|
+
* grayscale downsample, which forgives font hinting and a few pixels of
|
|
9
|
+
* drift and punishes a moved, missing, or invented region.
|
|
10
|
+
* - color: is the palette and its distribution the same? Histogram
|
|
11
|
+
* intersection in a coarse quantized space plus a dominant-color extraction,
|
|
12
|
+
* so a navy page built from a bone comp fails even if the shapes match.
|
|
13
|
+
* - detail: is the material there? Local high-frequency energy per cell. A
|
|
14
|
+
* comp with an illustration, texture, or photograph carries energy a flat
|
|
15
|
+
* CSS stand-in does not; the ratio build/comp per region is the most direct
|
|
16
|
+
* measure of "the plate got replaced by a gradient".
|
|
17
|
+
*/
|
|
18
|
+
import { resize } from './raster.mjs';
|
|
19
|
+
|
|
20
|
+
export function toGray(img) {
|
|
21
|
+
const g = new Float32Array(img.width * img.height);
|
|
22
|
+
for (let i = 0, p = 0; i < g.length; i++, p += 4) {
|
|
23
|
+
const a = img.data[p + 3] / 255;
|
|
24
|
+
// composite over white so transparent regions read as the page ground
|
|
25
|
+
const r = img.data[p] * a + 255 * (1 - a), gg = img.data[p + 1] * a + 255 * (1 - a), b = img.data[p + 2] * a + 255 * (1 - a);
|
|
26
|
+
g[i] = 0.2126 * r + 0.7152 * gg + 0.0722 * b;
|
|
27
|
+
}
|
|
28
|
+
return { width: img.width, height: img.height, data: g };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Separable box blur on a float gray image, radius r. */
|
|
32
|
+
export function blurGray(gray, r) {
|
|
33
|
+
if (r <= 0) return gray;
|
|
34
|
+
const { width, height, data } = gray;
|
|
35
|
+
const tmp = new Float32Array(data.length), out = new Float32Array(data.length);
|
|
36
|
+
const win = 2 * r + 1;
|
|
37
|
+
for (let y = 0; y < height; y++) {
|
|
38
|
+
let acc = 0;
|
|
39
|
+
for (let x = -r; x <= r; x++) acc += data[y * width + Math.min(width - 1, Math.max(0, x))];
|
|
40
|
+
for (let x = 0; x < width; x++) {
|
|
41
|
+
tmp[y * width + x] = acc / win;
|
|
42
|
+
const outX = x - r, inX = x + r + 1;
|
|
43
|
+
acc += data[y * width + Math.min(width - 1, inX)] - data[y * width + Math.max(0, outX)];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (let x = 0; x < width; x++) {
|
|
47
|
+
let acc = 0;
|
|
48
|
+
for (let y = -r; y <= r; y++) acc += tmp[Math.min(height - 1, Math.max(0, y)) * width + x];
|
|
49
|
+
for (let y = 0; y < height; y++) {
|
|
50
|
+
out[y * width + x] = acc / win;
|
|
51
|
+
const outY = y - r, inY = y + r + 1;
|
|
52
|
+
acc += tmp[Math.min(height - 1, inY) * width + x] - tmp[Math.max(0, outY) * width + x];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { width, height, data: out };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Global SSIM between two same-size gray images using an 8x8 window grid. */
|
|
59
|
+
export function ssim(a, b, win = 8) {
|
|
60
|
+
if (a.width !== b.width || a.height !== b.height) throw new Error('ssim: size mismatch');
|
|
61
|
+
const C1 = (0.01 * 255) ** 2, C2 = (0.03 * 255) ** 2;
|
|
62
|
+
let total = 0, n = 0;
|
|
63
|
+
for (let y = 0; y + win <= a.height; y += win) {
|
|
64
|
+
for (let x = 0; x + win <= a.width; x += win) {
|
|
65
|
+
let ma = 0, mb = 0;
|
|
66
|
+
for (let yy = 0; yy < win; yy++) for (let xx = 0; xx < win; xx++) { const i = (y + yy) * a.width + x + xx; ma += a.data[i]; mb += b.data[i]; }
|
|
67
|
+
ma /= win * win; mb /= win * win;
|
|
68
|
+
let va = 0, vb = 0, cov = 0;
|
|
69
|
+
for (let yy = 0; yy < win; yy++) for (let xx = 0; xx < win; xx++) { const i = (y + yy) * a.width + x + xx; const da = a.data[i] - ma, db = b.data[i] - mb; va += da * da; vb += db * db; cov += da * db; }
|
|
70
|
+
va /= win * win - 1; vb /= win * win - 1; cov /= win * win - 1;
|
|
71
|
+
total += ((2 * ma * mb + C1) * (2 * cov + C2)) / ((ma * ma + mb * mb + C1) * (va + vb + C2));
|
|
72
|
+
n++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return n ? total / n : 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** SSIM of `a` against `b` shifted by (dx, dy); the overlap is compared, edges dropped. */
|
|
79
|
+
export function ssimShifted(a, b, dx, dy, win = 8) {
|
|
80
|
+
const w = a.width - Math.abs(dx), h = a.height - Math.abs(dy);
|
|
81
|
+
if (w < win || h < win) return 0;
|
|
82
|
+
const sa = { width: w, height: h, data: new Float32Array(w * h) };
|
|
83
|
+
const sb = { width: w, height: h, data: new Float32Array(w * h) };
|
|
84
|
+
const ax = Math.max(0, -dx), ay = Math.max(0, -dy), bx = Math.max(0, dx), by = Math.max(0, dy);
|
|
85
|
+
for (let y = 0; y < h; y++) {
|
|
86
|
+
sa.data.set(a.data.subarray((y + ay) * a.width + ax, (y + ay) * a.width + ax + w), y * w);
|
|
87
|
+
sb.data.set(b.data.subarray((y + by) * b.width + bx, (y + by) * b.width + bx + w), y * w);
|
|
88
|
+
}
|
|
89
|
+
return ssim(sa, sb, win);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Structure score 0..1: SSIM over blurred grayscale at a fixed working width,
|
|
94
|
+
* taking the best of a small translation search so a composition that sits a
|
|
95
|
+
* few pixels off (a different masthead height, a scrollbar) is not read as a
|
|
96
|
+
* different composition. Shifts up to ~4% of the width are forgiven; a moved
|
|
97
|
+
* region is not.
|
|
98
|
+
*/
|
|
99
|
+
export function structureScore(imgA, imgB, workWidth = 256) {
|
|
100
|
+
const h = Math.max(8, Math.round((imgA.height / imgA.width) * workWidth));
|
|
101
|
+
const a = blurGray(toGray(resize(imgA, workWidth, h)), 2);
|
|
102
|
+
const b = blurGray(toGray(resize(imgB, workWidth, h)), 2);
|
|
103
|
+
const win = Math.min(8, Math.max(2, Math.floor(Math.min(workWidth, h) / 8)));
|
|
104
|
+
let best = ssim(a, b, win);
|
|
105
|
+
const maxShift = Math.max(2, Math.round(workWidth * 0.04));
|
|
106
|
+
for (const dy of [-maxShift, -maxShift / 2, 0, maxShift / 2, maxShift]) {
|
|
107
|
+
for (const dx of [-maxShift, -maxShift / 2, 0, maxShift / 2, maxShift]) {
|
|
108
|
+
if (!dx && !dy) continue;
|
|
109
|
+
best = Math.max(best, ssimShifted(a, b, Math.round(dx), Math.round(dy), win));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Math.max(0, Math.min(1, best));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- color -----------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
function rgbToLab(r, g, b) {
|
|
118
|
+
const lin = (c) => { c /= 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
|
|
119
|
+
const R = lin(r), G = lin(g), B = lin(b);
|
|
120
|
+
const X = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
|
|
121
|
+
const Y = (R * 0.2126 + G * 0.7152 + B * 0.0722) / 1.0;
|
|
122
|
+
const Z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
|
|
123
|
+
const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116);
|
|
124
|
+
const fx = f(X), fy = f(Y), fz = f(Z);
|
|
125
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function deltaE(lab1, lab2) {
|
|
129
|
+
return Math.hypot(lab1[0] - lab2[0], lab1[1] - lab2[1], lab1[2] - lab2[2]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Quantized color histogram (4 bits per channel = 4096 bins), normalized. */
|
|
133
|
+
export function colorHistogram(img, sampleStep = 2) {
|
|
134
|
+
const bins = new Float32Array(4096);
|
|
135
|
+
let n = 0;
|
|
136
|
+
for (let y = 0; y < img.height; y += sampleStep) {
|
|
137
|
+
for (let x = 0; x < img.width; x += sampleStep) {
|
|
138
|
+
const p = (y * img.width + x) * 4;
|
|
139
|
+
if (img.data[p + 3] < 16) continue;
|
|
140
|
+
const key = ((img.data[p] >> 4) << 8) | ((img.data[p + 1] >> 4) << 4) | (img.data[p + 2] >> 4);
|
|
141
|
+
bins[key]++; n++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (n) for (let i = 0; i < bins.length; i++) bins[i] /= n;
|
|
145
|
+
return bins;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function histogramIntersection(h1, h2) {
|
|
149
|
+
let s = 0;
|
|
150
|
+
for (let i = 0; i < h1.length; i++) s += Math.min(h1[i], h2[i]);
|
|
151
|
+
return s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Dominant colors: merge histogram bins greedily by Lab distance into up to
|
|
156
|
+
* `k` clusters and return them sorted by coverage.
|
|
157
|
+
*/
|
|
158
|
+
export function dominantColors(img, k = 6, sampleStep = 3) {
|
|
159
|
+
const hist = colorHistogram(img, sampleStep);
|
|
160
|
+
const entries = [];
|
|
161
|
+
for (let i = 0; i < hist.length; i++) if (hist[i] > 0.0005) entries.push({ key: i, w: hist[i] });
|
|
162
|
+
entries.sort((a, b) => b.w - a.w);
|
|
163
|
+
const clusters = [];
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
const r = ((e.key >> 8) & 15) * 16 + 8, g = ((e.key >> 4) & 15) * 16 + 8, b = (e.key & 15) * 16 + 8;
|
|
166
|
+
const lab = rgbToLab(r, g, b);
|
|
167
|
+
let best = null, bestD = Infinity;
|
|
168
|
+
for (const c of clusters) { const d = deltaE(c.lab, lab); if (d < bestD) { bestD = d; best = c; } }
|
|
169
|
+
if (best && bestD < 14) {
|
|
170
|
+
const tw = best.w + e.w;
|
|
171
|
+
best.rgb = [(best.rgb[0] * best.w + r * e.w) / tw, (best.rgb[1] * best.w + g * e.w) / tw, (best.rgb[2] * best.w + b * e.w) / tw];
|
|
172
|
+
best.lab = rgbToLab(...best.rgb); best.w = tw;
|
|
173
|
+
} else clusters.push({ rgb: [r, g, b], lab, w: e.w });
|
|
174
|
+
}
|
|
175
|
+
clusters.sort((a, b) => b.w - a.w);
|
|
176
|
+
const top = clusters.slice(0, k);
|
|
177
|
+
const covered = top.reduce((s, c) => s + c.w, 0) || 1;
|
|
178
|
+
return top.map((c) => ({ hex: toHex(c.rgb), coverage: +(c.w / covered).toFixed(4), lab: c.lab }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function toHex(rgb) {
|
|
182
|
+
return '#' + rgb.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Palette match 0..1: for each dominant comp color, coverage-weighted best
|
|
187
|
+
* Lab match in the build's dominant set (dE 0 -> 1, dE >= 25 -> 0).
|
|
188
|
+
*/
|
|
189
|
+
export function paletteMatch(compColors, buildColors) {
|
|
190
|
+
if (!compColors.length) return 1;
|
|
191
|
+
let s = 0, wsum = 0;
|
|
192
|
+
for (const c of compColors) {
|
|
193
|
+
let best = Infinity;
|
|
194
|
+
for (const b of buildColors) best = Math.min(best, deltaE(c.lab, b.lab));
|
|
195
|
+
s += c.coverage * Math.max(0, 1 - best / 25); wsum += c.coverage;
|
|
196
|
+
}
|
|
197
|
+
return wsum ? s / wsum : 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Color score 0..1: blend of histogram intersection and dominant-palette match. */
|
|
201
|
+
export function colorScore(imgA, imgB) {
|
|
202
|
+
const inter = histogramIntersection(colorHistogram(imgA), colorHistogram(imgB));
|
|
203
|
+
const pm = paletteMatch(dominantColors(imgA), dominantColors(imgB));
|
|
204
|
+
return { score: 0.35 * inter + 0.65 * pm, intersection: inter, paletteMatch: pm };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- detail ----------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
/** Mean absolute gradient (Sobel-lite) per cell over a cols x rows grid. */
|
|
210
|
+
export function detailGrid(img, cols = 12, rows = 8, workWidth = 512) {
|
|
211
|
+
const h = Math.max(rows, Math.round((img.height / img.width) * workWidth));
|
|
212
|
+
const g = toGray(resize(img, workWidth, h));
|
|
213
|
+
const grid = new Float32Array(cols * rows);
|
|
214
|
+
const counts = new Float32Array(cols * rows);
|
|
215
|
+
for (let y = 1; y < g.height - 1; y++) {
|
|
216
|
+
const cy = Math.min(rows - 1, Math.floor((y / g.height) * rows));
|
|
217
|
+
for (let x = 1; x < g.width - 1; x++) {
|
|
218
|
+
const cx = Math.min(cols - 1, Math.floor((x / g.width) * cols));
|
|
219
|
+
const i = y * g.width + x;
|
|
220
|
+
const gx = Math.abs(g.data[i + 1] - g.data[i - 1]);
|
|
221
|
+
const gy = Math.abs(g.data[i + g.width] - g.data[i - g.width]);
|
|
222
|
+
grid[cy * cols + cx] += gx + gy; counts[cy * cols + cx]++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (let i = 0; i < grid.length; i++) grid[i] = counts[i] ? grid[i] / counts[i] : 0;
|
|
226
|
+
return { cols, rows, cells: grid };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Detail score 0..1 and per-cell ratio. Cells where the comp is nearly flat
|
|
231
|
+
* are ignored (nothing to lose); the score is the coverage-weighted mean of
|
|
232
|
+
* min(1, build/comp) over cells with comp energy, so extra detail in the
|
|
233
|
+
* build (invented chrome) is reported separately as `added`.
|
|
234
|
+
*/
|
|
235
|
+
export function detailScore(imgA, imgB, cols = 12, rows = 8) {
|
|
236
|
+
const a = detailGrid(imgA, cols, rows), b = detailGrid(imgB, cols, rows);
|
|
237
|
+
const floor = 1.5; // energy below this is a flat field at the 512px working width
|
|
238
|
+
let s = 0, w = 0, added = 0, addedW = 0;
|
|
239
|
+
const ratios = new Float32Array(cols * rows);
|
|
240
|
+
for (let i = 0; i < a.cells.length; i++) {
|
|
241
|
+
const ca = a.cells[i], cb = b.cells[i];
|
|
242
|
+
ratios[i] = ca > floor ? cb / ca : (cb > floor ? Infinity : 1);
|
|
243
|
+
// Signed: too much energy is as wrong as too little. Noise, a tile
|
|
244
|
+
// shuffle, or a mosaic saturate a one-sided ratio; a real plate does not.
|
|
245
|
+
if (ca > floor) { s += Math.min(cb / ca, ca / cb) * ca; w += ca; }
|
|
246
|
+
if (cb > ca * 1.8 && cb > floor * 2) { added += 1; }
|
|
247
|
+
addedW += 1;
|
|
248
|
+
}
|
|
249
|
+
const addedFraction = addedW ? added / addedW : 0;
|
|
250
|
+
const raw = w ? s / w : 1;
|
|
251
|
+
return { score: Math.max(0, raw - 0.5 * addedFraction), rawScore: raw, addedFraction, comp: a, build: b, ratios };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- pixel diff -----------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
/** Per-pixel Lab-ish difference map (0..1) at a working width; blurred a little. */
|
|
257
|
+
export function diffMap(imgA, imgB, workWidth = 384) {
|
|
258
|
+
const h = Math.max(8, Math.round((imgA.height / imgA.width) * workWidth));
|
|
259
|
+
const a = resize(imgA, workWidth, h), b = resize(imgB, workWidth, h);
|
|
260
|
+
const out = new Float32Array(workWidth * h);
|
|
261
|
+
for (let i = 0, p = 0; i < out.length; i++, p += 4) {
|
|
262
|
+
const dr = a.data[p] - b.data[p], dg = a.data[p + 1] - b.data[p + 1], db = a.data[p + 2] - b.data[p + 2];
|
|
263
|
+
out[i] = Math.min(1, Math.sqrt(dr * dr + dg * dg + db * db) / 200);
|
|
264
|
+
}
|
|
265
|
+
return blurGray({ width: workWidth, height: h, data: out }, 1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---- bands (horizontal layout structure) ---------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Detect horizontal band boundaries: rows where the mean color changes
|
|
272
|
+
* sharply. Returns normalized y positions (0..1) with strengths. This is the
|
|
273
|
+
* "layout grid" read of a page: header / hero / index / footer as bands.
|
|
274
|
+
*/
|
|
275
|
+
export function horizontalBands(img, workWidth = 128, minGap = 0.02) {
|
|
276
|
+
const h = Math.max(16, Math.round((img.height / img.width) * workWidth));
|
|
277
|
+
const s = resize(img, workWidth, h);
|
|
278
|
+
const rowMean = new Float32Array(h * 3);
|
|
279
|
+
for (let y = 0; y < h; y++) {
|
|
280
|
+
let r = 0, g = 0, b = 0;
|
|
281
|
+
for (let x = 0; x < workWidth; x++) { const p = (y * workWidth + x) * 4; r += s.data[p]; g += s.data[p + 1]; b += s.data[p + 2]; }
|
|
282
|
+
rowMean[y * 3] = r / workWidth; rowMean[y * 3 + 1] = g / workWidth; rowMean[y * 3 + 2] = b / workWidth;
|
|
283
|
+
}
|
|
284
|
+
const edges = [];
|
|
285
|
+
for (let y = 1; y < h; y++) {
|
|
286
|
+
const d = Math.hypot(rowMean[y * 3] - rowMean[(y - 1) * 3], rowMean[y * 3 + 1] - rowMean[(y - 1) * 3 + 1], rowMean[y * 3 + 2] - rowMean[(y - 1) * 3 + 2]);
|
|
287
|
+
if (d > 18) edges.push({ y: y / h, strength: Math.min(1, d / 120) });
|
|
288
|
+
}
|
|
289
|
+
// merge close edges
|
|
290
|
+
const merged = [];
|
|
291
|
+
for (const e of edges) {
|
|
292
|
+
const last = merged[merged.length - 1];
|
|
293
|
+
if (last && e.y - last.y < minGap) { if (e.strength > last.strength) { last.y = e.y; last.strength = e.strength; } }
|
|
294
|
+
else merged.push({ ...e });
|
|
295
|
+
}
|
|
296
|
+
return merged;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Band agreement 0..1: fraction of comp bands with a build band within tolerance, and vice versa. */
|
|
300
|
+
export function bandScore(bandsA, bandsB, tol = 0.04) {
|
|
301
|
+
if (!bandsA.length && !bandsB.length) return 1;
|
|
302
|
+
const match = (from, to) => from.filter((a) => to.some((b) => Math.abs(a.y - b.y) <= tol)).length;
|
|
303
|
+
const recall = bandsA.length ? match(bandsA, bandsB) / bandsA.length : 1;
|
|
304
|
+
const precision = bandsB.length ? match(bandsB, bandsA) / bandsB.length : 1;
|
|
305
|
+
return 0.6 * recall + 0.4 * precision;
|
|
306
|
+
}
|
|
@@ -206,10 +206,10 @@ function parseIgnoreColor(value) {
|
|
|
206
206
|
if (rgb) {
|
|
207
207
|
const parts = splitColorArgs(rgb[1]);
|
|
208
208
|
if (parts.length < 3 || parts.length > 4) return null;
|
|
209
|
-
const r =
|
|
210
|
-
const g =
|
|
211
|
-
const b =
|
|
212
|
-
const a = parts[3] === undefined ? 1 :
|
|
209
|
+
const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb);
|
|
210
|
+
const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb);
|
|
211
|
+
const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb);
|
|
212
|
+
const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha);
|
|
213
213
|
if ([r, g, b, a].some((v) => v === null)) return null;
|
|
214
214
|
return { r, g, b, a };
|
|
215
215
|
}
|
|
@@ -218,10 +218,10 @@ function parseIgnoreColor(value) {
|
|
|
218
218
|
if (hsl) {
|
|
219
219
|
const parts = splitColorArgs(hsl[1]);
|
|
220
220
|
if (parts.length < 3 || parts.length > 4) return null;
|
|
221
|
-
const h =
|
|
222
|
-
const s =
|
|
223
|
-
const l =
|
|
224
|
-
const a = parts[3] === undefined ? 1 :
|
|
221
|
+
const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue);
|
|
222
|
+
const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent);
|
|
223
|
+
const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent);
|
|
224
|
+
const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha);
|
|
225
225
|
if ([h, s, l, a].some((v) => v === null)) return null;
|
|
226
226
|
return hslToRgb(h, s, l, a);
|
|
227
227
|
}
|
|
@@ -230,18 +230,13 @@ function parseIgnoreColor(value) {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
function parseHexIgnoreColor(hex) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
241
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
242
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
243
|
-
const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
|
|
244
|
-
return { r, g, b, a };
|
|
233
|
+
const expanded = hex.length <= 4
|
|
234
|
+
? [...hex].map((digit) => digit.repeat(2)).join('')
|
|
235
|
+
: hex;
|
|
236
|
+
const [r, g, b, alpha = 255] = expanded
|
|
237
|
+
.match(/../g)
|
|
238
|
+
.map((channel) => Number.parseInt(channel, 16));
|
|
239
|
+
return { r, g, b, a: alpha / 255 };
|
|
245
240
|
}
|
|
246
241
|
|
|
247
242
|
function splitColorArgs(body) {
|
|
@@ -259,47 +254,34 @@ function splitColorArgs(body) {
|
|
|
259
254
|
return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/');
|
|
260
255
|
}
|
|
261
256
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return alpha >= 0 && alpha <= 1 ? alpha : null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function parseHueChannel(raw) {
|
|
284
|
-
const text = String(raw || '').trim();
|
|
285
|
-
const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/);
|
|
286
|
-
if (!match) return null;
|
|
287
|
-
const value = Number.parseFloat(match[1]);
|
|
288
|
-
if (!Number.isFinite(value)) return null;
|
|
289
|
-
const unit = match[2] || 'deg';
|
|
290
|
-
if (unit === 'turn') return value * 360;
|
|
291
|
-
if (unit === 'rad') return value * (180 / Math.PI);
|
|
292
|
-
if (unit === 'grad') return value * 0.9;
|
|
293
|
-
return value;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function parsePercentChannel(raw) {
|
|
257
|
+
const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/;
|
|
258
|
+
const identity = (value) => value;
|
|
259
|
+
const COLOR_CHANNEL_FORMATS = {
|
|
260
|
+
rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true },
|
|
261
|
+
alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 },
|
|
262
|
+
hue: {
|
|
263
|
+
units: {
|
|
264
|
+
'': identity,
|
|
265
|
+
deg: identity,
|
|
266
|
+
rad: (value) => value * (180 / Math.PI),
|
|
267
|
+
turn: (value) => value * 360,
|
|
268
|
+
grad: (value) => value * 0.9,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 },
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) {
|
|
297
275
|
const text = String(raw || '').trim();
|
|
298
|
-
const match = text.match(
|
|
276
|
+
const match = text.match(CSS_NUMBER_RE);
|
|
299
277
|
if (!match) return null;
|
|
300
|
-
const
|
|
301
|
-
if (!
|
|
302
|
-
|
|
278
|
+
const convert = units[match[2] || ''];
|
|
279
|
+
if (!convert) return null;
|
|
280
|
+
const number = Number.parseFloat(match[1]);
|
|
281
|
+
if (!Number.isFinite(number)) return null;
|
|
282
|
+
const value = convert(number);
|
|
283
|
+
if (value < min || value > max) return null;
|
|
284
|
+
return round ? Math.round(value) : value;
|
|
303
285
|
}
|
|
304
286
|
|
|
305
287
|
function hslToRgb(hue, saturation, lightness, alpha) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* within the first ~300 characters — catches non-git projects.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|
|
41
41
|
|
|
42
42
|
function isGitIgnored(absPath, cwd) {
|
|
43
43
|
try {
|
|
44
|
-
|
|
44
|
+
// argv form, never a shell: this runs on every file the live-mode source
|
|
45
|
+
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
|
46
|
+
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
|
47
|
+
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
|
45
48
|
cwd,
|
|
46
49
|
stdio: 'ignore',
|
|
47
50
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a live-config glob pattern to a RegExp.
|
|
3
|
+
*
|
|
4
|
+
* Supports `**` across path segments, `*` within one segment, and `?` for one
|
|
5
|
+
* character. Callers normalize project-relative paths to forward slashes.
|
|
6
|
+
*/
|
|
7
|
+
export function livePathGlobToRegex(pattern) {
|
|
8
|
+
let re = '';
|
|
9
|
+
let i = 0;
|
|
10
|
+
while (i < pattern.length) {
|
|
11
|
+
const c = pattern[i];
|
|
12
|
+
if (c === '*') {
|
|
13
|
+
if (pattern[i + 1] === '*') {
|
|
14
|
+
if (pattern[i + 2] === '/') {
|
|
15
|
+
re += '(?:.*/)?';
|
|
16
|
+
i += 3;
|
|
17
|
+
} else {
|
|
18
|
+
re += '.*';
|
|
19
|
+
i += 2;
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
re += '[^/]*';
|
|
23
|
+
i += 1;
|
|
24
|
+
}
|
|
25
|
+
} else if (c === '?') {
|
|
26
|
+
re += '[^/]';
|
|
27
|
+
i += 1;
|
|
28
|
+
} else if (/[.+^${}()|[\]\\]/.test(c)) {
|
|
29
|
+
re += `\\${c}`;
|
|
30
|
+
i += 1;
|
|
31
|
+
} else {
|
|
32
|
+
re += c;
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new RegExp(`^${re}$`);
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function browserOpenCommand(url, {
|
|
4
|
+
platform = process.platform,
|
|
5
|
+
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
|
6
|
+
} = {}) {
|
|
7
|
+
if (platform === 'darwin') return { command: 'open', args: [url] };
|
|
8
|
+
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
|
9
|
+
return { command: 'xdg-open', args: [url] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function openSystemBrowser(url, {
|
|
13
|
+
platform = process.platform,
|
|
14
|
+
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
|
15
|
+
spawnImpl = spawn,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
|
18
|
+
try {
|
|
19
|
+
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
|
20
|
+
child.on('error', () => {});
|
|
21
|
+
child.unref();
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|