@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.7

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/package.json +1 -1
  7. package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
  8. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
  9. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
  10. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
  11. package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
  12. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
  13. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
  14. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  15. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
  16. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
  17. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
  18. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
  19. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
  20. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
  21. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
  22. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
  23. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
  24. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  25. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
  26. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  27. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
  28. package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
  29. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
  30. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
  31. package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
  32. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
  33. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
  34. package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
  35. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
  36. package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
  37. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
  38. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
  39. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
  40. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  41. package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
  42. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
  43. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
  44. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
  46. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
  48. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
  49. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
  66. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
  67. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
  68. package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
  70. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
  71. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
  72. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
  73. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  89. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
  94. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  95. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
  96. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
  97. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
  98. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
  99. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
  100. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
  101. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
  102. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
  103. package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
  104. package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
  105. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
  106. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  107. package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
  108. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
  109. package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
  110. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
  111. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
  112. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
  113. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
  115. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
  116. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
  117. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
  118. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
  120. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  121. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
  123. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  124. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
  125. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
  127. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  128. package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
  129. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/chat-session-host.js +22 -1
  131. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  132. package/dist/modes/interactive/interactive-agent-events.js +9 -3
  133. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  134. package/docs/tui.md +7 -2
  135. package/docs/workflows.md +1 -1
  136. package/npm-shrinkwrap.json +23 -23
  137. package/package.json +2 -2
  138. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
  139. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
  140. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
 
4
5
  import { finding } from './findings.mjs';
@@ -7,6 +8,11 @@ import { parseAnyColor, resolveLengthPx } from './rules/checks.mjs';
7
8
 
8
9
  const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
9
10
  const FALLBACK_DIRS = ['.agents/context', 'docs'];
11
+ // Files/dirs whose presence marks a directory as a project root. Mirrors the
12
+ // walk-up semantics of skill/scripts/context.mjs (`resolveProject`), which the
13
+ // CLI can't import (separate tree). `.git` and `package.json` are the common
14
+ // boundaries; `.impeccable` is our own project marker.
15
+ const PROJECT_ROOT_MARKERS = ['.git', 'package.json', '.impeccable'];
10
16
  const COLOR_CHANNEL_TOLERANCE = 6;
11
17
  const RADIUS_TOLERANCE_PX = 0.5;
12
18
  const FONT_SIZE_TOLERANCE_PX = 0.5;
@@ -288,15 +294,74 @@ function addTypographyFonts(out, typography) {
288
294
  }
289
295
  }
290
296
 
297
+ function addFontSizeStep(out, raw, { fluid = false } = {}) {
298
+ const text = String(raw ?? '').trim().toLowerCase();
299
+ if (!FONT_SIZE_LITERAL_RE.test(text)) return;
300
+ const px = resolveLengthPx(text, 16);
301
+ if (px == null || !Number.isFinite(px) || px <= 0) return;
302
+ out.allowedFontSizes.push({ value: text, px, fluid });
303
+ }
304
+
305
+ // Split a fluid value into its three terms, or null when it is not a
306
+ // well-formed clamp(). Used both to read DESIGN.md's fluid roles and to
307
+ // validate fluid values in source, so the two stay symmetric.
308
+ function parseClampArgs(raw) {
309
+ const match = /^clamp\(\s*([\s\S]+)\s*\)$/i.exec(String(raw ?? '').trim());
310
+ if (!match) return null;
311
+ const args = splitTopLevelArgs(match[1]);
312
+ return args.length === 3 ? args : null;
313
+ }
314
+
315
+ // A fluid role declares its two fixed endpoints and interpolates between them
316
+ // with a viewport unit. Both endpoints are documented sizes, so they belong in
317
+ // the allowlist; the middle term is viewport-relative and never a fixed step.
318
+ // Endpoints are marked `fluid` because they do not *enumerate* a ramp: see
319
+ // `hasFontSizes` below for why that distinction has to survive.
320
+ function addClampEndpoints(out, raw) {
321
+ const args = parseClampArgs(raw);
322
+ if (!args) return false;
323
+ addFontSizeStep(out, args[0], { fluid: true });
324
+ addFontSizeStep(out, args[2], { fluid: true });
325
+ return true;
326
+ }
327
+
328
+ function splitTopLevelArgs(s) {
329
+ const args = [];
330
+ let depth = 0;
331
+ let current = '';
332
+ for (const ch of String(s)) {
333
+ if (ch === '(') depth++;
334
+ else if (ch === ')') depth--;
335
+ if (ch === ',' && depth === 0) {
336
+ args.push(current.trim());
337
+ current = '';
338
+ continue;
339
+ }
340
+ current += ch;
341
+ }
342
+ if (current.trim()) args.push(current.trim());
343
+ return args;
344
+ }
345
+
291
346
  function addTypographySizes(out, typography) {
292
347
  if (!typography || typeof typography !== 'object') return;
293
- for (const role of Object.values(typography)) {
348
+
349
+ // `scale` is the enumerated ramp: a name -> size map, since the frontmatter
350
+ // parser has no list support. It sits alongside the named roles.
351
+ const scale = typography.scale;
352
+ if (scale && typeof scale === 'object') {
353
+ for (const value of Object.values(scale)) {
354
+ if (typeof value !== 'string' && typeof value !== 'number') continue;
355
+ addFontSizeStep(out, value);
356
+ }
357
+ }
358
+
359
+ for (const [name, role] of Object.entries(typography)) {
360
+ if (name === 'scale') continue;
294
361
  if (!role || typeof role !== 'object') continue;
295
362
  const raw = String(role.fontSize ?? '').trim().toLowerCase();
296
- if (!FONT_SIZE_LITERAL_RE.test(raw)) continue;
297
- const px = resolveLengthPx(raw, 16);
298
- if (px == null || !Number.isFinite(px) || px <= 0) continue;
299
- out.allowedFontSizes.push({ value: raw, px });
363
+ if (addClampEndpoints(out, raw)) continue;
364
+ addFontSizeStep(out, raw);
300
365
  }
301
366
  }
302
367
 
@@ -371,7 +436,10 @@ function normalizeDesignSystem(input = {}) {
371
436
  out.hasFonts = out.allowedFonts.size > 0;
372
437
  out.hasColors = out.allowedColorKeys.size > 0;
373
438
  out.hasRadii = out.allowedRadii.length > 0;
374
- out.hasFontSizes = out.allowedFontSizes.length > 0;
439
+ // Gate on *enumerated* steps only. A fully fluid system declares clamp
440
+ // endpoints but no discrete ramp, so treating those endpoints as the whole
441
+ // allowlist would flag every intermediate size. Abstain instead.
442
+ out.hasFontSizes = out.allowedFontSizes.some(entry => !entry.fluid);
375
443
  return out;
376
444
  }
377
445
 
@@ -407,6 +475,62 @@ function loadDesignSystemForCwd(cwd = process.cwd()) {
407
475
  });
408
476
  }
409
477
 
478
+ // Directory to begin the project-root walk from, given a scan target that may
479
+ // be a file or a directory (and may not exist yet).
480
+ function designSystemStartDir(targetPath, cwd = process.cwd()) {
481
+ const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(cwd, targetPath);
482
+ try {
483
+ return fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
484
+ } catch {
485
+ // Nonexistent path: treat an extension-bearing leaf as a file.
486
+ return path.extname(abs) ? path.dirname(abs) : abs;
487
+ }
488
+ }
489
+
490
+ // Walk up from `startDir` to the directory that governs the target's design
491
+ // system, mirroring skill/scripts/context.mjs's project-boundary semantics:
492
+ //
493
+ // - A directory carrying a DESIGN.md (directly or in a fallback dir) IS the
494
+ // design root — that's where the rules live.
495
+ // - 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.
498
+ // - Reaching the home directory / filesystem root with neither means no
499
+ // design system at all — never process.cwd()'s.
500
+ //
501
+ // Returns { dir, hasDesign } for the stopping directory, or null when the walk
502
+ // runs out. This is the fix for cross-project contamination.
503
+ export function findDesignRoot(startDir) {
504
+ let dir = path.resolve(startDir);
505
+ const homeDir = path.resolve(os.homedir());
506
+ 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 };
510
+ }
511
+ if (dir === homeDir) return null;
512
+ const parent = path.dirname(dir);
513
+ if (parent === dir) return null;
514
+ dir = parent;
515
+ }
516
+ }
517
+
518
+ // Resolve the design system that governs a specific scan target, by walking up
519
+ // from the target's own location — never process.cwd(). Scanning project B's
520
+ // files from inside project A applies B's DESIGN.md (or none), not A's.
521
+ //
522
+ // Pass a `cache` Map to memoize by resolved design root across a multi-file
523
+ // scan; a target with no design root above it resolves to null.
524
+ export function loadDesignSystemForTarget(targetPath, { cache, cwd = process.cwd() } = {}) {
525
+ const startDir = designSystemStartDir(targetPath, cwd);
526
+ const found = findDesignRoot(startDir);
527
+ const key = found ? `root:${found.dir}` : '\0none';
528
+ if (cache && cache.has(key)) return cache.get(key);
529
+ const loaded = found?.hasDesign ? loadDesignSystemForCwd(found.dir) : null;
530
+ if (cache) cache.set(key, loaded);
531
+ return loaded;
532
+ }
533
+
410
534
  function isAllowedFont(font, designSystem) {
411
535
  if (!font || GENERIC_FONTS.has(font)) return true;
412
536
  if (!designSystem?.hasFonts) return true;
@@ -438,17 +562,43 @@ function isAllowedRadiusRaw(raw, designSystem) {
438
562
  return designSystem.allowedRadii.some(entry => Math.abs(entry.px - px) <= RADIUS_TOLERANCE_PX);
439
563
  }
440
564
 
441
- function isAllowedFontSizeRaw(raw, designSystem) {
442
- if (!designSystem?.hasFontSizes) return true;
443
- const text = String(raw || '').trim().toLowerCase().replace(/\s*!important\s*$/, '');
444
- if (!FONT_SIZE_LITERAL_RE.test(text)) return true;
565
+ // One term of a font-size value. `unjudgeable` covers var(), calc(), percentages
566
+ // and units the ramp cannot resolve (em is parent-relative, not root-relative);
567
+ // those abstain rather than guess.
568
+ function fontSizeStepStatus(raw, designSystem) {
569
+ const text = String(raw || '').trim().toLowerCase();
570
+ if (!FONT_SIZE_LITERAL_RE.test(text)) return 'unjudgeable';
445
571
  const px = resolveLengthPx(text, 16);
446
- if (px == null || !Number.isFinite(px) || px <= 0) return true;
572
+ if (px == null || !Number.isFinite(px) || px <= 0) return 'unjudgeable';
447
573
  return designSystem.allowedFontSizes.some(
448
574
  entry => Math.abs(entry.px - px) <= FONT_SIZE_TOLERANCE_PX,
575
+ ) ? 'on-ramp' : 'off-ramp';
576
+ }
577
+
578
+ // The off-ramp endpoints of a fluid value, or null when `raw` is not a fluid
579
+ // value at all. Only the min and max are judged: the viewport term interpolates
580
+ // between them and is never a fixed step.
581
+ //
582
+ // Reading clamp endpoints as documented steps without also checking them in
583
+ // usage would let `clamp(99rem, 1vw, 200rem)` through, which is how a fluid
584
+ // declaration stayed invisible until someone measured computed styles.
585
+ export function offRampClampEndpoints(raw, designSystem) {
586
+ if (!designSystem?.hasFontSizes) return null;
587
+ const args = parseClampArgs(String(raw || '').trim().replace(/\s*!important\s*$/i, ''));
588
+ if (!args) return null;
589
+ return [args[0], args[2]].filter(
590
+ endpoint => fontSizeStepStatus(endpoint, designSystem) === 'off-ramp',
449
591
  );
450
592
  }
451
593
 
594
+ function isAllowedFontSizeRaw(raw, designSystem) {
595
+ if (!designSystem?.hasFontSizes) return true;
596
+ const text = String(raw || '').trim().toLowerCase().replace(/\s*!important\s*$/, '');
597
+ const offRampEndpoints = offRampClampEndpoints(text, designSystem);
598
+ if (offRampEndpoints) return offRampEndpoints.length === 0;
599
+ return fontSizeStepStatus(text, designSystem) !== 'off-ramp';
600
+ }
601
+
452
602
  function lineLooksCommented(line) {
453
603
  const trimmed = String(line || '').trim();
454
604
  return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('<!--');
@@ -543,12 +693,31 @@ function checkRadiusValue(value, filePath, line, designSystem, context) {
543
693
  function checkFontSizeValue(value, filePath, line, designSystem, context) {
544
694
  const token = String(value || '').trim();
545
695
  if (isAllowedFontSizeRaw(token, designSystem)) return [];
696
+
697
+ // Name the offending endpoint on a fluid value; the whole clamp() string is
698
+ // not actionable on its own, and it makes a poor ignore-value.
699
+ const offRampEndpoints = offRampClampEndpoints(token, designSystem) || [];
700
+ if (offRampEndpoints.length > 0) {
701
+ const plural = offRampEndpoints.length > 1 ? 's' : '';
702
+ return [makeDesignFinding(
703
+ 'design-system-font-size',
704
+ filePath,
705
+ `${context}: ${token} has fluid endpoint${plural} ${offRampEndpoints.join(' and ')} off the DESIGN.md type ramp`,
706
+ line,
707
+ { ignoreValue: offRampEndpoints[0] },
708
+ )];
709
+ }
710
+
711
+ // The snippet shows the declaration as authored, but the ignoreValue has to
712
+ // be what a `hooks ignore-value` waiver can match, so the priority marker is
713
+ // stripped. Otherwise the same size needs two different waivers depending on
714
+ // whether it carries !important. font-family already behaves this way.
546
715
  return [makeDesignFinding(
547
716
  'design-system-font-size',
548
717
  filePath,
549
718
  `${context}: ${token} is off the DESIGN.md type ramp`,
550
719
  line,
551
- { ignoreValue: token },
720
+ { ignoreValue: token.replace(/\s*!important\s*$/i, '').trim() },
552
721
  )];
553
722
  }
554
723