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
|
@@ -10,8 +10,27 @@
|
|
|
10
10
|
*
|
|
11
11
|
* node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium]
|
|
12
12
|
* node generate-image.mjs --prompt-file prompt.txt --out mock.png
|
|
13
|
+
* node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png]
|
|
14
|
+
*
|
|
15
|
+
* --ref anchors generation on input image(s) via the edits endpoint: pass a
|
|
16
|
+
* captured screenshot of a representative existing page when comping a new
|
|
17
|
+
* surface for an established world, so the identity comes from the real UI.
|
|
18
|
+
*
|
|
19
|
+
* node generate-image.mjs --plate <region-id> [--spec .impeccable/build/spec.json] [--quality high]
|
|
20
|
+
*
|
|
21
|
+
* --plate produces a shipping raster for one raster region of the measured
|
|
22
|
+
* comp spec (comp-spec.mjs): it crops the region from the approved comp,
|
|
23
|
+
* sends the crop as the reference with the spec's plate prompt (plus any
|
|
24
|
+
* --prompt you add), picks the closest supported output size to the region's
|
|
25
|
+
* aspect, writes the result to the region's `plate` path, embeds the prompt,
|
|
26
|
+
* and scores the plate against the comp crop with comp-diff so a plate that
|
|
27
|
+
* does not read as the region is reported (and, with --min, refused) here,
|
|
28
|
+
* before it lands on the page. In IMPECCABLE_IMAGE_GEN_FAKE mode the plate is
|
|
29
|
+
* the crop itself at 2x, so offline pipelines can walk the plate gate.
|
|
13
30
|
*/
|
|
14
31
|
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
15
34
|
import zlib from 'node:zlib';
|
|
16
35
|
|
|
17
36
|
function arg(name, fallback = null) {
|
|
@@ -181,6 +200,154 @@ function parseSize(sizeStr) {
|
|
|
181
200
|
return [Number(m[1]), Number(m[2])];
|
|
182
201
|
}
|
|
183
202
|
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Plate mode: one raster region of the measured spec -> a shipping plate.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
const plateId = arg('plate');
|
|
207
|
+
let plateCtx = null;
|
|
208
|
+
if (plateId) {
|
|
209
|
+
const { loadSpec, platePrompt, plateReference, SPEC_PATH } = await import('./comp-spec.mjs');
|
|
210
|
+
const { decodePng, encodePng, loadRaster } = await import('./lib/png.mjs');
|
|
211
|
+
const { crop, resize } = await import('./lib/raster.mjs');
|
|
212
|
+
const specPath = arg('spec', SPEC_PATH);
|
|
213
|
+
const spec = loadSpec(specPath);
|
|
214
|
+
if (!spec) { console.error(`generate-image: no spec at ${specPath}; run comp-spec.mjs first`); process.exit(1); }
|
|
215
|
+
const region = spec.regions.find((r) => r.id === plateId);
|
|
216
|
+
if (!region) { console.error(`generate-image: no region ${plateId} in ${specPath}; ids: ${spec.regions.map((r) => r.id).join(', ')}`); process.exit(1); }
|
|
217
|
+
if (region.medium !== 'raster') { console.error(`generate-image: region ${plateId} is ${region.medium}, not a plate; set its kind to plate|image|texture in the regions file`); process.exit(1); }
|
|
218
|
+
let comp;
|
|
219
|
+
try { comp = loadRaster(spec.comp).image; } catch (e) { console.error(`generate-image: cannot read comp ${spec.comp}: ${e.message}`); process.exit(1); }
|
|
220
|
+
const ref = plateReference(comp, spec, region);
|
|
221
|
+
const refPath = path.join(path.dirname(specPath), 'crops', `${region.id}.png`);
|
|
222
|
+
fs.mkdirSync(path.dirname(refPath), { recursive: true });
|
|
223
|
+
fs.writeFileSync(refPath, encodePng(ref, { text: { 'impeccable:crop-of': `${spec.comp}#${region.id}` } }));
|
|
224
|
+
const out = arg('out', region.plate);
|
|
225
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
226
|
+
// Closest supported size to the region's aspect; the page crops the rest
|
|
227
|
+
// with object-fit. The plates gate demands >= 1.5x the region's width
|
|
228
|
+
// (capped at 1536), so a square region wider than 682px cannot ship from
|
|
229
|
+
// 1024x1024: take the 1536-wide landscape frame instead and let cover crop.
|
|
230
|
+
const aspect = region.px.w / region.px.h;
|
|
231
|
+
const needW = Math.min(1536, Math.ceil(region.px.w * 1.5));
|
|
232
|
+
let size = arg('size');
|
|
233
|
+
if (!size) {
|
|
234
|
+
if (aspect > 1.2) size = '1536x1024';
|
|
235
|
+
else if (aspect < 0.83) size = needW > 1024 ? '1536x1024' : '1024x1536';
|
|
236
|
+
else size = needW > 1024 ? '1536x1024' : '1024x1024';
|
|
237
|
+
}
|
|
238
|
+
const extra = arg('prompt') || (arg('prompt-file') ? fs.readFileSync(arg('prompt-file'), 'utf8') : '');
|
|
239
|
+
// Chroma: an ink-on-ground plate (a line drawing, a figure on flat ground)
|
|
240
|
+
// is generated on a flat key color and keyed to alpha, so the page's own
|
|
241
|
+
// ground shows through instead of a second, mismatched paper. Default on
|
|
242
|
+
// for kind plate when the comp region reads as ink over one flat ground;
|
|
243
|
+
// --chroma / --no-chroma force it.
|
|
244
|
+
const wantsChroma = process.argv.includes('--chroma') ? true : process.argv.includes('--no-chroma') ? false : (region.kind === 'plate' && inkOnGround(region));
|
|
245
|
+
const chromaColor = '#00ff00';
|
|
246
|
+
const chromaLine = wantsChroma ? ` Render the artwork on a perfectly flat, uniform bright green background (${chromaColor}) that fills every pixel not covered by the artwork; no paper texture, no vignette, no shadow on the green; the green will be removed and the artwork composited onto the page's own surface.` : '';
|
|
247
|
+
const prompt = [platePrompt(spec, region), extra, chromaLine].filter(Boolean).join(' ');
|
|
248
|
+
plateCtx = { spec, specPath, region, ref, refPath, out, size, prompt, comp, encodePng, resize, chroma: wantsChroma ? chromaColor : null };
|
|
249
|
+
if (process.env.IMPECCABLE_IMAGE_GEN_FAKE) {
|
|
250
|
+
const up = resize(ref, ref.width * 2, ref.height * 2);
|
|
251
|
+
fs.writeFileSync(out, encodePng(up, { text: { 'impeccable:prompt': prompt, 'impeccable:fake': '1' } }));
|
|
252
|
+
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'fake', plate: region.id, refs: [refPath] }, null, 2));
|
|
253
|
+
console.log(`PLATE: ${out} (${up.width}x${up.height}, fake 2x crop of region ${region.id}, $0.00, no API call)`);
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
// fall through to the real call below with the crop as the single --ref
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** A region whose crop is dominated by one ground color with a dark second: ink on ground. */
|
|
260
|
+
function inkOnGround(region) {
|
|
261
|
+
const pal = region.palette || [];
|
|
262
|
+
if (pal.length < 2) return false;
|
|
263
|
+
return pal[0].coverage >= 0.55;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function hexRgb(h) { const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(h); return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [0, 255, 0]; }
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Key a flat color to alpha with a soft edge: pixels within `hard` of the key
|
|
270
|
+
* go fully transparent, within `soft` fade, and green spill on edge pixels is
|
|
271
|
+
* pulled toward the ink color. Writes back in place. Returns keyed fraction.
|
|
272
|
+
*/
|
|
273
|
+
async function keyChroma(file, keyHex) {
|
|
274
|
+
const { decodePng, encodePng } = await import('./lib/png.mjs');
|
|
275
|
+
const img = decodePng(fs.readFileSync(file));
|
|
276
|
+
const [kr, kg, kb] = hexRgb(keyHex);
|
|
277
|
+
// sample the actual key from the corners: generators shift the green
|
|
278
|
+
const corners = [[2, 2], [img.width - 3, 2], [2, img.height - 3], [img.width - 3, img.height - 3]];
|
|
279
|
+
let sr = 0, sg = 0, sb = 0;
|
|
280
|
+
for (const [x, y] of corners) { const p = (y * img.width + x) * 4; sr += img.data[p]; sg += img.data[p + 1]; sb += img.data[p + 2]; }
|
|
281
|
+
const key = [sr / 4, sg / 4, sb / 4];
|
|
282
|
+
const isGreenish = key[1] > 120 && key[1] > key[0] * 1.4 && key[1] > key[2] * 1.4;
|
|
283
|
+
const K = isGreenish ? key : [kr, kg, kb];
|
|
284
|
+
const hard = 60, soft = 120;
|
|
285
|
+
let keyed = 0;
|
|
286
|
+
for (let i = 0; i < img.data.length; i += 4) {
|
|
287
|
+
const r = img.data[i], g = img.data[i + 1], b = img.data[i + 2];
|
|
288
|
+
const d = Math.sqrt((r - K[0]) ** 2 + (g - K[1]) ** 2 + (b - K[2]) ** 2);
|
|
289
|
+
// also treat "greener than both other channels by a margin" as key, for gradients the generator adds
|
|
290
|
+
const greenDom = g > 150 && g - Math.max(r, b) > 60;
|
|
291
|
+
if (d < hard || greenDom) { img.data[i + 3] = 0; keyed++; continue; }
|
|
292
|
+
if (d < soft) {
|
|
293
|
+
const a = (d - hard) / (soft - hard);
|
|
294
|
+
img.data[i + 3] = Math.round(img.data[i + 3] * a);
|
|
295
|
+
// despill: pull green down to the mean of the others on the fringe
|
|
296
|
+
const m = (r + b) / 2; img.data[i + 1] = Math.round(g * a + m * (1 - a));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// keep the tEXt chunks (the embedded prompt written before keying)
|
|
300
|
+
fs.writeFileSync(file, encodePng(img, { text: img.text && Object.keys(img.text).length ? img.text : null }));
|
|
301
|
+
return keyed / (img.data.length / 4);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function scorePlate(ctx, outFile) {
|
|
305
|
+
try {
|
|
306
|
+
const { compare } = await import('./comp-diff.mjs');
|
|
307
|
+
const { decodePng } = await import('./lib/png.mjs');
|
|
308
|
+
let plate = decodePng(fs.readFileSync(outFile));
|
|
309
|
+
// a keyed plate ships over the page ground: composite it over the region's
|
|
310
|
+
// sampled ground before scoring, the way it will show
|
|
311
|
+
if (ctx.chroma) {
|
|
312
|
+
const { createImage, blit } = await import('./lib/raster.mjs');
|
|
313
|
+
const g = (ctx.region.palette && ctx.region.palette[0] && ctx.region.palette[0].hex) || '#ffffff';
|
|
314
|
+
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(g);
|
|
315
|
+
const ground = m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16), 255] : [255, 255, 255, 255];
|
|
316
|
+
const over = createImage(plate.width, plate.height, ground);
|
|
317
|
+
blit(over, plate, 0, 0);
|
|
318
|
+
plate = over;
|
|
319
|
+
}
|
|
320
|
+
// a plate ships under object-fit: cover, so score it the way it will show
|
|
321
|
+
const res = compare({ comp: ctx.ref, build: plate, align: 'cover', kind: ctx.region.kind });
|
|
322
|
+
const s = res.whole;
|
|
323
|
+
const min = arg('min') ? parseFloat(arg('min')) : null;
|
|
324
|
+
const line = `PLATE-SCORE ${ctx.region.id} ${(s.overall * 100).toFixed(0)}% against the comp region (structure ${(s.structure * 100).toFixed(0)}%, color ${(s.color * 100).toFixed(0)}%, detail ${(s.detail * 100).toFixed(0)}%)`;
|
|
325
|
+
console.log(line);
|
|
326
|
+
const { plateVerdict } = await import('./build-phase.mjs');
|
|
327
|
+
const v = plateVerdict(ctx.region, s);
|
|
328
|
+
if (!v.ok) console.log(`PLATE-WARN the plate does not read as region ${ctx.region.id}: ${v.reasons.join('; ')}. Open ${outFile} beside ${ctx.refPath} and regenerate before building on it; the plates gate refuses it as it stands.`);
|
|
329
|
+
if (min != null && s.overall < min) { console.log(`PLATE-REJECTED below --min ${(min * 100).toFixed(0)}%`); process.exit(3); }
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.log(`PLATE-SCORE unavailable: ${e.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A comp written into .impeccable/mocks/ while a direction is dealt but the
|
|
336
|
+
// build phases never started is a comp round happening outside the state
|
|
337
|
+
// file, and every session cut after it resumes with no state to follow. The
|
|
338
|
+
// roll writes .impeccable/build/pending.json; build-phase.mjs start clears
|
|
339
|
+
// it. Refuse mock output until start has run (or --force-mock).
|
|
340
|
+
{
|
|
341
|
+
const outArg = arg('out') || (plateCtx && plateCtx.out) || '';
|
|
342
|
+
const intoMocks = /(^|[\\/])\.impeccable[\\/]mocks[\\/]/.test(outArg) && !/[\\/]decision[\\/]/.test(outArg);
|
|
343
|
+
const pending = fs.existsSync(path.join('.impeccable', 'build', 'pending.json'));
|
|
344
|
+
const state = fs.existsSync(path.join('.impeccable', 'build', 'state.json'));
|
|
345
|
+
if (intoMocks && pending && !state && !process.argv.includes('--force-mock')) {
|
|
346
|
+
console.error(`generate-image: a direction was chosen (concept-seed rolled) but build-phase.mjs start has not run, so this comp would be generated outside the build's state. Run: node ${path.dirname(fileURLToPath(import.meta.url))}/build-phase.mjs start --direction <seed key> --kind <assigned|pick|challenger|canon> first (it opens the comps phase), then generate. --force-mock overrides.`);
|
|
347
|
+
process.exit(4);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
184
351
|
if (process.env.IMPECCABLE_IMAGE_GEN_FAKE) {
|
|
185
352
|
const fakePromptFile = arg('prompt-file');
|
|
186
353
|
const fakePrompt = fakePromptFile ? fs.readFileSync(fakePromptFile, 'utf8') : arg('prompt');
|
|
@@ -204,20 +371,52 @@ if (!key) {
|
|
|
204
371
|
process.exit(1);
|
|
205
372
|
}
|
|
206
373
|
const promptFile = arg('prompt-file');
|
|
207
|
-
const prompt = promptFile ? fs.readFileSync(promptFile, 'utf8') : arg('prompt');
|
|
208
|
-
const out = arg('out');
|
|
374
|
+
const prompt = plateCtx ? plateCtx.prompt : (promptFile ? fs.readFileSync(promptFile, 'utf8') : arg('prompt'));
|
|
375
|
+
const out = plateCtx ? plateCtx.out : arg('out');
|
|
209
376
|
if (!prompt || !out) {
|
|
210
377
|
console.error('generate-image: --prompt (or --prompt-file) and --out are required.');
|
|
211
378
|
process.exit(1);
|
|
212
379
|
}
|
|
213
|
-
const size = arg('size', '1536x1024');
|
|
214
|
-
const quality = arg('quality', 'medium');
|
|
380
|
+
const size = plateCtx ? plateCtx.size : arg('size', '1536x1024');
|
|
381
|
+
const quality = arg('quality', plateCtx ? 'high' : 'medium');
|
|
382
|
+
// Reference images (--ref, repeatable): route through the edits endpoint,
|
|
383
|
+
// which accepts input images. This is how a comp for an established world
|
|
384
|
+
// inherits the real UI's identity from a captured screenshot instead of a
|
|
385
|
+
// prose paraphrase of it; the prompt then describes the NEW surface and the
|
|
386
|
+
// reference carries palette, type, and component character.
|
|
387
|
+
const refs = (() => {
|
|
388
|
+
const found = plateCtx ? [plateCtx.refPath] : [];
|
|
389
|
+
for (let i = 0; i < process.argv.length; i += 1) {
|
|
390
|
+
if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]);
|
|
391
|
+
}
|
|
392
|
+
return found;
|
|
393
|
+
})();
|
|
215
394
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
395
|
+
let response;
|
|
396
|
+
if (refs.length) {
|
|
397
|
+
const form = new FormData();
|
|
398
|
+
form.append('model', 'gpt-image-2');
|
|
399
|
+
form.append('prompt', prompt);
|
|
400
|
+
form.append('size', size);
|
|
401
|
+
form.append('quality', quality);
|
|
402
|
+
form.append('n', '1');
|
|
403
|
+
for (const ref of refs) {
|
|
404
|
+
const bytes = fs.readFileSync(ref);
|
|
405
|
+
const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg';
|
|
406
|
+
form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop());
|
|
407
|
+
}
|
|
408
|
+
response = await fetch('https://api.openai.com/v1/images/edits', {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
411
|
+
body: form,
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
response = await fetch('https://api.openai.com/v1/images/generations', {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' },
|
|
417
|
+
body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
221
420
|
if (!response.ok) {
|
|
222
421
|
console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`);
|
|
223
422
|
process.exit(1);
|
|
@@ -232,9 +431,17 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
|
|
|
232
431
|
// The prompt travels with the asset: embedded in the file itself (EXIF-class
|
|
233
432
|
// metadata via embed-prompt.mjs) so intent survives copies across harnesses,
|
|
234
433
|
// plus a sidecar for anything that indexes rather than opens the image.
|
|
434
|
+
let embedded = false;
|
|
235
435
|
try {
|
|
236
436
|
const { spawnSync } = await import('node:child_process');
|
|
237
|
-
spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url)
|
|
238
|
-
|
|
437
|
+
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' });
|
|
438
|
+
embedded = !result.error && result.status === 0;
|
|
439
|
+
if (!embedded) console.warn('generate-image: failed to embed prompt in the image');
|
|
440
|
+
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
|
|
239
441
|
} catch { /* embedding is best-effort */ }
|
|
240
|
-
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
|
|
442
|
+
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`);
|
|
443
|
+
if (plateCtx && plateCtx.chroma) {
|
|
444
|
+
const frac = await keyChroma(out, plateCtx.chroma);
|
|
445
|
+
console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain <img> over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`);
|
|
446
|
+
}
|
|
447
|
+
if (plateCtx) await scorePlate(plateCtx, out);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* node hook-admin.mjs off # set enabled: false
|
|
11
11
|
* node hook-admin.mjs ignore-rule <rule-id> # append to ignoreRules
|
|
12
12
|
* node hook-admin.mjs ignore-rule overused-font --all-values
|
|
13
|
-
* node hook-admin.mjs ignore-file <glob>
|
|
13
|
+
* node hook-admin.mjs ignore-file <glob> [--shared|--local] # append to ignoreFiles
|
|
14
14
|
* node hook-admin.mjs ignore-value <rule> <value> # append to shared ignoreValues
|
|
15
15
|
* node hook-admin.mjs ignore-value <rule> <value> --local
|
|
16
16
|
* node hook-admin.mjs ignore-value <rule> "*" --file <glob> # rule off in <glob> only
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
ensureHookGitExcludes,
|
|
36
36
|
normalizeIgnoreValue,
|
|
37
37
|
normalizeIgnoreValueEntries,
|
|
38
|
+
extractFindingIgnoreValue,
|
|
38
39
|
} from './hook-lib.mjs';
|
|
39
40
|
|
|
40
41
|
const ACTIONS = new Set(['status', 'on', 'off', 'ignore-rule', 'ignore-file', 'ignore-value', 'reset']);
|
|
@@ -75,11 +76,11 @@ const HOOK_MANIFEST_TARGETS = [
|
|
|
75
76
|
destRel: '.claude/settings.local.json',
|
|
76
77
|
sharedDestRel: '.claude/settings.json',
|
|
77
78
|
manifest: () => ({
|
|
78
|
-
description: 'Impeccable design detector: immediate-tier checks after Edit/Write
|
|
79
|
+
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
|
79
80
|
hooks: {
|
|
80
81
|
PostToolUse: [
|
|
81
82
|
{
|
|
82
|
-
matcher: 'Edit|Write
|
|
83
|
+
matcher: 'Edit|Write',
|
|
83
84
|
hooks: [
|
|
84
85
|
{
|
|
85
86
|
type: 'command',
|
|
@@ -166,7 +167,7 @@ function readRawConfigFile(filePath) {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem']);
|
|
170
|
+
const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem', 'advisoryRules']);
|
|
170
171
|
|
|
171
172
|
function hookSection(unified) {
|
|
172
173
|
return unified && typeof unified === 'object' && !Array.isArray(unified) && unified.hook && typeof unified.hook === 'object' && !Array.isArray(unified.hook)
|
|
@@ -200,6 +201,15 @@ function stripDetectorKeys(raw) {
|
|
|
200
201
|
return out;
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
function pickDetectorKeys(raw) {
|
|
205
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
206
|
+
const out = {};
|
|
207
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
208
|
+
if (DETECTOR_CONFIG_KEYS.has(key)) out[key] = value;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
203
213
|
// Write hook runtime config under `hook`, leaving detector filters in
|
|
204
214
|
// `detector` and preserving sibling keys such as updateCheck.
|
|
205
215
|
function writeHookConfig(cwd, hookConfig, opts = {}) {
|
|
@@ -207,10 +217,19 @@ function writeHookConfig(cwd, hookConfig, opts = {}) {
|
|
|
207
217
|
if (opts.local) ensureHookGitExcludes(cwd);
|
|
208
218
|
const existingRaw = readRawConfigFile(filePath).raw;
|
|
209
219
|
const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
|
|
210
|
-
const
|
|
220
|
+
const existingHookSection = hookSection(existing);
|
|
221
|
+
const existingHook = stripDetectorKeys(existingHookSection);
|
|
222
|
+
const legacyDetector = pickDetectorKeys(existingHookSection);
|
|
211
223
|
// Merge over the existing hook object so fields the merge helpers don't manage
|
|
212
224
|
// (consent, quiet, auditLog) survive an Impeccable hooks edit.
|
|
213
225
|
const next = { ...existing, hook: { ...existingHook, ...hookConfig } };
|
|
226
|
+
if (Object.keys(legacyDetector).length > 0) {
|
|
227
|
+
const existingDetector = detectorSection(existing) || {};
|
|
228
|
+
next.detector = {
|
|
229
|
+
...existingDetector,
|
|
230
|
+
...mergeDetectorConfig(existingDetector, mergeDetectorConfig(legacyDetector)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
214
233
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
215
234
|
fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');
|
|
216
235
|
return filePath;
|
|
@@ -222,10 +241,14 @@ function writeDetectorConfig(cwd, detectorConfig, opts = {}) {
|
|
|
222
241
|
const existingRaw = readRawConfigFile(filePath).raw;
|
|
223
242
|
const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
|
|
224
243
|
const nextHook = stripDetectorKeys(hookSection(existing));
|
|
225
|
-
const
|
|
244
|
+
const existingDetectorSection = detectorSection(existing) || {};
|
|
245
|
+
const existingDetector = mergeDetectorConfig(existingDetectorSection);
|
|
226
246
|
const next = {
|
|
227
247
|
...existing,
|
|
228
|
-
detector:
|
|
248
|
+
detector: {
|
|
249
|
+
...existingDetectorSection,
|
|
250
|
+
...mergeDetectorConfig(detectorConfig, existingDetector),
|
|
251
|
+
},
|
|
229
252
|
};
|
|
230
253
|
if (Object.keys(nextHook).length > 0) next.hook = nextHook;
|
|
231
254
|
else delete next.hook;
|
|
@@ -259,12 +282,18 @@ function mergeDetectorConfig(existing, seed = null) {
|
|
|
259
282
|
if (seed?.designSystem && typeof seed.designSystem === 'object' && !Array.isArray(seed.designSystem)) {
|
|
260
283
|
out.designSystem = { ...seed.designSystem };
|
|
261
284
|
}
|
|
285
|
+
if (seed?.advisoryRules === 'include' || seed?.advisoryRules === 'exclude') {
|
|
286
|
+
out.advisoryRules = seed.advisoryRules;
|
|
287
|
+
}
|
|
262
288
|
if (base.designSystem && typeof base.designSystem === 'object' && !Array.isArray(base.designSystem)) {
|
|
263
289
|
out.designSystem = {
|
|
264
290
|
...(out.designSystem || {}),
|
|
265
291
|
enabled: base.designSystem.enabled === false ? false : true,
|
|
266
292
|
};
|
|
267
293
|
}
|
|
294
|
+
if (base.advisoryRules === 'include' || base.advisoryRules === 'exclude') {
|
|
295
|
+
out.advisoryRules = base.advisoryRules;
|
|
296
|
+
}
|
|
268
297
|
if (Array.isArray(base.ignoreRules)) {
|
|
269
298
|
out.ignoreRules = Array.from(new Set([...out.ignoreRules, ...base.ignoreRules.map(String)]));
|
|
270
299
|
}
|
|
@@ -558,12 +587,44 @@ function addIgnoreRule(cwd, args) {
|
|
|
558
587
|
return `Added "${rule}" to detector.ignoreRules. Current: ${config.ignoreRules.join(', ')}`;
|
|
559
588
|
}
|
|
560
589
|
|
|
561
|
-
function
|
|
590
|
+
function parseIgnoreFileArgs(args) {
|
|
591
|
+
const positionals = [];
|
|
592
|
+
let shared = false;
|
|
593
|
+
let local = false;
|
|
594
|
+
|
|
595
|
+
for (const raw of args) {
|
|
596
|
+
const arg = String(raw || '');
|
|
597
|
+
if (arg === '--shared') {
|
|
598
|
+
shared = true;
|
|
599
|
+
} else if (arg === '--local') {
|
|
600
|
+
local = true;
|
|
601
|
+
} else if (arg === '--reason' || arg.startsWith('--reason=')) {
|
|
602
|
+
throw new Error('--reason is not supported for ignore-file because detector.ignoreFiles stores globs only; use ignore-value when a documented rule-specific exception fits');
|
|
603
|
+
} else if (arg.startsWith('--')) {
|
|
604
|
+
throw new Error(`Unknown ignore-file flag: ${arg}`);
|
|
605
|
+
} else {
|
|
606
|
+
positionals.push(arg);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (shared && local) throw new Error('Pass only one scope flag: --shared or --local');
|
|
611
|
+
if (positionals.length > 1) throw new Error('Pass exactly one glob to ignore-file');
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
glob: positionals[0],
|
|
615
|
+
local,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function addIgnoreFile(cwd, args) {
|
|
620
|
+
const parsed = parseIgnoreFileArgs(args);
|
|
621
|
+
const glob = parsed.glob;
|
|
562
622
|
if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`);
|
|
563
|
-
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
623
|
+
const config = mergeDetectorConfig(readRawDetectorConfig(cwd, { local: parsed.local }));
|
|
564
624
|
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
|
|
565
|
-
writeDetectorConfig(cwd, config);
|
|
566
|
-
|
|
625
|
+
const target = writeDetectorConfig(cwd, config, { local: parsed.local });
|
|
626
|
+
const scope = parsed.local ? 'local detector.ignoreFiles' : 'shared detector.ignoreFiles';
|
|
627
|
+
return `Added "${glob}" to ${scope} (${path.relative(cwd, target) || target}). Current: ${config.ignoreFiles.join(', ')}`;
|
|
567
628
|
}
|
|
568
629
|
|
|
569
630
|
// An empty glob used to be dropped by filter(Boolean), so `--file=` reported
|
|
@@ -653,6 +714,10 @@ function addIgnoreValue(cwd, args) {
|
|
|
653
714
|
throw new Error(`Wildcard value ignores must be scoped with --file <glob>, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value design-system-font-size "*" --file "src/widget.js". To suppress the rule project-wide use ${projectWide}.`);
|
|
654
715
|
}
|
|
655
716
|
|
|
717
|
+
if (parsed.value !== '*' && !extractFindingIgnoreValue({ antipattern: parsed.rule, ignoreValue: parsed.value })) {
|
|
718
|
+
throw new Error(`${parsed.rule} has no extractable ignore value. Use ${IMPECCABLE_COMMAND} hooks ignore-value ${parsed.rule} "*" --file <glob> to suppress it in matching files.`);
|
|
719
|
+
}
|
|
720
|
+
|
|
656
721
|
const local = parsed.local;
|
|
657
722
|
const config = mergeDetectorConfig(readRawDetectorConfig(cwd, { local }));
|
|
658
723
|
// Key on the file scope too: the same rule/value legitimately appears more than
|
|
@@ -705,9 +770,22 @@ function reset(cwd) {
|
|
|
705
770
|
}
|
|
706
771
|
} catch { /* ignore */ }
|
|
707
772
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
773
|
+
// `on` writes three things: config, consent, and hook entries in the
|
|
774
|
+
// provider manifests. Reset must undo all three (issue #512): a leftover
|
|
775
|
+
// manifest entry kept invoking the hook after the config that said "off"
|
|
776
|
+
// was deleted. Local destRel only, since `on` never writes the team-shared
|
|
777
|
+
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
|
778
|
+
// gone, manifest still wired) is the case that most needs the prune.
|
|
779
|
+
const pruned = [];
|
|
780
|
+
for (const target of HOOK_MANIFEST_TARGETS) {
|
|
781
|
+
try {
|
|
782
|
+
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
|
783
|
+
} catch { /* ignore */ }
|
|
784
|
+
}
|
|
785
|
+
const parts = [];
|
|
786
|
+
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
|
787
|
+
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
|
788
|
+
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
|
711
789
|
}
|
|
712
790
|
|
|
713
791
|
function main() {
|
|
@@ -727,7 +805,7 @@ function main() {
|
|
|
727
805
|
case 'on': out = setEnabled(cwd, true); break;
|
|
728
806
|
case 'off': out = setEnabled(cwd, false); break;
|
|
729
807
|
case 'ignore-rule': out = addIgnoreRule(cwd, rest); break;
|
|
730
|
-
case 'ignore-file': out = addIgnoreFile(cwd, rest
|
|
808
|
+
case 'ignore-file': out = addIgnoreFile(cwd, rest); break;
|
|
731
809
|
case 'ignore-value': out = addIgnoreValue(cwd, rest); break;
|
|
732
810
|
case 'reset': out = reset(cwd); break;
|
|
733
811
|
}
|
|
@@ -16,13 +16,18 @@ import path from 'node:path';
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
ALLOWED_EXTS,
|
|
19
|
+
DEFAULT_CONFIG,
|
|
19
20
|
EDIT_COUNT_THRESHOLD,
|
|
20
21
|
GENERATED_PATH,
|
|
21
22
|
SENSITIVE_PATH,
|
|
22
|
-
|
|
23
|
+
appendDesignSystemNoteOnce,
|
|
24
|
+
commitFooterShown,
|
|
25
|
+
designNoteReserve,
|
|
23
26
|
designSystemOptions,
|
|
27
|
+
footerModeForSession,
|
|
24
28
|
filterFindings,
|
|
25
29
|
isNativePlatform,
|
|
30
|
+
isScanTargetInsideProject,
|
|
26
31
|
loadDetector,
|
|
27
32
|
matchConfiguredExtension,
|
|
28
33
|
matchesAnyGlob,
|
|
@@ -161,7 +166,7 @@ function replaceOnce(original, oldString, newString) {
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
function readExistingProjectFile(filePath, cwd) {
|
|
164
|
-
if (!
|
|
169
|
+
if (!isScanTargetInsideProject(filePath, cwd)) return null;
|
|
165
170
|
if (SENSITIVE_PATH.test(filePath) || GENERATED_PATH.test(filePath)) return null;
|
|
166
171
|
try {
|
|
167
172
|
const stat = fs.statSync(filePath);
|
|
@@ -232,7 +237,7 @@ function shellCopiedFileContent(command, cwd) {
|
|
|
232
237
|
const source = shellCopyPaths(command)?.source;
|
|
233
238
|
if (!source) return '';
|
|
234
239
|
const sourcePath = path.isAbsolute(source) ? source : path.resolve(cwd, source);
|
|
235
|
-
if (!
|
|
240
|
+
if (!isScanTargetInsideProject(sourcePath, cwd)) return '';
|
|
236
241
|
if (SENSITIVE_PATH.test(sourcePath) || GENERATED_PATH.test(sourcePath)) return '';
|
|
237
242
|
try {
|
|
238
243
|
const stat = fs.statSync(sourcePath);
|
|
@@ -328,15 +333,6 @@ function relativePath(filePath, cwd) {
|
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
|
|
331
|
-
function isInsideProject(filePath, cwd) {
|
|
332
|
-
try {
|
|
333
|
-
const rel = path.relative(cwd, filePath);
|
|
334
|
-
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
335
|
-
} catch {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
336
|
// The static HTML engine reads its input from disk, but preToolUse only has
|
|
341
337
|
// the proposed content. Stage it in a temp file so html-engine targets get the
|
|
342
338
|
// same DOM-structural rules pre-write that runHook applies post-edit.
|
|
@@ -353,13 +349,32 @@ async function detectProposedHtml(detector, content, filePath, scanOptions) {
|
|
|
353
349
|
}
|
|
354
350
|
}
|
|
355
351
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
352
|
+
// Cursor caps deny messages around 4000 chars. The cap feeds through the
|
|
353
|
+
// renderer's clamp, which preserves the policy footer, rather than tail-
|
|
354
|
+
// slicing the rendered text, which cut the footer off any message the
|
|
355
|
+
// default 8000-char budget let past 4000.
|
|
356
|
+
const CURSOR_DENY_LIMIT = 4000;
|
|
357
|
+
const BLOCK_PREFIX = 'Impeccable design hook blocked this write before it landed. ';
|
|
358
|
+
|
|
359
|
+
function cursorBlockMessage(findings, filePath, config, cwd, footerMode, reserveChars) {
|
|
360
|
+
const limits = config?.limits || DEFAULT_CONFIG.limits;
|
|
361
|
+
// Charge the prefix via reserveChars, not by subtracting from maxChars:
|
|
362
|
+
// renderTemplate's 500-char floor re-raises any maxChars pushed below it,
|
|
363
|
+
// un-charging a prefix subtracted from maxChars (Greptile P1 on PR #508).
|
|
364
|
+
// reserveChars comes off after the floor, so the prefix is charged at every
|
|
365
|
+
// config tier and the final prefixed message plus a pending staleness note
|
|
366
|
+
// fits the binding limit. Default-config output is byte-identical.
|
|
367
|
+
const budget = Math.min(
|
|
368
|
+
limits.maxChars || DEFAULT_CONFIG.limits.maxChars,
|
|
369
|
+
CURSOR_DENY_LIMIT,
|
|
370
|
+
);
|
|
371
|
+
const rendered = renderTemplate(findings, filePath,
|
|
372
|
+
{ ...config, limits: { ...limits, maxChars: budget } },
|
|
373
|
+
{ cwd, footer: footerMode, reserveChars: (reserveChars || 0) + BLOCK_PREFIX.length });
|
|
374
|
+
return rendered.replace(
|
|
359
375
|
'[impeccable@1] Design hook findings requiring review',
|
|
360
|
-
|
|
376
|
+
`[impeccable@1] ${BLOCK_PREFIX}Design hook findings requiring review`,
|
|
361
377
|
);
|
|
362
|
-
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
|
363
378
|
}
|
|
364
379
|
|
|
365
380
|
function findingSignature(findings) {
|
|
@@ -414,7 +429,7 @@ async function main() {
|
|
|
414
429
|
};
|
|
415
430
|
|
|
416
431
|
if (!filePath) return allow({ ...audit, skipped: 'no-file-path', durationMs: Date.now() - started });
|
|
417
|
-
if (!
|
|
432
|
+
if (!isScanTargetInsideProject(filePath, cwd)) return allow({ ...audit, skipped: 'outside-project', durationMs: Date.now() - started });
|
|
418
433
|
if (SENSITIVE_PATH.test(filePath)) return allow({ ...audit, skipped: 'sensitive', durationMs: Date.now() - started });
|
|
419
434
|
if (GENERATED_PATH.test(filePath)) return allow({ ...audit, skipped: 'generated', durationMs: Date.now() - started });
|
|
420
435
|
|
|
@@ -476,9 +491,16 @@ async function main() {
|
|
|
476
491
|
});
|
|
477
492
|
}
|
|
478
493
|
|
|
479
|
-
const message = appendDesignSystemNote(cursorBlockMessage(filtered, filePath, config, cwd), scanOptions);
|
|
480
494
|
const sessionId = event.session_id || event.conversation_id || 'unknown';
|
|
481
495
|
const cache = readCache(cwd);
|
|
496
|
+
// Repeated denials for the same session repeat the findings, not the
|
|
497
|
+
// policy: the full footer emits once per session, the short form after.
|
|
498
|
+
const footerMode = footerModeForSession(cache, sessionId);
|
|
499
|
+
const message = appendDesignSystemNoteOnce(
|
|
500
|
+
cursorBlockMessage(filtered, filePath, config, cwd, footerMode, designNoteReserve(scanOptions, cache, sessionId)),
|
|
501
|
+
scanOptions, cache, sessionId, config,
|
|
502
|
+
);
|
|
503
|
+
commitFooterShown(cache, sessionId, message);
|
|
482
504
|
const denial = bumpCursorDenial(cache, sessionId, filePath, filtered);
|
|
483
505
|
persistCache(cwd, cache);
|
|
484
506
|
if (denial.count > EDIT_COUNT_THRESHOLD) {
|