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.
Files changed (121) hide show
  1. package/dist/cli.js +237 -10
  2. package/package.json +1 -1
  3. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  4. package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
  5. package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
  6. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
  7. package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
  8. package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
  9. package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
  10. package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
  11. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
  12. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
  13. package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
  14. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
  15. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
  16. package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
  17. package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
  18. package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
  19. package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
  21. package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
  22. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
  23. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
  24. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  25. package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
  26. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  27. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  28. package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
  29. package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
  30. package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
  31. package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
  32. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  33. package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
  34. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  35. package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
  36. package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
  37. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
  38. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
  39. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
  40. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
  41. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
  42. package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
  43. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
  44. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
  45. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
  46. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
  47. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
  48. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
  49. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
  50. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
  51. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  52. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
  53. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
  54. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
  55. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
  56. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
  57. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
  58. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
  59. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
  60. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
  61. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
  62. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
  63. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
  64. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
  65. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
  66. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
  67. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
  68. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
  69. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
  70. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
  71. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
  72. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
  73. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
  74. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
  76. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
  77. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
  78. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
  79. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
  85. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
  86. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
  88. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
  89. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
  90. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
  91. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  92. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
  93. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
  95. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
  96. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  97. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
  98. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
  99. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
  100. package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
  101. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
  102. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
  103. package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
  104. package/template/.agents/skills/hono/SKILL.md +17 -3
  105. package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
  106. package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
  107. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
  108. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
  109. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
  110. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
  111. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
  112. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
  113. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
  114. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
  115. package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
  116. package/template/.agents/skills/supabase/SKILL.md +5 -1
  117. package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
  118. package/template/.agents/skills/writing/humanizer/README.md +209 -0
  119. package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
  120. package/template/.agents/upstreams.json +21 -22
  121. package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
@@ -47,18 +47,33 @@ import {
47
47
 
48
48
  // Top-level keys any reader honors: `hook` and `detector` subtrees (hook-lib's
49
49
  // readConfig), `updateCheck` (context.mjs), `projectRoots` (context.mjs's
50
- // monorepo resolution), plus `stalenessCheck` below. `$schema` and `version`
51
- // are allowed as conventional metadata nobody reads.
50
+ // monorepo resolution), `buildPath` (context.mjs's build-path directive), plus
51
+ // `stalenessCheck` below. `$schema` and `version` are allowed as conventional
52
+ // metadata nobody reads.
52
53
  const KNOWN_CONFIG_KEYS = new Set([
53
54
  'hook',
54
55
  'detector',
55
56
  'updateCheck',
56
57
  'stalenessCheck',
57
58
  'projectRoots',
59
+ 'buildPath',
58
60
  '$schema',
59
61
  'version',
60
62
  ]);
61
63
 
64
+ // The only two values context.mjs and new-work honor. A near miss reads as a
65
+ // working preference and silently rides the opposite path, so it is worth
66
+ // reporting rather than coercing.
67
+ const BUILD_PATH_VALUES = Object.freeze(['comp', 'code']);
68
+
69
+ // Evidence that this project does the kind of work `buildPath` governs. A
70
+ // project that only ever ran polish or audit has no use for the setting and
71
+ // should never be told it exists. Two stats, so Tier 1 can afford it.
72
+ const DIRECTION_WORK_PATHS = Object.freeze([
73
+ path.join('.impeccable', 'surfaces'),
74
+ path.join('.impeccable', 'mocks', 'decision'),
75
+ ]);
76
+
62
77
  // `detector` is a closed set, so a typo here is worth reporting. `hook` is not
63
78
  // checked: it carries runtime settings from several writers and the false
64
79
  // positive rate would outweigh the catch.
@@ -325,6 +340,20 @@ export function checkConfig({ projectRoot, repoRoot }) {
325
340
  }));
326
341
  }
327
342
 
343
+ if (Object.prototype.hasOwnProperty.call(raw, 'buildPath')
344
+ && !BUILD_PATH_VALUES.includes(raw.buildPath)) {
345
+ findings.push(finding({
346
+ id: 'config-invalid-build-path',
347
+ artifact: 'config.json',
348
+ filePath: rel,
349
+ severity: 'mention',
350
+ summary: `${rel} sets \`buildPath\` to ${JSON.stringify(raw.buildPath)}, which nothing reads. `
351
+ + `The values are ${BUILD_PATH_VALUES.map((value) => `\`${value}\``).join(' and ')}.`,
352
+ fix: 'Report the value. An unread `buildPath` does not fall back to the other path; '
353
+ + 'it falls back to the default, so a project meaning `code` has been building comp-led.',
354
+ }));
355
+ }
356
+
328
357
  const detector = raw.detector;
329
358
  if (detector && typeof detector === 'object' && !Array.isArray(detector)) {
330
359
  const unknownDetector = Object.keys(detector).filter((key) => !KNOWN_DETECTOR_KEYS.has(key));
@@ -345,6 +374,47 @@ export function checkConfig({ projectRoot, repoRoot }) {
345
374
  return findings;
346
375
  }
347
376
 
377
+ /**
378
+ * No recorded build-path preference on a project that plainly does visual
379
+ * direction work. Not drift in the usual sense: the setting is newer than the
380
+ * project, so every project that predates it lands here at once. That is why
381
+ * it is gated twice, on a product record and on evidence of the work the
382
+ * setting governs, and why it says the choice rather than assuming a harness
383
+ * can make it. Image generation is the real precondition and this module
384
+ * cannot see it: a harness-native image tool leaves no trace on disk, so the
385
+ * finding hands the question to the one reader that knows.
386
+ */
387
+ export function checkBuildPathUnset({ projectRoot, repoRoot, product }) {
388
+ if (!projectRoot || !product) return [];
389
+ const roots = [...new Set([projectRoot, repoRoot].filter(Boolean).map((root) => path.resolve(root)))];
390
+
391
+ for (const root of roots) {
392
+ for (const name of ['config.json', 'config.local.json']) {
393
+ const raw = readJson(path.join(root, '.impeccable', name));
394
+ // Any declared value ends this, valid or not: an invalid one already has
395
+ // its own finding and two reports of one key is noise.
396
+ if (raw && Object.prototype.hasOwnProperty.call(raw, 'buildPath')) return [];
397
+ }
398
+ }
399
+
400
+ const evidence = DIRECTION_WORK_PATHS.filter((rel) => fs.existsSync(path.join(projectRoot, rel)));
401
+ if (!evidence.length) return [];
402
+
403
+ return [finding({
404
+ id: 'config-build-path-unset',
405
+ artifact: 'config.json',
406
+ filePath: '.impeccable/config.json',
407
+ severity: 'mention',
408
+ summary: 'This project has run visual direction work but records no `buildPath`, '
409
+ + 'so every direction round takes the comp-first default without anyone having chosen it.',
410
+ fix: 'Only when image generation exists in your tool surface, offer the choice once: '
411
+ + '**comp-first** (an image sets the bar before any code; bolder composition, slower) or '
412
+ + '**code-first** (build directly; ambition carried by the direction contract; leaner, faster). '
413
+ + 'Write the answer to `.impeccable/config.json` as `"buildPath": "comp"` or `"buildPath": "code"`, '
414
+ + 'merging with the keys already there. Without image generation there is no choice to record: stay silent.',
415
+ })];
416
+ }
417
+
348
418
  // ─── Surface briefs ────────────────────────────────────────────────────────
349
419
 
350
420
  /**
@@ -418,40 +488,46 @@ export function describeWorkspaceContext(candidates = []) {
418
488
  // ─── Tier 1 orchestration ──────────────────────────────────────────────────
419
489
 
420
490
  /**
421
- * Everything a boot can afford. `ctx` is the loadContext result; `extras`
422
- * carries values the caller already computed so nothing is recomputed here.
491
+ * Everything a boot can afford, grouped by artifact so deeper reports can
492
+ * interleave their own checks without rebuilding this policy. `ctx` is the
493
+ * loadContext result; `extras` carries values the caller already computed so
494
+ * nothing is recomputed here.
423
495
  */
424
- export function collectBootFindings(ctx, extras = {}) {
425
- if (!ctx) return [];
496
+ export function collectBootFindingGroups(ctx, extras = {}) {
497
+ if (!ctx) return {};
426
498
  const projectRoot = ctx.projectRoot || process.cwd();
427
- const absProductPath = extras.absProductPath || null;
428
499
  const absDesignPath = extras.absDesignPath || null;
429
500
 
430
- return [
431
- ...checkProduct(ctx.product, ctx.productPath || 'PRODUCT.md'),
501
+ return {
502
+ product: checkProduct(ctx.product, ctx.productPath || 'PRODUCT.md'),
432
503
  // Only checked once a PRODUCT.md exists. Without one the boot already
433
504
  // emits NO_PRODUCT_MD and routes into init, which asks for the platform
434
505
  // directly; a second signal saying the same thing is noise.
435
- ...(ctx.product
506
+ nativePlatform: ctx.product
436
507
  ? checkNativePlatformEvidence({
437
508
  projectRoot,
438
509
  platform: ctx.platform,
439
510
  product: ctx.product,
440
511
  productPath: ctx.productPath,
441
512
  })
442
- : []),
443
- ...checkDesignSidecar({
513
+ : [],
514
+ designSidecar: checkDesignSidecar({
444
515
  designPath: absDesignPath,
445
516
  sidecarCandidates: extras.sidecarCandidates || [],
446
517
  projectRoot,
447
518
  }),
448
- ...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
449
- ...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
450
- ...(extras.projectRootPatterns
519
+ config: checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
520
+ buildPath: checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
521
+ surfaceBriefs: checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
522
+ projectRoots: extras.projectRootPatterns
451
523
  ? checkProjectRoots({
452
524
  patterns: extras.projectRootPatterns,
453
525
  candidates: extras.targetCandidates || [],
454
526
  })
455
- : []),
456
- ];
527
+ : [],
528
+ };
529
+ }
530
+
531
+ export function collectBootFindings(ctx, extras = {}) {
532
+ return Object.values(collectBootFindingGroups(ctx, extras)).flat();
457
533
  }
@@ -8,6 +8,12 @@ export function getSurfaceBriefDir(projectRoot) {
8
8
  return path.join(projectRoot, '.impeccable', 'surfaces');
9
9
  }
10
10
 
11
+ function normalizeRouteTarget(route) {
12
+ if (!route.startsWith('/') || route.includes('..')) return null;
13
+ const normalized = route.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
14
+ return `route:${normalized}`;
15
+ }
16
+
11
17
  export function normalizeSurfaceTarget(target, { projectRoot = process.cwd() } = {}) {
12
18
  if (!target || typeof target !== 'string' || !target.trim()) return null;
13
19
  const trimmed = target.trim();
@@ -21,21 +27,13 @@ export function normalizeSurfaceTarget(target, { projectRoot = process.cwd() } =
21
27
  return null;
22
28
  }
23
29
  }
24
- if (/^route:/i.test(trimmed)) {
25
- const route = trimmed.slice(trimmed.indexOf(':') + 1).trim();
26
- if (!route.startsWith('/') || route.includes('..')) return null;
27
- const normalizedRoute = route.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
28
- return `route:${normalizedRoute}`;
29
- }
30
- if (trimmed === '/') return 'route:/';
30
+ if (/^route:/i.test(trimmed)) return normalizeRouteTarget(trimmed.slice(trimmed.indexOf(':') + 1).trim());
31
+ if (trimmed === '/') return normalizeRouteTarget(trimmed);
31
32
  if (trimmed.startsWith('/')) {
32
33
  const absolute = path.resolve(trimmed);
33
34
  const relativeToProject = path.relative(projectRoot, absolute);
34
35
  const isProjectFile = relativeToProject && !relativeToProject.startsWith('..') && !path.isAbsolute(relativeToProject);
35
- if (!isProjectFile && !fs.existsSync(absolute) && !trimmed.includes('..')) {
36
- const normalizedRoute = trimmed.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
37
- return `route:${normalizedRoute}`;
38
- }
36
+ if (!isProjectFile && !fs.existsSync(absolute)) return normalizeRouteTarget(trimmed);
39
37
  }
40
38
  const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(projectRoot, trimmed);
41
39
  const rel = path.relative(projectRoot, abs);
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs';
5
+
4
6
  export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([
5
7
  Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }),
6
8
  Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }),
9
+ Object.freeze({ name: 'project-ignores', file: 'live-browser-ignores.js' }),
7
10
  Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }),
8
11
  ]);
9
12
 
@@ -32,7 +35,25 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re
32
35
  }));
33
36
  }
34
37
 
35
- export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) {
38
+ export function assembleLiveBrowserScript({
39
+ token,
40
+ port,
41
+ vocabulary,
42
+ commandPrefix = '/',
43
+ appRoot = null,
44
+ parts,
45
+ // Defaulted rather than threaded through live-server.mjs: the browser bundle
46
+ // must always carry the canonical inventory, and a default makes that true by
47
+ // construction instead of by every caller remembering to pass it. Overridable
48
+ // so tests can assemble with a stand-in.
49
+ uiSurfaces = LIVE_UI_SURFACES,
50
+ mountContract = LIVE_CHROME_MOUNT_CONTRACT,
51
+ // Project detector waivers ({ ignoreRules, ignoreValues, roots }), read from
52
+ // .impeccable config by live-server.mjs. live-browser-ignores.js resolves
53
+ // them against the page when a detect scan starts, so the overlay filters
54
+ // the same findings the CLI and the edit hook do (issue #639).
55
+ projectIgnores = null,
56
+ }) {
36
57
  const prelude =
37
58
  `window.__IMPECCABLE_TOKEN__ = '${token}';\n` +
38
59
  `window.__IMPECCABLE_PORT__ = ${port};\n` +
@@ -44,7 +65,15 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref
44
65
  `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` +
45
66
  // Canonical command vocabulary (values + labels + icons). live-browser.js
46
67
  // builds its action picker from this instead of an inline copy.
47
- `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`;
68
+ `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` +
69
+ // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js
70
+ // is a classic script and cannot import an ES module at runtime, so the list
71
+ // is serialized here and read off the global there. Node consumers (this
72
+ // repo's tests, the impeccable-site Live UI lab) import the module directly,
73
+ // which is what keeps the two from drifting.
74
+ `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` +
75
+ `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n` +
76
+ `window.__IMPECCABLE_PROJECT_IGNORES__ = ${JSON.stringify(projectIgnores)};\n`;
48
77
 
49
78
  const body = parts.map((part) => {
50
79
  const file = part.file || path.basename(part.path || '');
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Project detector waivers for the live overlay (issue #639, hardened in the
3
+ * PR #645 follow-up). One place decides what the /live.js prelude serializes
4
+ * as window.__IMPECCABLE_PROJECT_IGNORES__:
5
+ *
6
+ * ignoreRules detector.ignoreRules, unioned across every live root.
7
+ * ignoreValues detector.ignoreValues entries ({rule, value, files?}),
8
+ * deduped across roots; createdAt/reason stay local.
9
+ * ignoreFiles detector.ignoreFiles globs, unioned across roots, so a
10
+ * wholly waived page scans to zero findings in the overlay
11
+ * just as it reports nothing through the CLI and the hook.
12
+ * roots served-root prefixes derived from the inject config's own
13
+ * `files` globs. Never derived from the ignore globs: one
14
+ * entry scoped to prototype/library/** would lend
15
+ * prototype/library/ as a candidate prefix to every page,
16
+ * and that rule would suppress site-wide (issue #639).
17
+ * pageFiles the inject config's `files` expanded to real project
18
+ * files, so the browser can resolve a URL to the one file it
19
+ * actually serves instead of trying every root (PR #645
20
+ * review: with src/ and public/ both served, /foo.html must
21
+ * not borrow src/foo.html's waivers while actually serving
22
+ * public/foo.html).
23
+ *
24
+ * Config is read from every root the live session spans: the appRoot the
25
+ * server chdir'd onto, plus contextRoot and repoRoot when they differ. The
26
+ * edit hook keys the same config at the session cwd (the repo root in a
27
+ * monorepo, via resolveCacheCwd), and `impeccable detect` reads it from its
28
+ * invocation cwd, so reading only the appRoot silently dropped every waiver
29
+ * in exactly the monorepo layouts the roots manifest exists for. Reading is
30
+ * additive across roots, matching readConfig's own union of config.json and
31
+ * config.local.json.
32
+ *
33
+ * In a monorepo, roots and pageFiles are serialized repo-relative (the
34
+ * appRoot's path inside the repo is prefixed), so waivers spelled from
35
+ * either root match through the resolver's suffix expansion.
36
+ */
37
+ import fs from 'node:fs';
38
+ import path from 'node:path';
39
+ import { readConfig } from '../hook-lib.mjs';
40
+ import { resolveFiles } from '../live-inject.mjs';
41
+ import { resolveLiveConfigPath } from '../lib/impeccable-paths.mjs';
42
+
43
+ // Serializing thousands of page identities into every /live.js response
44
+ // helps nobody; past this cap pageFiles is omitted and the resolver falls
45
+ // back to the served-root common ancestor, which is correct, just less
46
+ // precise about cross-root duplicates.
47
+ const PAGE_FILES_CAP = 500;
48
+
49
+ export function collectProjectDetectorIgnores({ appRoot, contextRoot, repoRoot, scriptsDir } = {}) {
50
+ const configRoots = [];
51
+ for (const dir of [appRoot, contextRoot, repoRoot]) {
52
+ if (typeof dir !== 'string' || !dir) continue;
53
+ const resolved = path.resolve(dir);
54
+ if (!configRoots.includes(resolved)) configRoots.push(resolved);
55
+ }
56
+ if (configRoots.length === 0) configRoots.push(process.cwd());
57
+
58
+ const ignoreRules = new Set();
59
+ const ignoreFiles = new Set();
60
+ const valueEntries = new Map();
61
+ for (const dir of configRoots) {
62
+ // readConfig merges config.json with the gitignored config.local.json
63
+ // and type-checks both, exactly as the edit hook reads the same pair.
64
+ const config = readConfig(dir);
65
+ for (const rule of Array.isArray(config.ignoreRules) ? config.ignoreRules : []) {
66
+ if (typeof rule === 'string' && rule.trim()) ignoreRules.add(rule);
67
+ }
68
+ for (const glob of Array.isArray(config.ignoreFiles) ? config.ignoreFiles : []) {
69
+ if (typeof glob === 'string' && glob.trim()) ignoreFiles.add(glob);
70
+ }
71
+ for (const entry of Array.isArray(config.ignoreValues) ? config.ignoreValues : []) {
72
+ if (!entry || typeof entry !== 'object') continue;
73
+ // readConfig already normalized rule/value and folded `file` into
74
+ // `files`; serve only what the browser matches on.
75
+ const serialized = {
76
+ rule: entry.rule,
77
+ value: entry.value,
78
+ ...(Array.isArray(entry.files) && entry.files.length > 0 ? { files: entry.files } : {}),
79
+ };
80
+ const key = JSON.stringify([serialized.rule, serialized.value,
81
+ Array.isArray(serialized.files) ? [...serialized.files].sort() : []]);
82
+ if (!valueEntries.has(key)) valueEntries.set(key, serialized);
83
+ }
84
+ }
85
+
86
+ const served = readLiveServedPages({ appRoot: configRoots[0], repoRoot, scriptsDir });
87
+ return {
88
+ ignoreRules: [...ignoreRules],
89
+ ignoreValues: [...valueEntries.values()],
90
+ ignoreFiles: [...ignoreFiles],
91
+ roots: served.roots,
92
+ pageFiles: served.pageFiles,
93
+ };
94
+ }
95
+
96
+ function readLiveServedPages({ appRoot, repoRoot, scriptsDir }) {
97
+ let live = null;
98
+ try {
99
+ const configPath = resolveLiveConfigPath({ cwd: appRoot, scriptsDir });
100
+ live = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
101
+ } catch {
102
+ // No readable inject config: the browser matches URL paths as-is.
103
+ return { roots: [], pageFiles: [] };
104
+ }
105
+ const files = Array.isArray(live?.files)
106
+ ? live.files.filter((glob) => typeof glob === 'string' && glob)
107
+ : [];
108
+
109
+ // A monorepo appRoot serializes identities repo-relative, so waivers
110
+ // spelled from either root match through the resolver's suffix expansion.
111
+ let prefix = '';
112
+ if (typeof repoRoot === 'string' && repoRoot) {
113
+ const rel = path.relative(path.resolve(repoRoot), path.resolve(appRoot)).split(path.sep).join('/');
114
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) prefix = `${rel}/`;
115
+ }
116
+
117
+ const roots = [...new Set(files.map((glob) => {
118
+ const wildcardAt = glob.search(/[*?{]/);
119
+ const head = wildcardAt === -1 ? glob : glob.slice(0, wildcardAt);
120
+ const cut = head.lastIndexOf('/');
121
+ return prefix + (cut > -1 ? head.slice(0, cut + 1) : '');
122
+ }))];
123
+
124
+ let pageFiles = [];
125
+ try {
126
+ pageFiles = resolveFiles(appRoot, { ...live, files })
127
+ .filter((rel) => {
128
+ // resolveFiles passes literal entries through even when they do not
129
+ // exist; a missing file is nobody's identity.
130
+ try { return fs.statSync(path.join(appRoot, rel)).isFile(); } catch { return false; }
131
+ })
132
+ .map((rel) => prefix + rel);
133
+ } catch {
134
+ pageFiles = [];
135
+ }
136
+ if (pageFiles.length > PAGE_FILES_CAP) pageFiles = [];
137
+
138
+ return { roots, pageFiles };
139
+ }
@@ -933,9 +933,17 @@ function collectFreeIdentifierRanges(node, scopes, emit) {
933
933
  * Build the preview component's script block from a v2 contract, with
934
934
  * defaults that keep an unhydrated mount rendering instead of crashing.
935
935
  */
936
+ // `/** @type {...} */` directly before a destructuring declaration is also
937
+ // JSDoc's cast syntax, and Svelte 5.50+ re-emits the annotation in cast form
938
+ // onto the template's own declaration: `var /** @type {...} */ (h1) = root()`.
939
+ // That is a syntax error, so the browser's dynamic import of the variant dies
940
+ // with "Unexpected token '('" and nothing renders. `@typedef` carries the same
941
+ // shape without being a cast. Keep it a typedef;
942
+ // tests/live-svelte-props-script.test.mjs compiles what these builders emit
943
+ // and parses the result.
936
944
  export function buildPropsScriptV2(contract) {
937
945
  if (!contract || contract.length === 0) {
938
- return '<script>\n /** @type {Record<string, never>} */\n let {} = $props();\n</script>\n';
946
+ return '<script>\n /** @typedef {Record<string, never>} Props */\n let {} = $props();\n</script>\n';
939
947
  }
940
948
  const defaults = {
941
949
  text: "''",
@@ -957,5 +965,5 @@ export function buildPropsScriptV2(contract) {
957
965
  const typeFields = contract
958
966
  .map((c) => ` ${c.prop}?: ${types[c.kind] ?? 'string'};`)
959
967
  .join('\n');
960
- return `<script>\n /** @type {{\n${typeFields}\n }} */\n let { ${names} } = $props();\n</script>\n`;
968
+ return `<script>\n /** @typedef {{\n${typeFields}\n }} Props */\n let { ${names} } = $props();\n</script>\n`;
961
969
  }
@@ -154,13 +154,19 @@ export function parseSvelteComponentFile(content) {
154
154
  return { markup, cssLines, styleBlock };
155
155
  }
156
156
 
157
+ // A JSDoc `@type` directly before a destructuring declaration is JSDoc cast
158
+ // syntax, and Svelte 5.50+ re-emits it onto the template's own declaration as
159
+ // `var /** @type {...} */ (h1) = root()`, which does not parse. The browser's
160
+ // import of the variant then fails with "Unexpected token '('" and the session
161
+ // shows nothing. `@typedef` documents the same shape without being a cast.
162
+ // tests/live-svelte-props-script.test.mjs compiles and parses what this emits.
157
163
  function buildPropsScript(contract) {
158
164
  if (contract.length === 0) {
159
- return '<script>\n /** @type {Record<string, never>} */\n let {} = $props();\n</script>\n';
165
+ return '<script>\n /** @typedef {Record<string, never>} Props */\n let {} = $props();\n</script>\n';
160
166
  }
161
167
  const names = contract.map((c) => c.prop).join(', ');
162
168
  const typeFields = contract.map((c) => ` ${c.prop}: string;`).join('\n');
163
- return `<script>\n /** @type {{\n${typeFields}\n }} */\n let { ${names} } = $props();\n</script>\n`;
169
+ return `<script>\n /** @typedef {{\n${typeFields}\n }} Props */\n let { ${names} } = $props();\n</script>\n`;
164
170
  }
165
171
 
166
172
  function buildVariantStub(variantNum, originalWithProps, contract) {
@@ -1111,6 +1117,24 @@ export function removeSvelteComponentSession(id, cwd = process.cwd()) {
1111
1117
  * seeded one) used to surface as a red Vite overlay in the user's page plus
1112
1118
  * a mount-failure round trip; bounced at publish time it is a private
1113
1119
  * agent-side fix with the exact file and line.
1120
+ *
1121
+ * What this deliberately does NOT prove is that the emitted module is valid
1122
+ * JavaScript, and issue #580 was exactly that gap: valid .svelte source whose
1123
+ * generated JS did not parse, so this check passed and the browser's import
1124
+ * failed with "Unexpected token '('". Two reasons it cannot close the gap, both
1125
+ * structural rather than oversights:
1126
+ *
1127
+ * - `generate: false` produces no JS to inspect, and generating it here would
1128
+ * spend a full codegen per variant on the publish path.
1129
+ * - `loadSvelteCompiler` reaches the compiler through createRequire, which
1130
+ * Svelte's export map routes to a prebuilt CJS build. The dev server
1131
+ * imports `src/compiler` instead, and only that path uses the app's
1132
+ * installed printer (esrap). The two can disagree, so even a generated
1133
+ * check here would be checking a different compiler than the one whose
1134
+ * output the browser runs.
1135
+ *
1136
+ * tests/live-svelte-props-script.test.mjs covers the emitted JS, importing the
1137
+ * compiler as ESM so it sees what the dev server sees.
1114
1138
  */
1115
1139
  export function compileCheckVariants(id, cwd = process.cwd()) {
1116
1140
  const manifest = findSvelteComponentManifest(id, cwd);
@@ -11,6 +11,8 @@ import crypto from 'node:crypto';
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
 
14
+ import { firstExistingFile, hasAnyDependency } from './frameworks/detect-utils.mjs';
15
+
14
16
  export const SVELTE_LIVE_ROOT_COMPONENT = 'src/lib/impeccable/ImpeccableLiveRoot.svelte';
15
17
  export const SVELTE_LAYOUT_MARKER_OPEN = '<!-- impeccable-live-svelte-start -->';
16
18
  export const SVELTE_LAYOUT_MARKER_CLOSE = '<!-- impeccable-live-svelte-end -->';
@@ -45,11 +47,17 @@ export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
45
47
  && fileIncludes(path.join(cwd, appHtml), '%sveltekit.head%');
46
48
  if (!hasTemplateMarkers) return null;
47
49
 
48
- const hasSvelteConfig = fs.existsSync(path.join(cwd, 'svelte.config.js'))
49
- || fs.existsSync(path.join(cwd, 'svelte.config.mjs'))
50
- || fs.existsSync(path.join(cwd, 'svelte.config.cjs'))
51
- || fs.existsSync(path.join(cwd, 'svelte.config.ts'));
52
- const hasKitPackage = packageHasSvelteKit(cwd);
50
+ const hasSvelteConfig = Boolean(firstExistingFile(cwd, [
51
+ 'svelte.config.js',
52
+ 'svelte.config.mjs',
53
+ 'svelte.config.cjs',
54
+ 'svelte.config.ts',
55
+ ]));
56
+ const hasKitPackage = hasAnyDependency(cwd, [
57
+ '@sveltejs/kit',
58
+ '@sveltejs/vite-plugin-svelte',
59
+ 'svelte',
60
+ ]);
53
61
  if (!hasSvelteConfig && !hasKitPackage) return null;
54
62
 
55
63
  return {
@@ -260,36 +268,16 @@ function findSvelteKitAppHtml(cwd, config) {
260
268
  }
261
269
 
262
270
  function findSvelteKitLayout(cwd) {
263
- const candidates = [
271
+ return firstExistingFile(cwd, [
264
272
  'src/routes/+layout.svelte',
265
273
  'src/routes/(app)/+layout.svelte',
266
- ];
267
- for (const rel of candidates) {
268
- if (fs.existsSync(path.join(cwd, rel))) return rel;
269
- }
270
- return 'src/routes/+layout.svelte';
274
+ ]) || 'src/routes/+layout.svelte';
271
275
  }
272
276
 
273
277
  function defaultSvelteLayout() {
274
278
  return `<script>\n let { children } = $props();\n</script>\n\n{@render children?.()}\n`;
275
279
  }
276
280
 
277
- function packageHasSvelteKit(cwd) {
278
- const file = path.join(cwd, 'package.json');
279
- if (!fs.existsSync(file)) return false;
280
- try {
281
- const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
282
- const deps = {
283
- ...(pkg.dependencies || {}),
284
- ...(pkg.devDependencies || {}),
285
- ...(pkg.peerDependencies || {}),
286
- };
287
- return Boolean(deps['@sveltejs/kit'] || deps['@sveltejs/vite-plugin-svelte'] || deps.svelte);
288
- } catch {
289
- return false;
290
- }
291
- }
292
-
293
281
  function fileIncludes(file, text) {
294
282
  try {
295
283
  return fs.readFileSync(file, 'utf-8').includes(text);
@@ -19,6 +19,8 @@
19
19
 
20
20
  import fs from 'node:fs';
21
21
  import path from 'node:path';
22
+
23
+ import { firstExistingFile, hasAnyDependency } from './frameworks/detect-utils.mjs';
22
24
  import { buildLiveScriptSrc } from './frameworks/script-src.mjs';
23
25
 
24
26
  export const TANSTACK_MARKER_OPEN = '{/* impeccable-live-tanstack-start */}';
@@ -42,8 +44,8 @@ const START_PACKAGES = [
42
44
  ];
43
45
 
44
46
  export function detectTanStackStartProject(cwd = process.cwd()) {
45
- if (!packageHasTanStackStart(cwd)) return null;
46
- const rootRoute = findRootRouteFile(cwd);
47
+ if (!hasAnyDependency(cwd, START_PACKAGES)) return null;
48
+ const rootRoute = firstExistingFile(cwd, ROOT_ROUTE_CANDIDATES);
47
49
  if (!rootRoute) return null;
48
50
 
49
51
  const ext = path.extname(rootRoute);
@@ -218,29 +220,6 @@ function isManagedComponent(content) {
218
220
  return String(content || '').includes('impeccable-live-tanstack');
219
221
  }
220
222
 
221
- function findRootRouteFile(cwd) {
222
- for (const rel of ROOT_ROUTE_CANDIDATES) {
223
- if (fs.existsSync(path.join(cwd, rel))) return rel;
224
- }
225
- return null;
226
- }
227
-
228
- function packageHasTanStackStart(cwd) {
229
- const file = path.join(cwd, 'package.json');
230
- if (!fs.existsSync(file)) return false;
231
- try {
232
- const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
233
- const deps = {
234
- ...(pkg.dependencies || {}),
235
- ...(pkg.devDependencies || {}),
236
- ...(pkg.peerDependencies || {}),
237
- };
238
- return START_PACKAGES.some((name) => Boolean(deps[name]));
239
- } catch {
240
- return false;
241
- }
242
- }
243
-
244
223
  function relativeImportSpecifier(fromFile, toFile) {
245
224
  const rel = path.posix.relative(
246
225
  path.posix.dirname(fromFile.split(path.sep).join('/')),