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
@@ -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
- // Two independent exclusions, and either one is enough to hold a world back.
100
- // Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep
101
- // leaves the pool. Breadth says whether a world can serve an arbitrary build at
102
- // all, so a niche world leaves however good it is, keeping its approval for
103
- // direct briefs. Breadth was split out of rating because the only way to hold a
104
- // narrow world back used to be calling it marginal, which made "excellent but
105
- // narrow" unrecordable and corrupted ratings as a calibration signal.
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
- const rating = concept.review?.rating;
109
- if (rating === 1 || concept.review?.breadth === 'niche') return [];
110
- return rating === 3
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
- const rating = composition.review?.rating;
119
- if (rating === 1) return [];
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 missing = ['colors', 'typography', 'components']
129
- .filter((section) => !model[section]);
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: node "/Users/.../hook.mjs" (user-level installs)
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
  }