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
@@ -13,7 +13,16 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
13
13
  // CLI can't import (separate tree). `.git` and `package.json` are the common
14
14
  // boundaries; `.impeccable` is our own project marker.
15
15
  const PROJECT_ROOT_MARKERS = ['.git', 'package.json', '.impeccable'];
16
+ // Monorepo-root recognition, mirroring context.mjs's isMonorepoRoot: declared
17
+ // workspace globs (package.json `workspaces`, pnpm-workspace.yaml `packages:`)
18
+ // or a marker file beside apps/ or packages/ children.
19
+ const MONOREPO_MARKER_FILES = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
20
+ const MONOREPO_FALLBACK_PROJECT_DIRS = ['apps', 'packages'];
16
21
  const COLOR_CHANNEL_TOLERANCE = 6;
22
+ // Shadow blacks at different alphas are different tokens (0.28 vs 0.55 is the
23
+ // difference between a documented shadow and drift), so shadow matching cannot
24
+ // reuse the r/g/b-only channel tolerance.
25
+ const SHADOW_ALPHA_TOLERANCE = 0.02;
17
26
  const RADIUS_TOLERANCE_PX = 0.5;
18
27
  const FONT_SIZE_TOLERANCE_PX = 0.5;
19
28
  const FONT_SIZE_LITERAL_RE = /^-?[\d.]+(?:px|rem)$/;
@@ -142,10 +151,73 @@ function stripInlineYamlComment(s) {
142
151
  return s;
143
152
  }
144
153
 
154
+ // YAML double-quoted scalars process backslash escapes. Stripping the outer
155
+ // quotes without unescaping leaves them in place, so a nested font family like
156
+ // fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif"
157
+ // reaches allowedFonts as '\"ibm plex sans' and never matches the same family
158
+ // declared in CSS. Scanner instead of a regex: the escape set is small and the
159
+ // backslash handling stays readable.
160
+ // The full YAML 1.2 double-quote escape set (spec section 5.7).
161
+ const YAML_SIMPLE_ESCAPES = {
162
+ '0': '\0',
163
+ a: '\x07',
164
+ b: '\b',
165
+ t: '\t',
166
+ n: '\n',
167
+ v: '\v',
168
+ f: '\f',
169
+ r: '\r',
170
+ e: '\x1b',
171
+ ' ': ' ',
172
+ '"': '"',
173
+ '/': '/',
174
+ '\\': '\\',
175
+ N: '\u0085',
176
+ _: '\u00a0',
177
+ L: '\u2028',
178
+ P: '\u2029',
179
+ };
180
+ const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 };
181
+
182
+ function unescapeYamlDoubleQuoted(body) {
183
+ let out = '';
184
+ for (let i = 0; i < body.length; i++) {
185
+ const ch = body[i];
186
+ if (ch !== '\\' || i === body.length - 1) {
187
+ out += ch;
188
+ continue;
189
+ }
190
+ const next = body[i + 1];
191
+ if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) {
192
+ out += YAML_SIMPLE_ESCAPES[next];
193
+ i++;
194
+ continue;
195
+ }
196
+ // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay
197
+ // literal rather than corrupting the rest of the scalar.
198
+ const hexLen = YAML_HEX_ESCAPE_LENGTHS[next];
199
+ if (hexLen) {
200
+ const hex = body.slice(i + 2, i + 2 + hexLen);
201
+ const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1;
202
+ if (codePoint >= 0 && codePoint <= 0x10ffff) {
203
+ out += String.fromCodePoint(codePoint);
204
+ i += 1 + hexLen;
205
+ continue;
206
+ }
207
+ }
208
+ out += ch;
209
+ }
210
+ return out;
211
+ }
212
+
145
213
  function parseScalar(raw) {
146
214
  const s = raw.trim();
147
- if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
148
- return s.slice(1, -1);
215
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
216
+ return unescapeYamlDoubleQuoted(s.slice(1, -1));
217
+ }
218
+ // Single-quoted YAML escapes only the quote itself, by doubling it.
219
+ if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) {
220
+ return s.slice(1, -1).split("''").join("'");
149
221
  }
150
222
  if (s === 'true') return true;
151
223
  if (s === 'false') return false;
@@ -411,6 +483,25 @@ function addSidecarRadii(out, sidecar) {
411
483
  }
412
484
  }
413
485
 
486
+ // Sidecar `extensions.shadows` entries ({ name, value, purpose }) carry the
487
+ // documented shadow vocabulary that Stitch's frontmatter schema can't hold.
488
+ // Their colors go into a separate allowlist — NOT allowedColorKeys — because a
489
+ // shadow black is only documented *as a shadow*: feeding it into the general
490
+ // color allowlist would legalize #000 as a page ground (alpha is dropped from
491
+ // colorKey), which is the hole issue #547 warns against.
492
+ function addSidecarShadows(out, sidecar) {
493
+ const shadows = sidecar?.extensions?.shadows;
494
+ if (!Array.isArray(shadows)) return;
495
+
496
+ for (const entry of shadows) {
497
+ if (typeof entry?.value !== 'string') continue;
498
+ for (const match of entry.value.matchAll(CSS_COLOR_RE)) {
499
+ const parsed = parseDesignColor(match[0]);
500
+ if (parsed) out.allowedShadowColors.push({ color: parsed });
501
+ }
502
+ }
503
+ }
504
+
414
505
  function normalizeDesignSystem(input = {}) {
415
506
  const frontmatter = input.frontmatter || {};
416
507
  const sidecar = input.sidecar || null;
@@ -423,6 +514,7 @@ function normalizeDesignSystem(input = {}) {
423
514
  allowedColorKeys: new Map(),
424
515
  allowedRadii: [],
425
516
  allowedFontSizes: [],
517
+ allowedShadowColors: [],
426
518
  hasPillRadius: false,
427
519
  };
428
520
 
@@ -432,6 +524,7 @@ function normalizeDesignSystem(input = {}) {
432
524
  addSidecarColors(out, sidecar);
433
525
  addRoundedScale(out, frontmatter.rounded);
434
526
  addSidecarRadii(out, sidecar);
527
+ addSidecarShadows(out, sidecar);
435
528
 
436
529
  out.hasFonts = out.allowedFonts.size > 0;
437
530
  out.hasColors = out.allowedColorKeys.size > 0;
@@ -487,14 +580,179 @@ function designSystemStartDir(targetPath, cwd = process.cwd()) {
487
580
  }
488
581
  }
489
582
 
583
+ // Same two groups as context.mjs's readProjectPatternGroups: Impeccable
584
+ // projectRoots govern any path they match (positive or negated); package-manager
585
+ // globs only apply to paths the Impeccable group does not match.
586
+ function readWorkspacePatternGroups(dir) {
587
+ const impeccable = [];
588
+ for (const name of ['config.json', 'config.local.json']) {
589
+ const roots = safeReadJson(path.join(dir, '.impeccable', name))?.projectRoots;
590
+ if (Array.isArray(roots)) {
591
+ impeccable.push(...roots.filter(entry => typeof entry === 'string' && entry.trim()).map(entry => entry.trim()));
592
+ }
593
+ }
594
+ const pkg = [];
595
+ const workspaces = safeReadJson(path.join(dir, 'package.json'))?.workspaces;
596
+ if (Array.isArray(workspaces)) pkg.push(...workspaces);
597
+ else if (Array.isArray(workspaces?.packages)) pkg.push(...workspaces.packages);
598
+ const lernaPackages = safeReadJson(path.join(dir, 'lerna.json'))?.packages;
599
+ if (Array.isArray(lernaPackages)) pkg.push(...lernaPackages);
600
+ try {
601
+ let inPackages = false;
602
+ for (const line of fs.readFileSync(path.join(dir, 'pnpm-workspace.yaml'), 'utf-8').split(/\r?\n/)) {
603
+ const trimmed = stripInlineYamlComment(line).trim();
604
+ if (!trimmed || trimmed.startsWith('#')) continue;
605
+ const flow = trimmed.match(/^packages:\s*\[(.*)\]\s*$/);
606
+ if (flow) {
607
+ pkg.push(...flow[1].split(',').map(entry => entry.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean));
608
+ break;
609
+ }
610
+ if (/^packages:\s*$/.test(trimmed)) { inPackages = true; continue; }
611
+ if (!inPackages) continue;
612
+ const item = trimmed.match(/^-\s*(.+)$/);
613
+ if (item) pkg.push(item[1].trim().replace(/^['"]|['"]$/g, ''));
614
+ else if (/^[A-Za-z0-9_-]+:\s*/.test(trimmed)) break;
615
+ }
616
+ } catch { /* no pnpm-workspace.yaml */ }
617
+ return [impeccable, pkg];
618
+ }
619
+
620
+ function readWorkspacePatterns(dir) {
621
+ return readWorkspacePatternGroups(dir).flat();
622
+ }
623
+
624
+ function isMonorepoRoot(dir) {
625
+ if (readWorkspacePatterns(dir).some(pattern => !String(pattern).trim().startsWith('!'))) return true;
626
+ if (!MONOREPO_MARKER_FILES.some(file => fs.existsSync(path.join(dir, file)))) return false;
627
+ return MONOREPO_FALLBACK_PROJECT_DIRS.some(name => {
628
+ try {
629
+ return fs.readdirSync(path.join(dir, name), { withFileTypes: true }).some(entry => entry.isDirectory());
630
+ } catch {
631
+ return false;
632
+ }
633
+ });
634
+ }
635
+
636
+ function monorepoOwnsPath(root, boundaryDir) {
637
+ const rel = path.relative(root, boundaryDir);
638
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
639
+ const relSegments = rel.split(path.sep).filter(Boolean);
640
+
641
+ function normalizeWorkspacePattern(pattern) {
642
+ return String(pattern || '')
643
+ .trim()
644
+ .replace(/^['"]|['"]$/g, '')
645
+ .replace(/^\.\//, '')
646
+ .replace(/\/+$/, '');
647
+ }
648
+
649
+ function escapeRegExp(s) {
650
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
651
+ }
652
+
653
+ function segmentMatches(patternSegment, relSegment) {
654
+ if (patternSegment === '*') return true;
655
+ if (!patternSegment.includes('*')) return patternSegment === relSegment;
656
+ const re = new RegExp(`^${escapeRegExp(patternSegment).replace(/\\\*/g, '[^/]*')}$`);
657
+ return re.test(relSegment);
658
+ }
659
+
660
+ function matchGlobSegments(patternSegments, relSegments) {
661
+ function rec(pi, ri) {
662
+ if (pi === patternSegments.length) return ri === relSegments.length;
663
+ if (patternSegments[pi] === '**') {
664
+ if (pi === patternSegments.length - 1) return true;
665
+ for (let k = ri; k <= relSegments.length; k++) {
666
+ if (rec(pi + 1, k)) return true;
667
+ }
668
+ return false;
669
+ }
670
+ if (ri >= relSegments.length) return false;
671
+ if (!segmentMatches(patternSegments[pi], relSegments[ri])) return false;
672
+ return rec(pi + 1, ri + 1);
673
+ }
674
+ return rec(0, 0);
675
+ }
676
+
677
+ // Negations like !packages/excluded must also cover nested dirs under that path.
678
+ function matchesNegation(pattern) {
679
+ const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
680
+ if (!patternSegments.length) return false;
681
+ if (patternSegments.includes('**')) return matchGlobSegments(patternSegments, relSegments);
682
+ if (relSegments.length < patternSegments.length) return false;
683
+ for (let i = 0; i < patternSegments.length; i++) {
684
+ if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
685
+ }
686
+ return true;
687
+ }
688
+
689
+ // Positive globs identify workspace packages at exact depth (`*` is a direct
690
+ // child). A nested package.json under that package is still owned: the
691
+ // ancestor directory of glob length must itself be a package.
692
+ function positiveOwns(pattern) {
693
+ const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
694
+ if (!patternSegments.length) return false;
695
+ if (patternSegments.includes('**')) return matchGlobSegments(patternSegments, relSegments);
696
+ if (relSegments.length < patternSegments.length) return false;
697
+ for (let i = 0; i < patternSegments.length; i++) {
698
+ if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
699
+ }
700
+ if (relSegments.length === patternSegments.length) return true;
701
+ const ancestorDir = path.join(root, ...relSegments.slice(0, patternSegments.length));
702
+ return fs.existsSync(path.join(ancestorDir, 'package.json'));
703
+ }
704
+
705
+ function groupOwns(rawPatterns) {
706
+ const patterns = rawPatterns.map(normalizeWorkspacePattern).filter(Boolean);
707
+ if (!patterns.length) return null;
708
+ const excluded = patterns.some((pattern) => (
709
+ pattern.startsWith('!') && matchesNegation(pattern.slice(1))
710
+ ));
711
+ const included = patterns.filter((pattern) => !pattern.startsWith('!')).some(positiveOwns);
712
+ if (!excluded && !included) return null;
713
+ if (excluded) return false;
714
+ return true;
715
+ }
716
+
717
+ const [impeccable, pkg] = readWorkspacePatternGroups(root);
718
+ const fromImpeccable = groupOwns(impeccable);
719
+ if (fromImpeccable !== null) return fromImpeccable;
720
+ const fromPkg = groupOwns(pkg);
721
+ if (fromPkg !== null) return fromPkg;
722
+ if ([...impeccable, ...pkg].some((pattern) => !normalizeWorkspacePattern(pattern).startsWith('!'))) {
723
+ return false;
724
+ }
725
+ return relSegments.length >= 2 && MONOREPO_FALLBACK_PROJECT_DIRS.includes(relSegments[0]);
726
+ }
727
+
728
+ // Both forms of the home directory. The walk compares path strings, and a
729
+ // symlinked home (e.g. /home -> /var/home) never string-matches the physical
730
+ // paths a cwd-resolved target produces, which would let the post-boundary walk
731
+ // sail through $HOME and inherit from it.
732
+ function homeDirForms() {
733
+ const homeDir = path.resolve(os.homedir());
734
+ const forms = new Set([homeDir]);
735
+ try {
736
+ forms.add(fs.realpathSync(homeDir));
737
+ } catch { /* keep the logical form only */ }
738
+ return forms;
739
+ }
740
+
490
741
  // Walk up from `startDir` to the directory that governs the target's design
491
742
  // system, mirroring skill/scripts/context.mjs's project-boundary semantics:
492
743
  //
493
744
  // - A directory carrying a DESIGN.md (directly or in a fallback dir) IS the
494
745
  // design root — that's where the rules live.
495
746
  // - A directory carrying a project marker (.git / package.json / .impeccable)
496
- // but no DESIGN.md is a project BOUNDARY: the walk stops with no design
497
- // system, so a sibling project never inherits a parent's or cwd's rules.
747
+ // but no DESIGN.md is a project BOUNDARY. A nested package.json inherits
748
+ // the ancestor DESIGN.md only when that ancestor's workspace declarations
749
+ // include the path (negations win; a nested package under a matched
750
+ // workspace still inherits). Marker-only roots (turbo/nx/lerna/pnpm
751
+ // with no globs) still own apps/<name> and packages/<name>. A stray nested
752
+ // package that matches no glob does not inherit. This is detect's
753
+ // contamination contract, not skill-context's repoRoot fallback for
754
+ // excluded paths. A nested separate repository (.git with no workspace
755
+ // declaration) still inherits nothing (issue #570).
498
756
  // - Reaching the home directory / filesystem root with neither means no
499
757
  // design system at all — never process.cwd()'s.
500
758
  //
@@ -502,15 +760,33 @@ function designSystemStartDir(targetPath, cwd = process.cwd()) {
502
760
  // runs out. This is the fix for cross-project contamination.
503
761
  export function findDesignRoot(startDir) {
504
762
  let dir = path.resolve(startDir);
505
- const homeDir = path.resolve(os.homedir());
763
+ const homeDirs = homeDirForms();
764
+ let boundary = null;
506
765
  while (true) {
507
- if (resolveDesignMdPath(dir)) return { dir, hasDesign: true };
508
- if (PROJECT_ROOT_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) {
509
- return { dir, hasDesign: false };
766
+ if (!boundary && resolveDesignMdPath(dir)) return { dir, hasDesign: true };
767
+ if (boundary) {
768
+ // Past the boundary the walk only looks for the monorepo root that owns
769
+ // the workspace path (workspace globs including negations, or marker-only
770
+ // apps/packages fallback). Monorepo-root before .git, same order as
771
+ // context.mjs: a workspace root carrying its own .git is still recognized,
772
+ // while a .git that declares no workspaces is a separate repository and
773
+ // stops the walk with nothing inherited. The home directory is never an
774
+ // owning root, same as context.mjs's findMonorepoRoot, which stops at
775
+ // homeDir before its monorepo check.
776
+ if (!homeDirs.has(dir) && isMonorepoRoot(dir)) {
777
+ if (monorepoOwnsPath(dir, boundary.dir)) return { dir, hasDesign: !!resolveDesignMdPath(dir) };
778
+ return boundary;
779
+ }
780
+ if (fs.existsSync(path.join(dir, '.git'))) return boundary;
781
+ } else if (PROJECT_ROOT_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) {
782
+ boundary = { dir, hasDesign: false };
783
+ // A boundary that is itself a monorepo root, or a separate repository
784
+ // with its own .git, inherits nothing from above.
785
+ if (isMonorepoRoot(dir) || fs.existsSync(path.join(dir, '.git'))) return boundary;
510
786
  }
511
- if (dir === homeDir) return null;
787
+ if (homeDirs.has(dir)) return boundary;
512
788
  const parent = path.dirname(dir);
513
- if (parent === dir) return null;
789
+ if (parent === dir) return boundary;
514
790
  dir = parent;
515
791
  }
516
792
  }
@@ -551,6 +827,20 @@ function isAllowedColorRaw(raw, designSystem) {
551
827
  return false;
552
828
  }
553
829
 
830
+ // A color is a documented shadow color only when both the r/g/b channels AND
831
+ // the alpha match a sidecar shadow token's color. Alpha has to be compared
832
+ // here because colorKey()/colorsClose() drop it, and a match on r/g/b alone
833
+ // would let every black at every alpha through.
834
+ function isAllowedShadowColorRaw(raw, designSystem) {
835
+ if (!designSystem?.allowedShadowColors?.length) return false;
836
+ const parsed = parseDesignColor(String(raw || '').trim().toLowerCase());
837
+ if (!parsed) return false;
838
+ return designSystem.allowedShadowColors.some(entry =>
839
+ colorsClose(parsed, entry.color) &&
840
+ Math.abs((parsed.a ?? 1) - (entry.color.a ?? 1)) <= SHADOW_ALPHA_TOLERANCE,
841
+ );
842
+ }
843
+
554
844
  function isAllowedRadiusRaw(raw, designSystem) {
555
845
  if (!designSystem?.hasRadii) return true;
556
846
  const text = String(raw || '').trim().toLowerCase();
@@ -628,6 +918,40 @@ function isProbablyColorLiteral(line, match) {
628
918
  return styleContext || cssFunctionContext || jsColorKeyContext;
629
919
  }
630
920
 
921
+ // One complete `${...}` template interpolation. Its content may carry paired
922
+ // quoted strings (function arguments, ternary branches) and one level of
923
+ // braces (an object-literal argument, itself allowing paired quotes). Deeper
924
+ // nesting would need a parser, so the regex deliberately fails safe there:
925
+ // the context check misses and the finding fires — a false positive a waiver
926
+ // can silence, never a leak.
927
+ const QUOTED_STRING_SRC = `"[^"]*"|'[^']*'`;
928
+ const INTERPOLATION_SRC =
929
+ `\\$\\{(?:${QUOTED_STRING_SRC}|\\{(?:${QUOTED_STRING_SRC}|[^{}"'\`])*\\}|[^{}"'\`])*\\}`;
930
+ // The two shadow-context tails. Unlike jsColorKeyContext, the JS tail admits
931
+ // commas: a multi-layer shadow string is comma-separated, and a later
932
+ // property on the same line is still blocked because it sits past the
933
+ // string's closing quote. Both tails admit complete interpolations; a bare
934
+ // `}`, quote, or `;` still ends the context.
935
+ const SHADOW_CSS_CONTEXT_RE = new RegExp(
936
+ `(?:^|[{\\s;"'\`(,])(?:box-shadow|text-shadow)\\s*:\\s*(?:${INTERPOLATION_SRC}|[^;{}"'\`])*$`, 'i',
937
+ );
938
+ const SHADOW_JS_CONTEXT_RE = new RegExp(
939
+ `(?:^|[,{]\\s*)(?:boxShadow|textShadow)\\s*[:=]\\s*["'\`]?(?:${INTERPOLATION_SRC}|[^"'\`}])*$`, 'i',
940
+ );
941
+
942
+ // True when the color literal sits inside a box-shadow / text-shadow value —
943
+ // the only contexts where a documented shadow color is legal. Anchored to the
944
+ // end of `before` (no ; } { or quote in between) so a shadow property earlier
945
+ // on the line can't leak the allowance into a later declaration. Kept separate
946
+ // from isProbablyColorLiteral(), which stays a boolean for its existing call
947
+ // sites and deliberately discards which property matched.
948
+ function isShadowPropertyContext(line, match) {
949
+ const index = match.index ?? -1;
950
+ if (index < 0) return false;
951
+ const before = line.slice(0, index);
952
+ return SHADOW_CSS_CONTEXT_RE.test(before) || SHADOW_JS_CONTEXT_RE.test(before);
953
+ }
954
+
631
955
  function isInsideCssAttributeSelector(line, index) {
632
956
  if (index < 0) return false;
633
957
  const before = line.slice(0, index);
@@ -671,7 +995,9 @@ function extractRadiusTokens(value) {
671
995
  return String(value || '')
672
996
  .replace(/\s*\/\s*/g, ' ')
673
997
  .split(/\s+/)
674
- .map(token => token.trim())
998
+ // var() fallbacks leave the closing parenthesis on the final token. Strip
999
+ // it before length resolution so `8px)` is not treated as unitless 8rem.
1000
+ .map(token => token.trim().replace(/\)+$/, ''))
675
1001
  .filter(Boolean);
676
1002
  }
677
1003
 
@@ -761,6 +1087,7 @@ function checkSourceDesignSystem(content, filePath, options = {}) {
761
1087
  if (!isProbablyColorLiteral(line, match)) continue;
762
1088
  const raw = cssColorLabel(match[0]);
763
1089
  if (isAllowedColorRaw(raw, designSystem)) continue;
1090
+ if (isShadowPropertyContext(line, match) && isAllowedShadowColorRaw(raw, designSystem)) continue;
764
1091
  findings.push(makeDesignFinding(
765
1092
  'design-system-color',
766
1093
  filePath,
@@ -975,6 +1302,7 @@ export {
975
1302
  loadDesignSystemForCwd,
976
1303
  isAllowedFont,
977
1304
  isAllowedColorRaw,
1305
+ isAllowedShadowColorRaw,
978
1306
  isAllowedRadiusRaw,
979
1307
  isAllowedFontSizeRaw,
980
1308
  checkSourceDesignSystem,