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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free PNG decode/encode for the skill scripts.
|
|
3
|
+
*
|
|
4
|
+
* decodePng(buffer) -> { width, height, data } where data is RGBA8 (Uint8Array,
|
|
5
|
+
* width*height*4). Handles every color type (0, 2, 3, 4, 6), bit depths 1-16
|
|
6
|
+
* (16-bit is reduced to 8), all five filters, and Adam7 interlacing.
|
|
7
|
+
*
|
|
8
|
+
* encodePng({ width, height, data }) -> Buffer, RGBA8 in, 8-bit RGBA PNG out.
|
|
9
|
+
*
|
|
10
|
+
* Kept small on purpose: the skill scripts ship without npm dependencies, and
|
|
11
|
+
* comps (gpt-image PNGs) and screenshots (Playwright / harness PNGs) are the
|
|
12
|
+
* only formats the comp-fidelity tooling has to read.
|
|
13
|
+
*/
|
|
14
|
+
import zlib from 'node:zlib';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
19
|
+
|
|
20
|
+
const crcTable = (() => {
|
|
21
|
+
const t = new Uint32Array(256);
|
|
22
|
+
for (let n = 0; n < 256; n++) {
|
|
23
|
+
let c = n;
|
|
24
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
25
|
+
t[n] = c >>> 0;
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
function crc32(data) {
|
|
31
|
+
let c = 0xffffffff;
|
|
32
|
+
for (let i = 0; i < data.length; i++) c = crcTable[(c ^ data[i]) & 0xff] ^ (c >>> 8);
|
|
33
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isPng(buf) {
|
|
37
|
+
return buf && buf.length > 8 && buf.subarray(0, 8).equals(SIGNATURE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readChunks(buf) {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let pos = 8;
|
|
43
|
+
while (pos + 8 <= buf.length) {
|
|
44
|
+
const length = buf.readUInt32BE(pos);
|
|
45
|
+
const type = buf.toString('latin1', pos + 4, pos + 8);
|
|
46
|
+
const data = buf.subarray(pos + 8, pos + 8 + length);
|
|
47
|
+
chunks.push({ type, data });
|
|
48
|
+
pos += 12 + length;
|
|
49
|
+
if (type === 'IEND') break;
|
|
50
|
+
}
|
|
51
|
+
return chunks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
55
|
+
|
|
56
|
+
function paeth(a, b, c) {
|
|
57
|
+
const p = a + b - c;
|
|
58
|
+
const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
|
59
|
+
if (pa <= pb && pa <= pc) return a;
|
|
60
|
+
if (pb <= pc) return b;
|
|
61
|
+
return c;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Unfilter one pass of scanlines in place; returns the raw (unfiltered) bytes. */
|
|
65
|
+
function unfilter(raw, width, height, bpp, bitDepth, channels) {
|
|
66
|
+
const stride = Math.ceil((width * channels * bitDepth) / 8);
|
|
67
|
+
const out = new Uint8Array(stride * height);
|
|
68
|
+
let inPos = 0;
|
|
69
|
+
let prev = null;
|
|
70
|
+
for (let y = 0; y < height; y++) {
|
|
71
|
+
const filter = raw[inPos++];
|
|
72
|
+
const line = out.subarray(y * stride, (y + 1) * stride);
|
|
73
|
+
line.set(raw.subarray(inPos, inPos + stride));
|
|
74
|
+
inPos += stride;
|
|
75
|
+
switch (filter) {
|
|
76
|
+
case 0: break;
|
|
77
|
+
case 1: for (let i = bpp; i < stride; i++) line[i] = (line[i] + line[i - bpp]) & 0xff; break;
|
|
78
|
+
case 2: if (prev) for (let i = 0; i < stride; i++) line[i] = (line[i] + prev[i]) & 0xff; break;
|
|
79
|
+
case 3:
|
|
80
|
+
for (let i = 0; i < stride; i++) {
|
|
81
|
+
const left = i >= bpp ? line[i - bpp] : 0;
|
|
82
|
+
const up = prev ? prev[i] : 0;
|
|
83
|
+
line[i] = (line[i] + ((left + up) >> 1)) & 0xff;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case 4:
|
|
87
|
+
for (let i = 0; i < stride; i++) {
|
|
88
|
+
const left = i >= bpp ? line[i - bpp] : 0;
|
|
89
|
+
const up = prev ? prev[i] : 0;
|
|
90
|
+
const ul = prev && i >= bpp ? prev[i - bpp] : 0;
|
|
91
|
+
line[i] = (line[i] + paeth(left, up, ul)) & 0xff;
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
default: throw new Error(`png: unknown filter ${filter} on row ${y}`);
|
|
95
|
+
}
|
|
96
|
+
prev = line;
|
|
97
|
+
}
|
|
98
|
+
return { bytes: out, stride, consumed: inPos };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Read sample `index` (0-based across the row) from a packed scanline. */
|
|
102
|
+
function sampleReader(bitDepth) {
|
|
103
|
+
if (bitDepth === 8) return (line, i) => line[i];
|
|
104
|
+
if (bitDepth === 16) return (line, i) => line[i * 2]; // high byte
|
|
105
|
+
const perByte = 8 / bitDepth;
|
|
106
|
+
const mask = (1 << bitDepth) - 1;
|
|
107
|
+
const scale = 255 / mask;
|
|
108
|
+
return (line, i) => {
|
|
109
|
+
const byte = line[(i / perByte) | 0];
|
|
110
|
+
const shift = 8 - bitDepth * ((i % perByte) + 1);
|
|
111
|
+
return Math.round(((byte >> shift) & mask) * scale);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writePixels(dst, dstWidth, bytes, stride, passWidth, passHeight, colorType, bitDepth, palette, trns, mapX, mapY) {
|
|
116
|
+
const channels = CHANNELS[colorType];
|
|
117
|
+
const read = sampleReader(bitDepth);
|
|
118
|
+
const rawIndex = bitDepth < 8 ? (line, i) => {
|
|
119
|
+
const perByte = 8 / bitDepth;
|
|
120
|
+
const mask = (1 << bitDepth) - 1;
|
|
121
|
+
const byte = line[(i / perByte) | 0];
|
|
122
|
+
const shift = 8 - bitDepth * ((i % perByte) + 1);
|
|
123
|
+
return (byte >> shift) & mask;
|
|
124
|
+
} : read;
|
|
125
|
+
for (let y = 0; y < passHeight; y++) {
|
|
126
|
+
const line = bytes.subarray(y * stride, (y + 1) * stride);
|
|
127
|
+
const dy = mapY(y);
|
|
128
|
+
for (let x = 0; x < passWidth; x++) {
|
|
129
|
+
const dx = mapX(x);
|
|
130
|
+
const o = (dy * dstWidth + dx) * 4;
|
|
131
|
+
let r, g, b, a = 255;
|
|
132
|
+
switch (colorType) {
|
|
133
|
+
case 0: {
|
|
134
|
+
r = g = b = read(line, x);
|
|
135
|
+
if (trns && trns.gray === rawIndex(line, x)) a = 0;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 2: {
|
|
139
|
+
r = read(line, x * 3); g = read(line, x * 3 + 1); b = read(line, x * 3 + 2);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 3: {
|
|
143
|
+
const idx = rawIndex(line, x);
|
|
144
|
+
r = palette[idx * 3]; g = palette[idx * 3 + 1]; b = palette[idx * 3 + 2];
|
|
145
|
+
if (trns && trns.alpha && idx < trns.alpha.length) a = trns.alpha[idx];
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 4: {
|
|
149
|
+
r = g = b = read(line, x * 2); a = read(line, x * 2 + 1);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 6: {
|
|
153
|
+
r = read(line, x * 4); g = read(line, x * 4 + 1); b = read(line, x * 4 + 2); a = read(line, x * 4 + 3);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
default: throw new Error(`png: unsupported color type ${colorType}`);
|
|
157
|
+
}
|
|
158
|
+
dst[o] = r; dst[o + 1] = g; dst[o + 2] = b; dst[o + 3] = a;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return channels;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function decodePng(buf) {
|
|
165
|
+
if (!isPng(buf)) throw new Error('png: not a PNG (bad signature)');
|
|
166
|
+
const chunks = readChunks(buf);
|
|
167
|
+
const ihdr = chunks.find((c) => c.type === 'IHDR');
|
|
168
|
+
if (!ihdr) throw new Error('png: missing IHDR');
|
|
169
|
+
const width = ihdr.data.readUInt32BE(0);
|
|
170
|
+
const height = ihdr.data.readUInt32BE(4);
|
|
171
|
+
const bitDepth = ihdr.data[8];
|
|
172
|
+
const colorType = ihdr.data[9];
|
|
173
|
+
const interlace = ihdr.data[12];
|
|
174
|
+
const channels = CHANNELS[colorType];
|
|
175
|
+
if (!channels) throw new Error(`png: unsupported color type ${colorType}`);
|
|
176
|
+
const palChunk = chunks.find((c) => c.type === 'PLTE');
|
|
177
|
+
const palette = palChunk ? palChunk.data : null;
|
|
178
|
+
const trnsChunk = chunks.find((c) => c.type === 'tRNS');
|
|
179
|
+
let trns = null;
|
|
180
|
+
if (trnsChunk) {
|
|
181
|
+
if (colorType === 3) trns = { alpha: trnsChunk.data };
|
|
182
|
+
else if (colorType === 0) trns = { gray: trnsChunk.data.readUInt16BE(0) >> (bitDepth === 16 ? 8 : 0) };
|
|
183
|
+
}
|
|
184
|
+
const idat = Buffer.concat(chunks.filter((c) => c.type === 'IDAT').map((c) => c.data));
|
|
185
|
+
const raw = zlib.inflateSync(idat);
|
|
186
|
+
const bpp = Math.max(1, Math.ceil((channels * bitDepth) / 8));
|
|
187
|
+
const data = new Uint8Array(width * height * 4);
|
|
188
|
+
const text = {};
|
|
189
|
+
for (const c of chunks) {
|
|
190
|
+
if (c.type === 'tEXt') {
|
|
191
|
+
const z = c.data.indexOf(0);
|
|
192
|
+
if (z > 0) text[c.data.toString('latin1', 0, z)] = c.data.toString('utf8', z + 1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (interlace === 0) {
|
|
197
|
+
const { bytes, stride } = unfilter(raw, width, height, bpp, bitDepth, channels);
|
|
198
|
+
writePixels(data, width, bytes, stride, width, height, colorType, bitDepth, palette, trns, (x) => x, (y) => y);
|
|
199
|
+
} else {
|
|
200
|
+
// Adam7
|
|
201
|
+
const passes = [
|
|
202
|
+
[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2],
|
|
203
|
+
];
|
|
204
|
+
let offset = 0;
|
|
205
|
+
for (const [sx, sy, dx, dy] of passes) {
|
|
206
|
+
const pw = Math.ceil((width - sx) / dx);
|
|
207
|
+
const ph = Math.ceil((height - sy) / dy);
|
|
208
|
+
if (pw <= 0 || ph <= 0) continue;
|
|
209
|
+
const { bytes, stride, consumed } = unfilter(raw.subarray(offset), pw, ph, bpp, bitDepth, channels);
|
|
210
|
+
offset += consumed;
|
|
211
|
+
writePixels(data, width, bytes, stride, pw, ph, colorType, bitDepth, palette, trns, (x) => sx + x * dx, (y) => sy + y * dy);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { width, height, data, text };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function chunk(type, data) {
|
|
218
|
+
const len = Buffer.alloc(4);
|
|
219
|
+
len.writeUInt32BE(data.length, 0);
|
|
220
|
+
const typeBuf = Buffer.from(type, 'latin1');
|
|
221
|
+
const crc = Buffer.alloc(4);
|
|
222
|
+
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
|
223
|
+
return Buffer.concat([len, typeBuf, data, crc]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Encode RGBA8 to PNG. `text` (optional) is a map of tEXt keyword -> value.
|
|
228
|
+
* Uses filter type 0 on every row: comps and screenshots compress fine and the
|
|
229
|
+
* encoder stays trivial.
|
|
230
|
+
*/
|
|
231
|
+
export function encodePng({ width, height, data }, { text = null, level = 6 } = {}) {
|
|
232
|
+
if (data.length !== width * height * 4) throw new Error(`png: data length ${data.length} != ${width}x${height}x4`);
|
|
233
|
+
const stride = width * 4;
|
|
234
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
235
|
+
for (let y = 0; y < height; y++) {
|
|
236
|
+
raw[y * (stride + 1)] = 0;
|
|
237
|
+
raw.set(data.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1);
|
|
238
|
+
}
|
|
239
|
+
const ihdr = Buffer.alloc(13);
|
|
240
|
+
ihdr.writeUInt32BE(width, 0);
|
|
241
|
+
ihdr.writeUInt32BE(height, 4);
|
|
242
|
+
ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
|
243
|
+
const parts = [SIGNATURE, chunk('IHDR', ihdr)];
|
|
244
|
+
if (text) {
|
|
245
|
+
for (const [k, v] of Object.entries(text)) {
|
|
246
|
+
parts.push(chunk('tEXt', Buffer.concat([Buffer.from(k, 'latin1'), Buffer.from([0]), Buffer.from(String(v), 'utf8')])));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
parts.push(chunk('IDAT', zlib.deflateSync(raw, { level })));
|
|
250
|
+
parts.push(chunk('IEND', Buffer.alloc(0)));
|
|
251
|
+
return Buffer.concat(parts);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Read any raster the comp pipeline meets (PNG natively; WebP / JPEG / GIF /
|
|
256
|
+
* AVIF through a converter on PATH) as RGBA. Non-PNG input is converted to a
|
|
257
|
+
* sibling cache file `<name>.<ext>.png` next to the source, never in place:
|
|
258
|
+
* a session that overwrites `comp.webp` with PNG bytes leaves a file the
|
|
259
|
+
* next tool cannot trust and a transcript replay cannot reconstruct.
|
|
260
|
+
* Returns { image, path } where path is the PNG actually decoded.
|
|
261
|
+
*/
|
|
262
|
+
export function loadRaster(file) {
|
|
263
|
+
const buf = fs.readFileSync(file);
|
|
264
|
+
if (isPng(buf)) return { image: decodePng(buf), path: file };
|
|
265
|
+
const cache = `${file}.png`;
|
|
266
|
+
if (fs.existsSync(cache)) {
|
|
267
|
+
try { const b = fs.readFileSync(cache); if (isPng(b)) return { image: decodePng(b), path: cache }; } catch { /* reconvert */ }
|
|
268
|
+
}
|
|
269
|
+
const attempts = [
|
|
270
|
+
['dwebp', [file, '-o', cache]],
|
|
271
|
+
['sips', ['-s', 'format', 'png', file, '--out', cache]],
|
|
272
|
+
['magick', [file, cache]],
|
|
273
|
+
['convert', [file, cache]],
|
|
274
|
+
];
|
|
275
|
+
let lastErr = null;
|
|
276
|
+
for (const [cmd, args] of attempts) {
|
|
277
|
+
try { execFileSync(cmd, args, { stdio: 'ignore' }); const b = fs.readFileSync(cache); if (isPng(b)) return { image: decodePng(b), path: cache }; }
|
|
278
|
+
catch (e) { lastErr = e; }
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`png: ${file} is not a PNG and no converter (dwebp, sips, magick, convert) could produce ${cache}${lastErr ? `: ${lastErr.message}` : ''}`);
|
|
281
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small RGBA raster toolkit shared by the comp-fidelity scripts: crop, resize
|
|
3
|
+
* (area-averaging down, bilinear up), composite, fills, rectangles, and a
|
|
4
|
+
* bitmap-font label so composites can be captioned without a font stack.
|
|
5
|
+
*
|
|
6
|
+
* An image is `{ width, height, data }` with RGBA8 data (Uint8Array).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function createImage(width, height, fill = [0, 0, 0, 0]) {
|
|
10
|
+
const data = new Uint8Array(width * height * 4);
|
|
11
|
+
if (fill[0] || fill[1] || fill[2] || fill[3]) {
|
|
12
|
+
for (let i = 0; i < data.length; i += 4) { data[i] = fill[0]; data[i + 1] = fill[1]; data[i + 2] = fill[2]; data[i + 3] = fill[3]; }
|
|
13
|
+
}
|
|
14
|
+
return { width, height, data };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function clampRect(img, x, y, w, h) {
|
|
18
|
+
const x0 = Math.max(0, Math.min(img.width, Math.round(x)));
|
|
19
|
+
const y0 = Math.max(0, Math.min(img.height, Math.round(y)));
|
|
20
|
+
const x1 = Math.max(x0, Math.min(img.width, Math.round(x + w)));
|
|
21
|
+
const y1 = Math.max(y0, Math.min(img.height, Math.round(y + h)));
|
|
22
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function crop(img, x, y, w, h) {
|
|
26
|
+
const r = clampRect(img, x, y, w, h);
|
|
27
|
+
const out = createImage(Math.max(1, r.w), Math.max(1, r.h));
|
|
28
|
+
for (let yy = 0; yy < r.h; yy++) {
|
|
29
|
+
const src = ((r.y + yy) * img.width + r.x) * 4;
|
|
30
|
+
out.data.set(img.data.subarray(src, src + r.w * 4), yy * out.width * 4);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resize with area averaging when shrinking and bilinear when growing. */
|
|
36
|
+
export function resize(img, width, height) {
|
|
37
|
+
width = Math.max(1, Math.round(width));
|
|
38
|
+
height = Math.max(1, Math.round(height));
|
|
39
|
+
if (width === img.width && height === img.height) return { width, height, data: new Uint8Array(img.data) };
|
|
40
|
+
const out = createImage(width, height);
|
|
41
|
+
const sx = img.width / width, sy = img.height / height;
|
|
42
|
+
if (sx >= 1 && sy >= 1) {
|
|
43
|
+
for (let y = 0; y < height; y++) {
|
|
44
|
+
const y0 = Math.floor(y * sy), y1 = Math.min(img.height, Math.max(y0 + 1, Math.floor((y + 1) * sy)));
|
|
45
|
+
for (let x = 0; x < width; x++) {
|
|
46
|
+
const x0 = Math.floor(x * sx), x1 = Math.min(img.width, Math.max(x0 + 1, Math.floor((x + 1) * sx)));
|
|
47
|
+
let r = 0, g = 0, b = 0, a = 0, n = 0;
|
|
48
|
+
for (let yy = y0; yy < y1; yy++) {
|
|
49
|
+
let p = (yy * img.width + x0) * 4;
|
|
50
|
+
for (let xx = x0; xx < x1; xx++, p += 4) { r += img.data[p]; g += img.data[p + 1]; b += img.data[p + 2]; a += img.data[p + 3]; n++; }
|
|
51
|
+
}
|
|
52
|
+
const o = (y * width + x) * 4;
|
|
53
|
+
out.data[o] = r / n; out.data[o + 1] = g / n; out.data[o + 2] = b / n; out.data[o + 3] = a / n;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
for (let y = 0; y < height; y++) {
|
|
59
|
+
const fy = Math.min(img.height - 1, (y + 0.5) * sy - 0.5);
|
|
60
|
+
const y0 = Math.max(0, Math.floor(fy)), y1 = Math.min(img.height - 1, y0 + 1), wy = fy - y0;
|
|
61
|
+
for (let x = 0; x < width; x++) {
|
|
62
|
+
const fx = Math.min(img.width - 1, (x + 0.5) * sx - 0.5);
|
|
63
|
+
const x0 = Math.max(0, Math.floor(fx)), x1 = Math.min(img.width - 1, x0 + 1), wx = fx - x0;
|
|
64
|
+
const o = (y * width + x) * 4;
|
|
65
|
+
for (let c = 0; c < 4; c++) {
|
|
66
|
+
const p00 = img.data[(y0 * img.width + x0) * 4 + c], p10 = img.data[(y0 * img.width + x1) * 4 + c];
|
|
67
|
+
const p01 = img.data[(y1 * img.width + x0) * 4 + c], p11 = img.data[(y1 * img.width + x1) * 4 + c];
|
|
68
|
+
out.data[o + c] = (p00 * (1 - wx) + p10 * wx) * (1 - wy) + (p01 * (1 - wx) + p11 * wx) * wy;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Scale to fit inside (maxW x maxH) preserving aspect; never upscale unless `allowUpscale`. */
|
|
76
|
+
export function fit(img, maxW, maxH, allowUpscale = false) {
|
|
77
|
+
const s = Math.min(maxW / img.width, maxH / img.height);
|
|
78
|
+
if (s >= 1 && !allowUpscale) return img;
|
|
79
|
+
return resize(img, img.width * s, img.height * s);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Alpha-composite `src` onto `dst` at (x, y). */
|
|
83
|
+
export function blit(dst, src, x, y) {
|
|
84
|
+
x = Math.round(x); y = Math.round(y);
|
|
85
|
+
for (let yy = 0; yy < src.height; yy++) {
|
|
86
|
+
const dy = y + yy; if (dy < 0 || dy >= dst.height) continue;
|
|
87
|
+
for (let xx = 0; xx < src.width; xx++) {
|
|
88
|
+
const dx = x + xx; if (dx < 0 || dx >= dst.width) continue;
|
|
89
|
+
const s = (yy * src.width + xx) * 4, d = (dy * dst.width + dx) * 4;
|
|
90
|
+
const a = src.data[s + 3] / 255;
|
|
91
|
+
if (a >= 1) { dst.data[d] = src.data[s]; dst.data[d + 1] = src.data[s + 1]; dst.data[d + 2] = src.data[s + 2]; dst.data[d + 3] = 255; continue; }
|
|
92
|
+
if (a <= 0) continue;
|
|
93
|
+
const da = dst.data[d + 3] / 255, oa = a + da * (1 - a);
|
|
94
|
+
for (let c = 0; c < 3; c++) dst.data[d + c] = (src.data[s + c] * a + dst.data[d + c] * da * (1 - a)) / (oa || 1);
|
|
95
|
+
dst.data[d + 3] = oa * 255;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function fillRect(img, x, y, w, h, rgba) {
|
|
101
|
+
const r = clampRect(img, x, y, w, h);
|
|
102
|
+
const a = (rgba[3] ?? 255) / 255;
|
|
103
|
+
for (let yy = r.y; yy < r.y + r.h; yy++) {
|
|
104
|
+
for (let xx = r.x; xx < r.x + r.w; xx++) {
|
|
105
|
+
const o = (yy * img.width + xx) * 4;
|
|
106
|
+
if (a >= 1) { img.data[o] = rgba[0]; img.data[o + 1] = rgba[1]; img.data[o + 2] = rgba[2]; img.data[o + 3] = 255; }
|
|
107
|
+
else { for (let c = 0; c < 3; c++) img.data[o + c] = rgba[c] * a + img.data[o + c] * (1 - a); img.data[o + 3] = Math.max(img.data[o + 3], a * 255); }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function strokeRect(img, x, y, w, h, rgba, thickness = 2) {
|
|
113
|
+
fillRect(img, x, y, w, thickness, rgba);
|
|
114
|
+
fillRect(img, x, y + h - thickness, w, thickness, rgba);
|
|
115
|
+
fillRect(img, x, y, thickness, h, rgba);
|
|
116
|
+
fillRect(img, x + w - thickness, y, thickness, h, rgba);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 5x7 bitmap font, uppercase + digits + a little punctuation. Enough for labels.
|
|
120
|
+
const GLYPHS = {
|
|
121
|
+
A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
|
|
122
|
+
B: ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
|
|
123
|
+
C: ['01110', '10001', '10000', '10000', '10000', '10001', '01110'],
|
|
124
|
+
D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
|
|
125
|
+
E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],
|
|
126
|
+
F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'],
|
|
127
|
+
G: ['01110', '10001', '10000', '10111', '10001', '10001', '01111'],
|
|
128
|
+
H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],
|
|
129
|
+
I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
|
|
130
|
+
J: ['00111', '00010', '00010', '00010', '00010', '10010', '01100'],
|
|
131
|
+
K: ['10001', '10010', '10100', '11000', '10100', '10010', '10001'],
|
|
132
|
+
L: ['10000', '10000', '10000', '10000', '10000', '10000', '11111'],
|
|
133
|
+
M: ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
|
|
134
|
+
N: ['10001', '10001', '11001', '10101', '10011', '10001', '10001'],
|
|
135
|
+
O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
|
|
136
|
+
P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
|
|
137
|
+
Q: ['01110', '10001', '10001', '10001', '10101', '10010', '01101'],
|
|
138
|
+
R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
|
|
139
|
+
S: ['01111', '10000', '10000', '01110', '00001', '00001', '11110'],
|
|
140
|
+
T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
|
|
141
|
+
U: ['10001', '10001', '10001', '10001', '10001', '10001', '01110'],
|
|
142
|
+
V: ['10001', '10001', '10001', '10001', '10001', '01010', '00100'],
|
|
143
|
+
W: ['10001', '10001', '10001', '10101', '10101', '10101', '01010'],
|
|
144
|
+
X: ['10001', '10001', '01010', '00100', '01010', '10001', '10001'],
|
|
145
|
+
Y: ['10001', '10001', '01010', '00100', '00100', '00100', '00100'],
|
|
146
|
+
Z: ['11111', '00001', '00010', '00100', '01000', '10000', '11111'],
|
|
147
|
+
0: ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
|
|
148
|
+
1: ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
|
|
149
|
+
2: ['01110', '10001', '00001', '00010', '00100', '01000', '11111'],
|
|
150
|
+
3: ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
|
|
151
|
+
4: ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
|
|
152
|
+
5: ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
|
|
153
|
+
6: ['00110', '01000', '10000', '11110', '10001', '10001', '01110'],
|
|
154
|
+
7: ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
|
|
155
|
+
8: ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
|
|
156
|
+
9: ['01110', '10001', '10001', '01111', '00001', '00010', '01100'],
|
|
157
|
+
' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
|
|
158
|
+
'.': ['00000', '00000', '00000', '00000', '00000', '01100', '01100'],
|
|
159
|
+
':': ['00000', '01100', '01100', '00000', '01100', '01100', '00000'],
|
|
160
|
+
'-': ['00000', '00000', '00000', '11111', '00000', '00000', '00000'],
|
|
161
|
+
'/': ['00001', '00010', '00010', '00100', '01000', '01000', '10000'],
|
|
162
|
+
'%': ['11001', '11010', '00010', '00100', '01000', '01011', '10011'],
|
|
163
|
+
'(': ['00010', '00100', '01000', '01000', '01000', '00100', '00010'],
|
|
164
|
+
')': ['01000', '00100', '00010', '00010', '00010', '00100', '01000'],
|
|
165
|
+
'#': ['01010', '01010', '11111', '01010', '11111', '01010', '01010'],
|
|
166
|
+
'_': ['00000', '00000', '00000', '00000', '00000', '00000', '11111'],
|
|
167
|
+
'?': ['01110', '10001', '00001', '00010', '00100', '00000', '00100'],
|
|
168
|
+
'=': ['00000', '00000', '11111', '00000', '11111', '00000', '00000'],
|
|
169
|
+
'+': ['00000', '00100', '00100', '11111', '00100', '00100', '00000'],
|
|
170
|
+
',': ['00000', '00000', '00000', '00000', '01100', '00100', '01000'],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export function textWidth(text, scale = 2) {
|
|
174
|
+
return text.length * 6 * scale;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Draw uppercase bitmap text. Returns width drawn. */
|
|
178
|
+
export function drawText(img, text, x, y, rgba, scale = 2) {
|
|
179
|
+
let cx = Math.round(x);
|
|
180
|
+
for (const chRaw of String(text).toUpperCase()) {
|
|
181
|
+
const g = GLYPHS[chRaw] || GLYPHS['?'];
|
|
182
|
+
for (let r = 0; r < 7; r++) for (let c = 0; c < 5; c++) if (g[r][c] === '1') fillRect(img, cx + c * scale, y + r * scale, scale, scale, rgba);
|
|
183
|
+
cx += 6 * scale;
|
|
184
|
+
}
|
|
185
|
+
return cx - x;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Draw a label with a background pill. */
|
|
189
|
+
export function drawLabel(img, text, x, y, { fg = [255, 255, 255, 255], bg = [0, 0, 0, 220], scale = 2, pad = 4 } = {}) {
|
|
190
|
+
const w = textWidth(text, scale) + pad * 2, h = 7 * scale + pad * 2;
|
|
191
|
+
fillRect(img, x, y, w, h, bg);
|
|
192
|
+
drawText(img, text, x + pad, y + pad, fg, scale);
|
|
193
|
+
return { w, h };
|
|
194
|
+
}
|
|
@@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) {
|
|
|
96
96
|
.map(entry => entry.item);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
99
|
+
// Rating sets how many tickets a world holds; breadth decides whether it draws
|
|
100
|
+
// at all. A niche world leaves the pool however good it is, keeping its approval
|
|
101
|
+
// for direct briefs. Breadth was split out of rating because the only way to
|
|
102
|
+
// hold a narrow world back used to be calling it marginal, which made "excellent
|
|
103
|
+
// but narrow" unrecordable and corrupted ratings as a calibration signal.
|
|
104
|
+
//
|
|
105
|
+
// Two tickets for a 3-star, one for everything else, was too sharp. Measured
|
|
106
|
+
// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic
|
|
107
|
+
// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and
|
|
108
|
+
// 75% of interaction from 15 of 25. The reviewer's complaint, that the same
|
|
109
|
+
// worlds keep coming back, is what a rating multiplier does to a pool whose
|
|
110
|
+
// thinnest tier holds 25 worlds.
|
|
111
|
+
//
|
|
112
|
+
// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather
|
|
113
|
+
// than not at all. A marginal keep is still worth showing sometimes: the
|
|
114
|
+
// judgement it records is "narrow or unexceptional", not "wrong", and excluding
|
|
115
|
+
// it entirely made a rating do a job breadth already does properly.
|
|
116
|
+
const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 };
|
|
117
|
+
const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2;
|
|
118
|
+
|
|
106
119
|
function challengerTickets(pool) {
|
|
107
120
|
return pool.flatMap(concept => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
|
|
112
|
-
: [{ concept, ticket: 0 }];
|
|
121
|
+
if (concept.review?.breadth === 'niche') return [];
|
|
122
|
+
return Array.from({ length: ticketsForRating(concept.review?.rating) },
|
|
123
|
+
(_, ticket) => ({ concept, ticket }));
|
|
113
124
|
});
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
function compositionTickets(pool) {
|
|
117
|
-
return pool.flatMap(composition =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return rating === 3
|
|
121
|
-
? [{ composition, ticket: 0 }, { composition, ticket: 1 }]
|
|
122
|
-
: [{ composition, ticket: 0 }];
|
|
123
|
-
});
|
|
128
|
+
return pool.flatMap(composition => Array.from(
|
|
129
|
+
{ length: ticketsForRating(composition.review?.rating) },
|
|
130
|
+
(_, ticket) => ({ composition, ticket })));
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
/**
|
|
@@ -117,6 +117,23 @@ export function checkDesignDrift({ designPath, projectRoot, threshold = 25 }) {
|
|
|
117
117
|
* a section can be absent because it never applied, so this is reported as a
|
|
118
118
|
* documentation gap for a human to judge, never as an error.
|
|
119
119
|
*/
|
|
120
|
+
function hasCoverageValue(value) {
|
|
121
|
+
if (Array.isArray(value)) return value.some(hasCoverageValue);
|
|
122
|
+
if (value && typeof value === 'object') {
|
|
123
|
+
return Object.values(value).some(hasCoverageValue);
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === 'string') {
|
|
126
|
+
const trimmed = value.trim();
|
|
127
|
+
return trimmed.length > 0 && !/^(?:\[\s*\]|\{\s*\})$/.test(trimmed);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const SEED_DESIGN_MARKERS = ['/', '$'].map((prefix) =>
|
|
133
|
+
'<!-- SEED: established with the user before implementation; '
|
|
134
|
+
+ `re-run ${prefix}impeccable document once there's code to capture the actual tokens and components. -->`
|
|
135
|
+
);
|
|
136
|
+
|
|
120
137
|
export function checkDesignCoverage({ design, designPath, parseDesignMd }) {
|
|
121
138
|
if (!design || typeof parseDesignMd !== 'function') return [];
|
|
122
139
|
let model;
|
|
@@ -125,8 +142,12 @@ export function checkDesignCoverage({ design, designPath, parseDesignMd }) {
|
|
|
125
142
|
} catch {
|
|
126
143
|
return [];
|
|
127
144
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
145
|
+
const isSeed = SEED_DESIGN_MARKERS.some((marker) => design.includes(marker));
|
|
146
|
+
const requiredSections = isSeed
|
|
147
|
+
? ['colors', 'typography']
|
|
148
|
+
: ['colors', 'typography', 'components'];
|
|
149
|
+
const missing = requiredSections
|
|
150
|
+
.filter((section) => !model[section] && !hasCoverageValue(model.frontmatter?.[section]));
|
|
130
151
|
if (!missing.length) return [];
|
|
131
152
|
return [finding({
|
|
132
153
|
id: 'design-md-coverage',
|
|
@@ -223,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|
|
223
244
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
|
224
245
|
// * legacy unquoted: node .claude/.../hook.mjs
|
|
225
246
|
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
|
226
|
-
// * absolute:
|
|
247
|
+
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
|
248
|
+
// the shell-injection fix; older installs double-quote)
|
|
227
249
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
|
228
250
|
// A quoted path wins; the guard's two occurrences are identical, so the first
|
|
229
251
|
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
|
@@ -234,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
|
|
234
256
|
if (!HOOK_MARKER.test(str)) return null;
|
|
235
257
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
|
236
258
|
if (quoted) return quoted[1];
|
|
259
|
+
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
|
260
|
+
// no regex reassembles that, and the bare fallback would misread a fragment
|
|
261
|
+
// of it, so return null: the caller never asserts on a path it can't parse.
|
|
262
|
+
if (str.includes("'\\''")) return null;
|
|
263
|
+
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
|
264
|
+
if (singleQuoted) return singleQuoted[1];
|
|
237
265
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
|
238
266
|
return bare ? bare[1] : null;
|
|
239
267
|
}
|