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
@@ -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
- const response = await fetch('https://api.openai.com/v1/images/generations', {
217
- method: 'POST',
218
- headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' },
219
- body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }),
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).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
238
- fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2));
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> # append to ignoreFiles
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/MultiEdit on UI files, full-rule deep pass on Stop.',
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|MultiEdit',
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 existingHook = stripDetectorKeys(hookSection(existing));
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 existingDetector = mergeDetectorConfig(detectorSection(existing));
244
+ const existingDetectorSection = detectorSection(existing) || {};
245
+ const existingDetector = mergeDetectorConfig(existingDetectorSection);
226
246
  const next = {
227
247
  ...existing,
228
- detector: mergeDetectorConfig(detectorConfig, existingDetector),
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 addIgnoreFile(cwd, glob) {
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
- return `Added "${glob}" to detector.ignoreFiles. Current: ${config.ignoreFiles.join(', ')}`;
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
- return removed.length
709
- ? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
710
- : 'No hook config or cache to remove. Already at defaults.';
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[0]); break;
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
- appendDesignSystemNote,
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 (!isInsideProject(filePath, cwd)) return null;
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 (!isInsideProject(sourcePath, cwd)) return '';
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
- function cursorBlockMessage(findings, filePath, config, cwd) {
357
- const rendered = renderTemplate(findings, filePath, config, { cwd });
358
- const blocked = rendered.replace(
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
- '[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
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 (!isInsideProject(filePath, cwd)) return allow({ ...audit, skipped: 'outside-project', durationMs: Date.now() - started });
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) {