monomind 2.5.2 → 2.5.3

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 (122) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/@monomind/cli/.claude/agents/github/repo-architect.md +3 -3
  4. package/packages/@monomind/cli/.claude/agents/github/sync-coordinator.md +2 -2
  5. package/packages/@monomind/cli/.claude/agents/specialists/integration-architect.md +4 -4
  6. package/packages/@monomind/cli/.claude/commands/github/repo-architect.md +4 -4
  7. package/packages/@monomind/cli/.claude/commands/github/sync-coordinator.md +2 -2
  8. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +2 -2
  9. package/packages/@monomind/cli/.claude/helpers/extras-registry.json +1 -1
  10. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +2 -2
  11. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +2 -2
  12. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +1 -1
  13. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +1 -1
  14. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +1 -1
  15. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +11 -11
  16. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +2 -2
  17. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +8 -8
  18. package/packages/@monomind/cli/.claude/skills/mastermind-techport/SKILL.md +1 -1
  19. package/packages/@monomind/cli/.claude/skills/monodesign/SKILL.md +2 -2
  20. package/packages/@monomind/cli/.claude/skills/monodesign/reference/antipatterns-catalog.md +47 -2
  21. package/packages/@monomind/cli/.claude/skills/monodesign/reference/critique.md +1 -1
  22. package/packages/@monomind/cli/.claude/skills/monodesign/reference/live.md +0 -8
  23. package/packages/@monomind/cli/.claude/skills/monodesign/reference/polish.md +2 -2
  24. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/context-signals.mjs +39 -6
  25. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/context.mjs +0 -1
  26. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/critique-storage.mjs +175 -2
  27. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detect-csp.mjs +0 -1
  28. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detect.mjs +0 -1
  29. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/browser/injected/index.mjs +25 -1
  30. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/cli/main.mjs +0 -1
  31. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/design-system.mjs +0 -1
  32. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/detect-antipatterns-browser.js +356 -1
  33. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/detect-antipatterns.mjs +0 -1
  34. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/browser/detect-url.mjs +0 -1
  35. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/browser/drivers.mjs +0 -1
  36. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/regex/detect-text.mjs +63 -1
  37. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/static-html/css-cascade.mjs +0 -1
  38. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/static-html/detect-html.mjs +19 -1
  39. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/visual/screenshot-contrast.mjs +0 -1
  40. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/findings.mjs +0 -1
  41. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/fix/css-regions.mjs +181 -0
  42. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/fix/fixers.mjs +373 -0
  43. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/fix/index.mjs +426 -0
  44. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/node/file-system.mjs +0 -1
  45. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/profile/profiler.mjs +0 -1
  46. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/registry/antipatterns.mjs +49 -1
  47. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +292 -1
  48. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/color.mjs +0 -1
  49. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/constants.mjs +0 -1
  50. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/fonts.mjs +0 -1
  51. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/inline-ignores.mjs +0 -1
  52. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/page.mjs +0 -1
  53. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook-admin.mjs +0 -1
  54. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook-before-edit.mjs +0 -1
  55. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook-lib.mjs +0 -1
  56. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook.mjs +0 -1
  57. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/design-parser.mjs +0 -1
  58. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/is-generated.mjs +0 -1
  59. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/monodesign-config.mjs +0 -1
  60. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/monodesign-paths.mjs +0 -1
  61. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/provider.mjs +0 -1
  62. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/target-args.mjs +0 -1
  63. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/browser-script-parts.mjs +0 -1
  64. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/completion.mjs +0 -1
  65. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/event-validation.mjs +0 -1
  66. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/insert-ui.mjs +0 -1
  67. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/manual-apply.mjs +0 -1
  68. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/manual-edit-routes.mjs +0 -1
  69. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/manual-edits-buffer.mjs +0 -1
  70. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/session-store.mjs +5 -2
  71. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/svelte-component.mjs +0 -1
  72. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/sveltekit-adapter.mjs +0 -1
  73. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/ui-core.mjs +0 -1
  74. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/vocabulary.mjs +0 -1
  75. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-accept.mjs +0 -1
  76. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-browser-dom.js +0 -1
  77. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-browser-session.js +0 -1
  78. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-browser.js +0 -1
  79. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-commit-manual-edits.mjs +0 -1
  80. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-complete.mjs +0 -1
  81. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-copy-edit-agent.mjs +0 -1
  82. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-discard-manual-edits.mjs +0 -1
  83. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-inject.mjs +0 -1
  84. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-insert.mjs +0 -1
  85. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-manual-edit-evidence.mjs +0 -1
  86. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-poll.mjs +0 -1
  87. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-resume.mjs +0 -1
  88. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-server.mjs +0 -1
  89. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-status.mjs +0 -1
  90. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-target.mjs +0 -1
  91. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-wrap.mjs +0 -1
  92. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live.mjs +0 -1
  93. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/modern-screenshot.umd.js +0 -1
  94. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/palette.mjs +0 -1
  95. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/pin.mjs +0 -1
  96. package/packages/@monomind/cli/README.md +1 -1
  97. package/packages/@monomind/cli/dist/src/commands/design-detect.js +62 -1
  98. package/packages/@monomind/cli/dist/src/commands/design-palette.d.ts +0 -1
  99. package/packages/@monomind/cli/dist/src/commands/design-palette.js +0 -1
  100. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.d.ts +1 -1
  101. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +1 -1
  102. package/packages/@monomind/cli/dist/src/commands/hooks-workers.d.ts +1 -1
  103. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +8 -8
  104. package/packages/@monomind/cli/dist/src/commands/init.js +2 -2
  105. package/packages/@monomind/cli/dist/src/index.d.ts +1 -1
  106. package/packages/@monomind/cli/dist/src/index.js +3 -3
  107. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +5 -5
  108. package/packages/@monomind/cli/dist/src/init/executor.js +3 -3
  109. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +3 -1
  110. package/packages/@monomind/cli/dist/src/mcp-server.js +2 -2
  111. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.d.ts +1 -1
  112. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.js +1 -1
  113. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-routing.js +2 -2
  114. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +1 -1
  115. package/packages/@monomind/cli/dist/src/memory/memory-kg.d.ts +5 -4
  116. package/packages/@monomind/cli/dist/src/memory/memory-kg.js +5 -4
  117. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +1 -1
  118. package/packages/@monomind/cli/dist/src/routing/llm-caller.js +1 -1
  119. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +1 -1
  120. package/packages/@monomind/cli/dist/src/ui/dashboard.html +22 -20
  121. package/packages/@monomind/cli/dist/src/update/checker.js +3 -3
  122. package/packages/@monomind/cli/package.json +5 -5
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import {
3
2
  BORDER_SAFE_TAGS,
4
3
  GENERIC_FONTS,
@@ -2630,6 +2629,288 @@ function checkElementTextOverflowDOM(el) {
2630
2629
  return [];
2631
2630
  }
2632
2631
 
2632
+ // ─── Section 7: Stylesheet-level accessibility rules ─────────────────────────
2633
+ // These operate on raw CSS text (concatenated <style> blocks + linked sheets),
2634
+ // not per-element computed style — because :hover / :focus-visible / @media
2635
+ // conditions can't be read off a resting element. Shared by the regex, static
2636
+ // and browser engines (each hands over its own CSS text).
2637
+
2638
+ // Parse flat CSS into { selector, body, dark } tuples, tracking one level of
2639
+ // dark-scheme context (a `@media (prefers-color-scheme: dark)` ancestor).
2640
+ // @supports / @layer / @container / @scope are transparently descended.
2641
+ // @font-face / @keyframes and other at-rules are skipped.
2642
+ function collectCssRules(css, dark = false, depth = 0) {
2643
+ const out = [];
2644
+ if (!css || depth > 6) return out;
2645
+ const src = css.replace(/\/\*[\s\S]*?\*\//g, '');
2646
+ let i = 0;
2647
+ while (i < src.length) {
2648
+ const braceStart = src.indexOf('{', i);
2649
+ if (braceStart === -1) break;
2650
+ const prelude = src.slice(i, braceStart).trim();
2651
+ let d = 1, j = braceStart + 1;
2652
+ for (; j < src.length; j++) {
2653
+ if (src[j] === '{') d++;
2654
+ else if (src[j] === '}') { d--; if (d === 0) break; }
2655
+ }
2656
+ const body = src.slice(braceStart + 1, j);
2657
+ i = j + 1;
2658
+ if (!prelude) continue;
2659
+ if (prelude.startsWith('@')) {
2660
+ const atName = prelude.slice(1).split(/[\s(]/)[0].toLowerCase();
2661
+ if (['media', 'supports', 'layer', 'container', 'scope'].includes(atName)) {
2662
+ const isDarkMedia = atName === 'media' && /prefers-color-scheme\s*:\s*dark/i.test(prelude);
2663
+ out.push(...collectCssRules(body, dark || isDarkMedia, depth + 1));
2664
+ }
2665
+ continue;
2666
+ }
2667
+ for (const sel of prelude.split(',').map(s => s.trim()).filter(Boolean)) {
2668
+ out.push({ selector: sel, body, dark });
2669
+ }
2670
+ }
2671
+ return out;
2672
+ }
2673
+
2674
+ // --- missing-focus-visible ---
2675
+ function selectorTargetsInteractive(sel) {
2676
+ return /(^|[\s>+~(,])(a|button|input|select|textarea|summary)(\b|[.:#[])/i.test(sel)
2677
+ || /:focus\b/i.test(sel)
2678
+ || /\[role\s*=\s*["']?button["']?\]/i.test(sel);
2679
+ }
2680
+
2681
+ function checkFocusVisible(cssText) {
2682
+ if (!cssText) return [];
2683
+ const rules = collectCssRules(cssText);
2684
+ let suppressor = null;
2685
+ let hasFocusVisible = false;
2686
+ let hasFocusReplacement = false;
2687
+ for (const r of rules) {
2688
+ const sel = r.selector.toLowerCase();
2689
+ const body = r.body.toLowerCase();
2690
+ if (/:focus-visible/.test(sel)) {
2691
+ // Any :focus-visible styling counts as deliberate focus handling.
2692
+ hasFocusVisible = true;
2693
+ }
2694
+ const suppresses = /outline\s*:\s*(none|0(?:px)?)\b/.test(body)
2695
+ || /outline-style\s*:\s*none/.test(body)
2696
+ || /outline-width\s*:\s*0(?:px)?\b/.test(body);
2697
+ if (suppresses && selectorTargetsInteractive(sel) && !/:focus-visible/.test(sel)) {
2698
+ suppressor = suppressor || r;
2699
+ }
2700
+ // A plain :focus rule that draws a visible affordance is a valid fallback.
2701
+ if (/:focus\b/.test(sel) && !/:focus-visible/.test(sel)) {
2702
+ const olVal = /outline\s*:\s*([^;}]+)/.exec(body);
2703
+ const outlineReplaces = olVal && !/^\s*(none|0(?:px)?|inherit|initial|unset)\s*$/.test(olVal[1]);
2704
+ if (/box-shadow\s*:/.test(body)
2705
+ || outlineReplaces
2706
+ || /outline-(?:color|width|style)\s*:\s*(?!none|0(?:px)?\b)/.test(body)
2707
+ || /border(?:-\w+)?(?:-color)?\s*:/.test(body)) {
2708
+ hasFocusReplacement = true;
2709
+ }
2710
+ }
2711
+ }
2712
+ if (suppressor && !hasFocusVisible && !hasFocusReplacement) {
2713
+ return [{ id: 'missing-focus-visible', snippet: `"${suppressor.selector}" removes outline with no :focus-visible replacement` }];
2714
+ }
2715
+ return [];
2716
+ }
2717
+
2718
+ // --- hover-only-affordance ---
2719
+ function lastCompound(sel) {
2720
+ const parts = sel.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
2721
+ const last = parts[parts.length - 1] || sel;
2722
+ return last.replace(/:{1,2}[a-z-]+(\([^)]*\))?/gi, '').trim().toLowerCase();
2723
+ }
2724
+ function bodyRevealsVisible(body) {
2725
+ const b = body.toLowerCase();
2726
+ if (/display\s*:\s*(block|flex|grid|inline(?:-\w+)?|table|list-item)/.test(b)) return true;
2727
+ if (/visibility\s*:\s*visible/.test(b)) return true;
2728
+ if (/opacity\s*:\s*(?:1(?:\.0+)?|0?\.[1-9]\d*|\.\d*[1-9])\b/.test(b)) return true;
2729
+ return false;
2730
+ }
2731
+ function bodyHidesByDefault(body) {
2732
+ const b = body.toLowerCase();
2733
+ return /display\s*:\s*none/.test(b)
2734
+ || /visibility\s*:\s*hidden/.test(b)
2735
+ || /opacity\s*:\s*0(?:\.0+)?\b/.test(b);
2736
+ }
2737
+ function checkHoverOnlyAffordance(cssText) {
2738
+ if (!cssText) return [];
2739
+ const rules = collectCssRules(cssText);
2740
+ const focusTargets = new Set();
2741
+ const hiddenTargets = new Set();
2742
+ for (const r of rules) {
2743
+ const sel = r.selector;
2744
+ if (/:(focus|focus-within|focus-visible|active)\b/i.test(sel) && bodyRevealsVisible(r.body)) {
2745
+ focusTargets.add(lastCompound(sel));
2746
+ }
2747
+ if (!/:hover\b/i.test(sel) && bodyHidesByDefault(r.body)) {
2748
+ hiddenTargets.add(lastCompound(sel));
2749
+ }
2750
+ }
2751
+ const findings = [];
2752
+ const seen = new Set();
2753
+ for (const r of rules) {
2754
+ const sel = r.selector;
2755
+ if (!/:hover\b/i.test(sel)) continue;
2756
+ if (!bodyRevealsVisible(r.body)) continue;
2757
+ const parts = sel.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
2758
+ if (parts.length < 2) continue; // hover on the subject itself — can't hide+reveal
2759
+ if (/:hover\b/i.test(parts[parts.length - 1])) continue; // :hover is on the revealed subject
2760
+ const target = lastCompound(sel);
2761
+ if (!target || !hiddenTargets.has(target)) continue;
2762
+ if (focusTargets.has(target)) continue;
2763
+ if (seen.has(target)) continue;
2764
+ seen.add(target);
2765
+ findings.push({ id: 'hover-only-affordance', snippet: `"${target}" is revealed on :hover with no focus/active equivalent` });
2766
+ }
2767
+ return findings;
2768
+ }
2769
+
2770
+ // --- dark-scheme-contrast-blindspot ---
2771
+ const DARK_SCOPE_RE = /(?:^|[\s>+~(,])(?:html|:root|body)?(?:\.dark|\.theme-dark|\.dark-mode)\b|\[data-theme\s*[~|]?=\s*["']?dark["']?\]|\[data-mode\s*=\s*["']?dark["']?\]|\[data-color-scheme\s*=\s*["']?dark["']?\]/i;
2772
+ function stripDarkScope(sel) {
2773
+ return sel
2774
+ .replace(/(?:html|:root|body)?(?:\.dark|\.theme-dark|\.dark-mode)\b/gi, '')
2775
+ .replace(/\[data-(?:theme|mode|color-scheme)\s*[~|]?=\s*["']?dark["']?\]/gi, '')
2776
+ .replace(/\s*[>+~]\s*/g, ' ')
2777
+ .replace(/\s+/g, ' ')
2778
+ .trim();
2779
+ }
2780
+ function normalizeSelKey(sel) {
2781
+ return sel.replace(/\s*[>+~]\s*/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
2782
+ }
2783
+ function declHasColor(body) { return /(?:^|[;{\s])color\s*:/i.test(body); }
2784
+ function declHasBg(body) { return /(?:^|[;{\s])background(?:-color)?\s*:/i.test(body); }
2785
+ function checkDarkSchemeContrast(cssText) {
2786
+ if (!cssText) return [];
2787
+ const rules = collectCssRules(cssText);
2788
+ const hasDarkMedia = /@media[^{]*prefers-color-scheme\s*:\s*dark/i.test(cssText);
2789
+ const hasDarkSelectors = rules.some(r => DARK_SCOPE_RE.test(r.selector));
2790
+ if (!hasDarkMedia && !hasDarkSelectors) return [];
2791
+
2792
+ const light = new Map(); // base selector → { color, bg }
2793
+ const darkAgg = new Map();
2794
+ for (const r of rules) {
2795
+ const scopedDark = DARK_SCOPE_RE.test(r.selector);
2796
+ const isDark = r.dark || scopedDark;
2797
+ if (isDark) {
2798
+ const base = normalizeSelKey(scopedDark ? stripDarkScope(r.selector) : r.selector);
2799
+ if (!base) continue;
2800
+ const e = darkAgg.get(base) || { color: false, bg: false };
2801
+ if (declHasColor(r.body)) e.color = true;
2802
+ if (declHasBg(r.body)) e.bg = true;
2803
+ darkAgg.set(base, e);
2804
+ } else {
2805
+ const base = normalizeSelKey(r.selector);
2806
+ if (!base) continue;
2807
+ const e = light.get(base) || { color: false, bg: false };
2808
+ if (declHasColor(r.body)) e.color = true;
2809
+ if (declHasBg(r.body)) e.bg = true;
2810
+ light.set(base, e);
2811
+ }
2812
+ }
2813
+
2814
+ const findings = [];
2815
+ for (const [base, d] of darkAgg) {
2816
+ if (d.color === d.bg) continue; // sets both or neither — not a split pair
2817
+ const lp = light.get(base);
2818
+ if (!lp || !(lp.color && lp.bg)) continue; // light scheme must pair both
2819
+ const changed = d.bg ? 'background' : 'text color';
2820
+ const missing = d.bg ? 'text color' : 'background';
2821
+ findings.push({ id: 'dark-scheme-contrast-blindspot', snippet: `"${base}" overrides ${changed} in dark scheme but not ${missing}` });
2822
+ }
2823
+ return findings;
2824
+ }
2825
+
2826
+ // --- image-missing-dimensions ---
2827
+ function checkImageDimensions({ hasWidthAttr, hasHeightAttr, style }) {
2828
+ const s = String(style || '').toLowerCase();
2829
+ if (hasWidthAttr && hasHeightAttr) return [];
2830
+ if (/aspect-?ratio\s*:/.test(s)) return [];
2831
+ const heightM = /(?:^|[;{\s])height\s*:\s*([^;]+)/.exec(s);
2832
+ if (heightM && !/^\s*(auto|inherit|initial|unset|0(?:px)?)\s*$/.test(heightM[1])) return [];
2833
+ return [{ id: 'image-missing-dimensions', snippet: '<img> without width/height attributes or CSS aspect-ratio (layout shift risk)' }];
2834
+ }
2835
+
2836
+ function checkElementImageDimensions(el, style) {
2837
+ const hasWidthAttr = (el.getAttribute?.('width') ?? el.attribs?.width) != null;
2838
+ const hasHeightAttr = (el.getAttribute?.('height') ?? el.attribs?.height) != null;
2839
+ const parts = [el.getAttribute?.('style') || el.attribs?.style || ''];
2840
+ const ar = style && (style.aspectRatio || style['aspect-ratio']);
2841
+ if (ar && ar !== 'auto') parts.push(`aspect-ratio:${ar}`);
2842
+ const h = style && style.height;
2843
+ if (h && h !== 'auto' && h !== '') parts.push(`height:${h}`);
2844
+ return checkImageDimensions({ hasWidthAttr, hasHeightAttr, style: parts.join(';') });
2845
+ }
2846
+
2847
+ function checkElementImageDimensionsDOM(el) {
2848
+ const hasWidthAttr = el.getAttribute('width') != null;
2849
+ const hasHeightAttr = el.getAttribute('height') != null;
2850
+ // Browser computed `height` always resolves to a used px value after layout,
2851
+ // so it can't tell a reserved height from a rendered one — only use inline
2852
+ // style + the computed aspect-ratio (which stays 'auto' unless authored).
2853
+ const parts = [el.getAttribute('style') || ''];
2854
+ const style = getComputedStyle(el);
2855
+ const ar = style.aspectRatio || '';
2856
+ if (ar && ar !== 'auto') parts.push(`aspect-ratio:${ar}`);
2857
+ return checkImageDimensions({ hasWidthAttr, hasHeightAttr, style: parts.join(';') });
2858
+ }
2859
+
2860
+ // --- small-touch-target ---
2861
+ const CLICKABLE_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'image']);
2862
+ const CLICKABLE_ROLES = new Set(['button', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'tab', 'switch', 'checkbox', 'radio', 'option']);
2863
+ function isClickableControl(tag, role, href, inputType) {
2864
+ if (tag === 'button' || tag === 'select' || tag === 'summary') return true;
2865
+ if (tag === 'a') return href != null && href !== '';
2866
+ if (tag === 'input') return CLICKABLE_INPUT_TYPES.has(String(inputType || '').toLowerCase());
2867
+ if (role && CLICKABLE_ROLES.has(role)) return true;
2868
+ return false;
2869
+ }
2870
+ function checkSmallTouchTarget({ tag, role, href, inputType, widthPx, heightPx, isInlineLinkInProse }) {
2871
+ if (!isClickableControl(tag, role, href, inputType)) return [];
2872
+ if (isInlineLinkInProse) return [];
2873
+ if (!(widthPx > 0) || !(heightPx > 0)) return [];
2874
+ if (widthPx >= 44 && heightPx >= 44) return [];
2875
+ const roleLabel = role && !['button', 'a', 'input', 'select'].includes(tag) ? `[role=${role}]` : '';
2876
+ return [{ id: 'small-touch-target', snippet: `<${tag}>${roleLabel} renders ${Math.round(widthPx)}x${Math.round(heightPx)}px (min 44x44)` }];
2877
+ }
2878
+
2879
+ const PROSE_ANCESTOR_SELECTOR = 'p,li,blockquote,figcaption,dd,dt';
2880
+ function checkElementSmallTouchTarget(el, style, tag) {
2881
+ // Static path: only provable when width AND height are explicit px lengths
2882
+ // (jsdom performs no layout; rem/%/auto can't be resolved to a real box).
2883
+ const wRaw = style.width || '';
2884
+ const hRaw = style.height || '';
2885
+ if (!/px\s*$/.test(wRaw) || !/px\s*$/.test(hRaw)) return [];
2886
+ const widthPx = parseFloat(wRaw);
2887
+ const heightPx = parseFloat(hRaw);
2888
+ const href = el.getAttribute?.('href');
2889
+ const role = (el.getAttribute?.('role') || '').toLowerCase();
2890
+ const inputType = el.getAttribute?.('type') || '';
2891
+ // An <a> is a prose text link unless it is explicitly laid out as a block
2892
+ // (button-like). jsdom returns '' for the default inline display, so key
2893
+ // off "not blockish" rather than "== inline".
2894
+ const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
2895
+ const inProse = !!(el.closest && el.closest(PROSE_ANCESTOR_SELECTOR));
2896
+ const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
2897
+ return checkSmallTouchTarget({ tag, role, href, inputType, widthPx, heightPx, isInlineLinkInProse });
2898
+ }
2899
+
2900
+ function checkElementSmallTouchTargetDOM(el) {
2901
+ const tag = el.tagName.toLowerCase();
2902
+ const rect = el.getBoundingClientRect();
2903
+ if (!(rect.width > 0) || !(rect.height > 0)) return [];
2904
+ const href = el.getAttribute('href');
2905
+ const role = (el.getAttribute('role') || '').toLowerCase();
2906
+ const inputType = el.getAttribute('type') || '';
2907
+ const style = getComputedStyle(el);
2908
+ const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
2909
+ const inProse = !!(el.closest && el.closest(PROSE_ANCESTOR_SELECTOR));
2910
+ const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
2911
+ return checkSmallTouchTarget({ tag, role, href, inputType, widthPx: rect.width, heightPx: rect.height, isInlineLinkInProse });
2912
+ }
2913
+
2633
2914
  export {
2634
2915
  checkBorders,
2635
2916
  isEmojiOnlyText,
@@ -2701,4 +2982,14 @@ export {
2701
2982
  checkElementClippedOverflowDOM,
2702
2983
  isScreenReaderOnlyTextStyle,
2703
2984
  checkElementTextOverflowDOM,
2985
+ collectCssRules,
2986
+ checkFocusVisible,
2987
+ checkHoverOnlyAffordance,
2988
+ checkDarkSchemeContrast,
2989
+ checkImageDimensions,
2990
+ checkElementImageDimensions,
2991
+ checkElementImageDimensionsDOM,
2992
+ checkSmallTouchTarget,
2993
+ checkElementSmallTouchTarget,
2994
+ checkElementSmallTouchTargetDOM,
2704
2995
  };
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  // ─── Section 2: Color Utilities ─────────────────────────────────────────────
3
2
 
4
3
  function isNeutralColor(color) {
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  // ─── Section 1: Constants ───────────────────────────────────────────────────
3
2
 
4
3
  const SAFE_TAGS = new Set([
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi;
3
2
 
4
3
  function normalizeGoogleFontFamilyParam(value) {
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Inline, in-file ignore directives — eslint-disable-style waivers that live at
4
3
  * the point they apply and travel with the artifact instead of (or alongside)
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /** Check if content looks like a full page (not a component/partial) */
3
2
  function isFullPage(content) {
4
3
  const stripped = content.replace(/<!--[\s\S]*?-->/g, '');
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * The Monodesign hooks command manages the design hook runtime
5
4
  * via the `hook` key and shared detector ignores via the `detector` key in
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Monodesign design hook — Cursor preToolUse write gate.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Shared library for the Monodesign design hook.
4
3
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Monodesign design hook — PostToolUse entry point.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that
3
2
  // the live-mode design-system panel can render. Deterministic, dependency-free.
4
3
  //
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Decide whether a given file is "generated" (regenerated by a build step,
4
3
  * unsafe to write variants into) or "source" (safe to edit, changes persist).
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI-side reader/writer for the unified `.monodesign` config.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { resolveProjectRoot } from '../context.mjs';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  // Source scripts default to slash commands. The provider build replaces only
3
2
  // this exact declaration, avoiding heuristic rewrites across executable code.
4
3
  export const MONODESIGN_COMMAND_PREFIX = '/'; // @monodesign-provider-command-prefix
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  class TargetArgError extends Error {
3
2
  constructor(message, code) {
4
3
  super(message);
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
 
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  export function completionTypeForAcceptResult(eventType, acceptResult) {
3
2
  if (eventType === 'discard') return acceptResult?.handled === true ? 'discarded' : 'error';
4
3
  if (acceptResult?.handled === true && acceptResult?.carbonize === true) return 'agent_done';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Shared event validation for the live helper server.
4
3
  * Extracted for unit testing (insert mode rules).
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Pure helpers for live-mode insert UI (browser + tests).
4
3
  * Kept separate from live-browser.js so insert logic is unit-testable.
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import { randomUUID } from 'node:crypto';
3
2
  import fs from 'node:fs';
4
3
  import path from 'node:path';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import { validateEvent } from './event-validation.mjs';
3
2
  import {
4
3
  countByPage as countPendingByPage,
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Shared helpers for the pending-manual-edits buffer on disk.
4
3
  *
@@ -1,9 +1,12 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { getLegacyLiveSessionsDir, getLiveSessionsDir } from '../lib/monodesign-paths.mjs';
5
4
 
6
- const COMPLETED_PHASES = new Set(['completed', 'discarded']);
5
+ // steer_done is terminal too: a steer session has no pending work after the
6
+ // agent's reply, and leaving it "active" makes live-status suggest recovery
7
+ // for finished steers forever (the browser's trailing steer checkpoint would
8
+ // also re-stamp phase "steer"; terminal phases ignore late checkpoints).
9
+ const COMPLETED_PHASES = new Set(['completed', 'discarded', 'steer_done']);
7
10
 
8
11
  export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {}) {
9
12
  const rootDir = getLiveSessionsDir(cwd);
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Svelte live-mode component injection helpers.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * SvelteKit live-mode adapter.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Framework-neutral Monodesign live chrome contract.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Canonical design-command vocabulary for Live Mode: each command's value, human
4
3
  * label, and SVG icon. Icons stack above the chip label; strokes use currentColor
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: deterministic accept/discard of variant sessions.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Browser-side DOM helpers for Monodesign live mode.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Browser-side durable session helpers for Monodesign live mode.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Monodesign Live Variant Mode - Browser Script
4
3
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * CLI helper: apply pending live copy edits as one AI-owned batch.
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Canonical durable completion acknowledgement for Monodesign live sessions.
5
4
  */
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Applies staged live copy-edit batches by waking a local AI coding agent.
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * CLI helper: discard pending manual edits from the buffer without applying.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: insert/remove the live variant mode script tag in the project's
4
3
  * main HTML entry point.
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: find an anchor element in source and splice an insert-variant
4
3
  * wrapper before or after it (no original variant — net-new content).
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Collect evidence for pending live copy edits.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI client for the live variant mode poll/reply protocol.
4
3
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Recover the next agent action from the durable live-session journal.
5
4
  */
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Live variant mode server (self-contained, zero dependencies).
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Print durable recovery status for Monodesign live sessions.
5
4
  */
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import path from 'node:path';
3
2
  import { resolveProjectRoot } from './context.mjs';
4
3
  import { parseTargetPath } from './lib/target-args.mjs';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: find an element in source and wrap it in a variant container.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI entry point: prepare everything needed to enter the live variant poll loop.
4
3
  *