@bastani/atomic 0.9.13 → 0.9.14-alpha.2

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 (762) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +3 -3
  3. package/dist/builtin/intercom/package.json +2 -2
  4. package/dist/builtin/mcp/package.json +3 -3
  5. package/dist/builtin/subagents/CHANGELOG.md +14 -0
  6. package/dist/builtin/subagents/README.md +6 -4
  7. package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
  8. package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
  9. package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
  11. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
  12. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
  13. package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
  14. package/dist/builtin/subagents/agents/debugger.md +1 -1
  15. package/dist/builtin/subagents/agents/worker.md +1 -1
  16. package/dist/builtin/subagents/package.json +6 -5
  17. package/dist/builtin/subagents/src/agents/agent-discovery.ts +17 -11
  18. package/dist/builtin/subagents/src/agents/agent-loaders.ts +115 -43
  19. package/dist/builtin/subagents/src/agents/agent-management-helpers.ts +10 -8
  20. package/dist/builtin/subagents/src/agents/agent-management.ts +8 -7
  21. package/dist/builtin/subagents/src/agents/agent-overrides.ts +2 -2
  22. package/dist/builtin/subagents/src/agents/agent-scope.ts +1 -1
  23. package/dist/builtin/subagents/src/agents/agent-selection.ts +1 -1
  24. package/dist/builtin/subagents/src/agents/agent-serializer.ts +24 -3
  25. package/dist/builtin/subagents/src/agents/agent-types.ts +3 -1
  26. package/dist/builtin/subagents/src/agents/agents.ts +7 -5
  27. package/dist/builtin/subagents/src/agents/frontmatter.ts +67 -22
  28. package/dist/builtin/subagents/src/agents/identity.ts +1 -1
  29. package/dist/builtin/subagents/src/agents/skills-paths.ts +1 -1
  30. package/dist/builtin/subagents/src/agents/skills.ts +64 -6
  31. package/dist/builtin/subagents/src/extension/config.ts +1 -1
  32. package/dist/builtin/subagents/src/extension/control-notices.ts +2 -2
  33. package/dist/builtin/subagents/src/extension/doctor.ts +15 -5
  34. package/dist/builtin/subagents/src/extension/fanout-child.ts +12 -12
  35. package/dist/builtin/subagents/src/extension/index.ts +23 -23
  36. package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
  37. package/dist/builtin/subagents/src/extension/schemas.ts +1 -1
  38. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +3 -3
  39. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +3 -3
  40. package/dist/builtin/subagents/src/intercom/result-intercom.ts +1 -1
  41. package/dist/builtin/subagents/src/intercom/supervisor-authorization.ts +1 -1
  42. package/dist/builtin/subagents/src/runs/foreground/completion-notification.ts +2 -2
  43. package/dist/builtin/subagents/src/runs/foreground/execution-detach-reservations.ts +1 -1
  44. package/dist/builtin/subagents/src/runs/foreground/execution-intercom-detach.ts +3 -3
  45. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -1
  46. package/dist/builtin/subagents/src/runs/foreground/execution-updates.ts +2 -2
  47. package/dist/builtin/subagents/src/runs/foreground/execution-utils.ts +1 -1
  48. package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -2
  49. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +27 -11
  50. package/dist/builtin/subagents/src/runs/foreground/notify.ts +3 -3
  51. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +11 -11
  52. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-input.ts +3 -3
  53. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +15 -14
  54. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +19 -19
  55. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -2
  56. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +14 -13
  57. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +9 -9
  58. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +7 -7
  59. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-worktree.ts +6 -6
  60. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +24 -23
  61. package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +1 -1
  62. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +3 -3
  63. package/dist/builtin/subagents/src/runs/inprocess/index.ts +3 -3
  64. package/dist/builtin/subagents/src/runs/inprocess/prompt-behavior.ts +17 -8
  65. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +60 -12
  66. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-api.ts +8 -8
  67. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-control.ts +3 -3
  68. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-core.ts +4 -4
  69. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-projection.ts +4 -4
  70. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-registry.ts +5 -5
  71. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-rendering.ts +3 -3
  72. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-sanitize.ts +3 -3
  73. package/dist/builtin/subagents/src/runs/inprocess.ts +1 -1
  74. package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +1 -1
  75. package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +1 -1
  76. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +3 -3
  77. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +2 -2
  78. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -1
  79. package/dist/builtin/subagents/src/runs/shared/single-output.ts +1 -1
  80. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +1 -1
  81. package/dist/builtin/subagents/src/shared/artifacts.ts +2 -2
  82. package/dist/builtin/subagents/src/shared/event-jsonl-writer.ts +1 -1
  83. package/dist/builtin/subagents/src/shared/fast-mode.ts +1 -1
  84. package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
  85. package/dist/builtin/subagents/src/shared/jsonl-writer.ts +1 -1
  86. package/dist/builtin/subagents/src/shared/model-resolution.ts +1 -1
  87. package/dist/builtin/subagents/src/shared/session-tokens.ts +1 -1
  88. package/dist/builtin/subagents/src/shared/settings.ts +4 -4
  89. package/dist/builtin/subagents/src/shared/status-format.ts +1 -1
  90. package/dist/builtin/subagents/src/shared/types-config.ts +16 -4
  91. package/dist/builtin/subagents/src/shared/types-depth.ts +1 -1
  92. package/dist/builtin/subagents/src/shared/types-nested.ts +1 -1
  93. package/dist/builtin/subagents/src/shared/types-output.ts +1 -1
  94. package/dist/builtin/subagents/src/shared/types-results.ts +1 -1
  95. package/dist/builtin/subagents/src/shared/types-runtime.ts +1 -1
  96. package/dist/builtin/subagents/src/shared/types.ts +6 -6
  97. package/dist/builtin/subagents/src/shared/utils.ts +3 -3
  98. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +1 -1
  99. package/dist/builtin/subagents/src/slash/slash-bridge.ts +3 -3
  100. package/dist/builtin/subagents/src/slash/slash-commands.ts +6 -6
  101. package/dist/builtin/subagents/src/slash/slash-live-state.ts +3 -3
  102. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +2 -2
  103. package/dist/builtin/subagents/src/tui/render-layout.ts +1 -1
  104. package/dist/builtin/subagents/src/tui/render-progress.ts +2 -2
  105. package/dist/builtin/subagents/src/tui/render-result-compact.ts +7 -7
  106. package/dist/builtin/subagents/src/tui/render-result.ts +10 -10
  107. package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
  108. package/dist/builtin/subagents/src/tui/render-status-progress.ts +5 -5
  109. package/dist/builtin/subagents/src/tui/render.ts +4 -4
  110. package/dist/builtin/web-access/package.json +2 -2
  111. package/dist/builtin/workflows/CHANGELOG.md +57 -0
  112. package/dist/builtin/workflows/README.md +39 -8
  113. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +16 -5
  114. package/dist/builtin/workflows/builtin/adversarial-verification.ts +4 -0
  115. package/dist/builtin/workflows/builtin/classify-and-act.ts +4 -0
  116. package/dist/builtin/workflows/builtin/fan-out-and-synthesize.ts +4 -0
  117. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +12 -4
  118. package/dist/builtin/workflows/builtin/generate-and-filter.ts +4 -0
  119. package/dist/builtin/workflows/builtin/goal-models.ts +8 -10
  120. package/dist/builtin/workflows/builtin/goal.ts +4 -0
  121. package/dist/builtin/workflows/builtin/index.d.ts +0 -4
  122. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +4 -2
  123. package/dist/builtin/workflows/builtin/loop-until-done.ts +4 -0
  124. package/dist/builtin/workflows/builtin/open-claude-design-live-protocol.ts +305 -0
  125. package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +115 -171
  126. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +9 -25
  127. package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +19 -86
  128. package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +0 -6
  129. package/dist/builtin/workflows/builtin/open-claude-design.d.ts +0 -5
  130. package/dist/builtin/workflows/builtin/open-claude-design.ts +11 -15
  131. package/dist/builtin/workflows/builtin/ralph-models.ts +24 -29
  132. package/dist/builtin/workflows/builtin/ralph.ts +4 -0
  133. package/dist/builtin/workflows/builtin/tournament-runner.ts +6 -3
  134. package/dist/builtin/workflows/builtin/tournament.ts +4 -0
  135. package/dist/builtin/workflows/package.json +2 -2
  136. package/dist/builtin/workflows/skills/impeccable/SKILL.md +3 -3
  137. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +25 -31
  138. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +1 -0
  139. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +16 -10
  140. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +2 -2
  141. package/dist/builtin/workflows/skills/impeccable/reference/android.md +6 -0
  142. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +3 -0
  143. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +3 -1
  144. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +6 -1
  145. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +18 -2
  146. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +24 -30
  147. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +1 -0
  148. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +16 -10
  149. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +2 -2
  150. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +1 -1
  151. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +1 -0
  152. package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
  153. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
  154. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +12 -6
  155. package/dist/builtin/workflows/skills/impeccable/reference/init.md +12 -3
  156. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +6 -0
  157. package/dist/builtin/workflows/skills/impeccable/reference/live-setup.md +102 -0
  158. package/dist/builtin/workflows/skills/impeccable/reference/live.md +113 -521
  159. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +39 -26
  160. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
  161. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -3
  162. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +1 -1
  163. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +25 -11
  164. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +332 -221
  165. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +10 -19
  166. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +91 -38
  167. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +19 -10
  168. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +56 -6
  169. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +10 -16
  170. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +140 -2
  171. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +1174 -694
  172. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  173. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +5 -1
  174. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +431 -45
  175. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +10 -1
  176. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +47 -6
  177. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +23 -22
  178. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +0 -10
  179. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +384 -384
  180. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +466 -2
  181. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +2 -0
  182. package/dist/builtin/workflows/skills/impeccable/scripts/embed-prompt.mjs +270 -0
  183. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +51 -6
  184. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +70 -10
  185. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +336 -90
  186. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +38 -3
  187. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +77 -11
  188. package/dist/builtin/workflows/skills/impeccable/scripts/lib/design-parser.mjs +102 -19
  189. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +44 -59
  190. package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +3 -0
  191. package/dist/builtin/workflows/skills/impeccable/scripts/lib/open-system-browser.mjs +57 -0
  192. package/dist/builtin/workflows/skills/impeccable/scripts/lib/roll-selection.mjs +369 -0
  193. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  194. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +73 -2
  195. package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-css.mjs +617 -0
  196. package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-verify.mjs +71 -0
  197. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +29 -2
  198. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +54 -7
  199. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  200. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +157 -0
  201. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
  202. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/journal.mjs +205 -0
  203. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  204. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nuxt.mjs +170 -0
  205. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/script-src.mjs +26 -0
  206. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  207. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  208. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  209. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  210. package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  211. package/dist/builtin/workflows/skills/impeccable/scripts/live/instructions.mjs +169 -0
  212. package/dist/builtin/workflows/skills/impeccable/scripts/live/roots.mjs +670 -0
  213. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +232 -35
  214. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-ast.mjs +969 -0
  215. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +621 -74
  216. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +66 -28
  217. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +12 -12
  218. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  219. package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +135 -0
  220. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +23 -37
  221. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +945 -115
  222. package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +33 -1
  223. package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  224. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +173 -425
  225. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +2 -0
  226. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +16 -3
  227. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +39 -10
  228. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +194 -27
  229. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +10 -4
  230. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +50 -31
  231. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +91 -28
  232. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +8 -5
  233. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +759 -109
  234. package/dist/builtin/workflows/src/authoring/workflow.ts +9 -0
  235. package/dist/builtin/workflows/src/authoring.d.ts +3 -0
  236. package/dist/builtin/workflows/src/durable/completed-catalog.ts +23 -2
  237. package/dist/builtin/workflows/src/durable/dbos-backend.ts +30 -17
  238. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +14 -0
  239. package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +60 -57
  240. package/dist/builtin/workflows/src/durable/dbos-process-owner.ts +87 -0
  241. package/dist/builtin/workflows/src/durable/dbos-registration-diagnostics.ts +113 -0
  242. package/dist/builtin/workflows/src/durable/factory.ts +11 -8
  243. package/dist/builtin/workflows/src/durable/tool-failure-checkpoint.ts +5 -0
  244. package/dist/builtin/workflows/src/durable/tool-primitive.ts +186 -20
  245. package/dist/builtin/workflows/src/durable/types.ts +4 -0
  246. package/dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts +153 -0
  247. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +28 -0
  248. package/dist/builtin/workflows/src/engine/run-tool-control-registry.ts +9 -1
  249. package/dist/builtin/workflows/src/engine/run.ts +1 -0
  250. package/dist/builtin/workflows/src/extension/adopt-session-run-state.ts +20 -0
  251. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +9 -30
  252. package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
  253. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +28 -10
  254. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +138 -0
  255. package/dist/builtin/workflows/src/extension/index.bundle.mjs +3365 -1341
  256. package/dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts +197 -0
  257. package/dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts +115 -0
  258. package/dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts +962 -0
  259. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +4 -1
  260. package/dist/builtin/workflows/src/extension/workflow-schema.ts +29 -5
  261. package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +32 -1
  262. package/dist/builtin/workflows/src/runs/background/cancellation-registry.ts +10 -1
  263. package/dist/builtin/workflows/src/runs/background/job-tracker.ts +10 -1
  264. package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +16 -11
  265. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +31 -1
  266. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +41 -10
  267. package/dist/builtin/workflows/src/runs/foreground/stage-runner-messages.ts +48 -0
  268. package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +4 -1
  269. package/dist/builtin/workflows/src/runs/foreground/stage-runner-structured-output.ts +45 -12
  270. package/dist/builtin/workflows/src/sdk-surface.ts +9 -0
  271. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +10 -0
  272. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +10 -0
  273. package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +14 -2
  274. package/dist/builtin/workflows/src/shared/persistence-restore.ts +12 -0
  275. package/dist/builtin/workflows/src/shared/prompt-answer.ts +130 -0
  276. package/dist/builtin/workflows/src/shared/session-scoped-singleton.ts +98 -0
  277. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +9 -1
  278. package/dist/builtin/workflows/src/shared/store-factory.ts +9 -1
  279. package/dist/builtin/workflows/src/shared/store-public-types.ts +2 -1
  280. package/dist/builtin/workflows/src/shared/store-tool-node-methods.ts +7 -0
  281. package/dist/builtin/workflows/src/shared/store-types.ts +18 -1
  282. package/dist/builtin/workflows/src/shared/tool-payload-bounds.ts +576 -0
  283. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +1 -0
  284. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +1 -0
  285. package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.d.ts +21 -0
  286. package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts +25 -0
  287. package/dist/builtin/workflows/src/tui/graph-theme.ts +8 -1
  288. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +11 -0
  289. package/dist/builtin/workflows/src/tui/graph-view-input.ts +100 -5
  290. package/dist/builtin/workflows/src/tui/graph-view-layout.ts +42 -11
  291. package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +55 -6
  292. package/dist/builtin/workflows/src/tui/graph-view-render.ts +71 -6
  293. package/dist/builtin/workflows/src/tui/graph-view-state.ts +43 -0
  294. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  295. package/dist/builtin/workflows/src/tui/node-card.ts +5 -1
  296. package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +11 -2
  297. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +36 -10
  298. package/dist/builtin/workflows/src/tui/switcher.ts +11 -4
  299. package/dist/builtin/workflows/src/tui/tool-detail.ts +348 -0
  300. package/dist/builtin/workflows/src/tui/workflow-status.ts +5 -2
  301. package/dist/cli/args.d.ts +1 -0
  302. package/dist/cli/args.d.ts.map +1 -1
  303. package/dist/cli/args.js +11 -0
  304. package/dist/cli/args.js.map +1 -1
  305. package/dist/cli/auth-check.d.ts +1 -1
  306. package/dist/cli/auth-check.js.map +1 -1
  307. package/dist/cli/credential-print.d.ts +1 -1
  308. package/dist/cli/credential-print.js.map +1 -1
  309. package/dist/cli/list-models.d.ts +1 -1
  310. package/dist/cli/list-models.js.map +1 -1
  311. package/dist/cli/startup-ui.d.ts +9 -0
  312. package/dist/cli/startup-ui.d.ts.map +1 -1
  313. package/dist/cli/startup-ui.js +7 -1
  314. package/dist/cli/startup-ui.js.map +1 -1
  315. package/dist/core/agent-session-custom-message-commit.js +2 -2
  316. package/dist/core/agent-session-custom-message-commit.js.map +1 -1
  317. package/dist/core/agent-session-export.d.ts +6 -1
  318. package/dist/core/agent-session-export.d.ts.map +1 -1
  319. package/dist/core/agent-session-export.js +6 -3
  320. package/dist/core/agent-session-export.js.map +1 -1
  321. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  322. package/dist/core/agent-session-extension-bindings.js +6 -4
  323. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  324. package/dist/core/agent-session-methods.d.ts +5 -2
  325. package/dist/core/agent-session-methods.d.ts.map +1 -1
  326. package/dist/core/agent-session-methods.js.map +1 -1
  327. package/dist/core/agent-session-prompt.d.ts +12 -1
  328. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  329. package/dist/core/agent-session-prompt.js +31 -10
  330. package/dist/core/agent-session-prompt.js.map +1 -1
  331. package/dist/core/agent-session-runtime-auth.js.map +1 -1
  332. package/dist/core/agent-session-services.d.ts +1 -1
  333. package/dist/core/agent-session-services.js.map +1 -1
  334. package/dist/core/agent-session-skill-block.d.ts +1 -0
  335. package/dist/core/agent-session-skill-block.d.ts.map +1 -1
  336. package/dist/core/agent-session-skill-block.js +18 -2
  337. package/dist/core/agent-session-skill-block.js.map +1 -1
  338. package/dist/core/agent-session-state.d.ts.map +1 -1
  339. package/dist/core/agent-session-state.js +2 -1
  340. package/dist/core/agent-session-state.js.map +1 -1
  341. package/dist/core/agent-session-types.d.ts +2 -1
  342. package/dist/core/agent-session-types.d.ts.map +1 -1
  343. package/dist/core/agent-session-types.js.map +1 -1
  344. package/dist/core/agent-session.d.ts +1 -1
  345. package/dist/core/agent-session.js.map +1 -1
  346. package/dist/core/atomic-guide-command.js +2 -2
  347. package/dist/core/atomic-guide-command.js.map +1 -1
  348. package/dist/core/builtin-packages.d.ts.map +1 -1
  349. package/dist/core/builtin-packages.js +0 -6
  350. package/dist/core/builtin-packages.js.map +1 -1
  351. package/dist/core/cloudflare-gateway-binding.d.ts +11 -0
  352. package/dist/core/cloudflare-gateway-binding.d.ts.map +1 -0
  353. package/dist/core/cloudflare-gateway-binding.js +11 -0
  354. package/dist/core/cloudflare-gateway-binding.js.map +1 -0
  355. package/dist/core/codex-fast-mode-transport.d.ts +12 -0
  356. package/dist/core/codex-fast-mode-transport.d.ts.map +1 -0
  357. package/dist/core/codex-fast-mode-transport.js +130 -0
  358. package/dist/core/codex-fast-mode-transport.js.map +1 -0
  359. package/dist/core/codex-fast-mode.d.ts +20 -6
  360. package/dist/core/codex-fast-mode.d.ts.map +1 -1
  361. package/dist/core/codex-fast-mode.js +111 -3
  362. package/dist/core/codex-fast-mode.js.map +1 -1
  363. package/dist/core/diagnostics.d.ts +4 -0
  364. package/dist/core/diagnostics.d.ts.map +1 -1
  365. package/dist/core/diagnostics.js.map +1 -1
  366. package/dist/core/event-bus.d.ts +12 -0
  367. package/dist/core/event-bus.d.ts.map +1 -1
  368. package/dist/core/event-bus.js +22 -0
  369. package/dist/core/event-bus.js.map +1 -1
  370. package/dist/core/experimental.d.ts +25 -0
  371. package/dist/core/experimental.d.ts.map +1 -1
  372. package/dist/core/experimental.js +24 -0
  373. package/dist/core/experimental.js.map +1 -1
  374. package/dist/core/export-html/template-js/tree-filter-render.js +4 -3
  375. package/dist/core/export-html/template.js +4 -3
  376. package/dist/core/extension-session-state.d.ts +28 -0
  377. package/dist/core/extension-session-state.d.ts.map +1 -0
  378. package/dist/core/extension-session-state.js +55 -0
  379. package/dist/core/extension-session-state.js.map +1 -0
  380. package/dist/core/extensions/api-types.d.ts +6 -0
  381. package/dist/core/extensions/api-types.d.ts.map +1 -1
  382. package/dist/core/extensions/api-types.js.map +1 -1
  383. package/dist/core/extensions/context-types.d.ts +4 -0
  384. package/dist/core/extensions/context-types.d.ts.map +1 -1
  385. package/dist/core/extensions/context-types.js.map +1 -1
  386. package/dist/core/extensions/index.d.ts +1 -0
  387. package/dist/core/extensions/index.d.ts.map +1 -1
  388. package/dist/core/extensions/index.js +1 -0
  389. package/dist/core/extensions/index.js.map +1 -1
  390. package/dist/core/extensions/loader-api.d.ts +1 -1
  391. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  392. package/dist/core/extensions/loader-api.js +16 -10
  393. package/dist/core/extensions/loader-api.js.map +1 -1
  394. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  395. package/dist/core/extensions/loader-virtual-modules.js +12 -1
  396. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  397. package/dist/core/extensions/runner-context.d.ts +2 -0
  398. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  399. package/dist/core/extensions/runner-context.js +8 -0
  400. package/dist/core/extensions/runner-context.js.map +1 -1
  401. package/dist/core/extensions/runner.d.ts +1 -0
  402. package/dist/core/extensions/runner.d.ts.map +1 -1
  403. package/dist/core/extensions/runner.js +3 -0
  404. package/dist/core/extensions/runner.js.map +1 -1
  405. package/dist/core/extensions/runtime-types.d.ts +3 -0
  406. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  407. package/dist/core/extensions/runtime-types.js.map +1 -1
  408. package/dist/core/extensions/ui-types.d.ts +26 -0
  409. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  410. package/dist/core/extensions/ui-types.js +17 -1
  411. package/dist/core/extensions/ui-types.js.map +1 -1
  412. package/dist/core/http-dispatcher.d.ts.map +1 -1
  413. package/dist/core/http-dispatcher.js +5 -0
  414. package/dist/core/http-dispatcher.js.map +1 -1
  415. package/dist/core/keybindings.d.ts +24 -0
  416. package/dist/core/keybindings.d.ts.map +1 -1
  417. package/dist/core/keybindings.js +6 -0
  418. package/dist/core/keybindings.js.map +1 -1
  419. package/dist/core/model-config.d.ts +5 -0
  420. package/dist/core/model-config.d.ts.map +1 -1
  421. package/dist/core/model-config.js +1 -0
  422. package/dist/core/model-config.js.map +1 -1
  423. package/dist/core/model-registry.d.ts +1 -1
  424. package/dist/core/model-registry.js.map +1 -1
  425. package/dist/core/model-resolver-cli.d.ts +1 -1
  426. package/dist/core/model-resolver-cli.js.map +1 -1
  427. package/dist/core/model-resolver-defaults.js +3 -3
  428. package/dist/core/model-resolver-defaults.js.map +1 -1
  429. package/dist/core/model-resolver-initial.d.ts +1 -1
  430. package/dist/core/model-resolver-initial.js.map +1 -1
  431. package/dist/core/model-resolver-scope.d.ts +1 -1
  432. package/dist/core/model-resolver-scope.js.map +1 -1
  433. package/dist/core/model-runtime.d.ts +54 -0
  434. package/dist/core/model-runtime.d.ts.map +1 -1
  435. package/dist/core/model-runtime.js +96 -13
  436. package/dist/core/model-runtime.js.map +1 -1
  437. package/dist/core/oauth-login.js.map +1 -1
  438. package/dist/core/project-trust.d.ts +10 -0
  439. package/dist/core/project-trust.d.ts.map +1 -1
  440. package/dist/core/project-trust.js +12 -0
  441. package/dist/core/project-trust.js.map +1 -1
  442. package/dist/core/resource-loader-assets.d.ts.map +1 -1
  443. package/dist/core/resource-loader-assets.js +14 -5
  444. package/dist/core/resource-loader-assets.js.map +1 -1
  445. package/dist/core/resource-loader-core.d.ts +3 -0
  446. package/dist/core/resource-loader-core.d.ts.map +1 -1
  447. package/dist/core/resource-loader-core.js +5 -0
  448. package/dist/core/resource-loader-core.js.map +1 -1
  449. package/dist/core/resource-loader-internals.d.ts +2 -0
  450. package/dist/core/resource-loader-internals.d.ts.map +1 -1
  451. package/dist/core/resource-loader-internals.js.map +1 -1
  452. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  453. package/dist/core/resource-loader-reload.js +2 -0
  454. package/dist/core/resource-loader-reload.js.map +1 -1
  455. package/dist/core/resource-loader-types.d.ts +2 -0
  456. package/dist/core/resource-loader-types.d.ts.map +1 -1
  457. package/dist/core/resource-loader-types.js.map +1 -1
  458. package/dist/core/resource-loader.d.ts +2 -0
  459. package/dist/core/resource-loader.d.ts.map +1 -1
  460. package/dist/core/resource-loader.js +1 -0
  461. package/dist/core/resource-loader.js.map +1 -1
  462. package/dist/core/sdk-types.d.ts +7 -6
  463. package/dist/core/sdk-types.d.ts.map +1 -1
  464. package/dist/core/sdk-types.js.map +1 -1
  465. package/dist/core/sdk.d.ts.map +1 -1
  466. package/dist/core/sdk.js +17 -6
  467. package/dist/core/sdk.js.map +1 -1
  468. package/dist/core/session-manager-core.d.ts +7 -1
  469. package/dist/core/session-manager-core.d.ts.map +1 -1
  470. package/dist/core/session-manager-core.js +11 -2
  471. package/dist/core/session-manager-core.js.map +1 -1
  472. package/dist/core/session-manager-entries.d.ts +2 -2
  473. package/dist/core/session-manager-entries.d.ts.map +1 -1
  474. package/dist/core/session-manager-entries.js +6 -4
  475. package/dist/core/session-manager-entries.js.map +1 -1
  476. package/dist/core/session-manager-list.d.ts.map +1 -1
  477. package/dist/core/session-manager-list.js +5 -1
  478. package/dist/core/session-manager-list.js.map +1 -1
  479. package/dist/core/session-manager-types.d.ts +25 -2
  480. package/dist/core/session-manager-types.d.ts.map +1 -1
  481. package/dist/core/session-manager-types.js.map +1 -1
  482. package/dist/core/session-manager.d.ts +1 -1
  483. package/dist/core/session-manager.d.ts.map +1 -1
  484. package/dist/core/session-manager.js.map +1 -1
  485. package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
  486. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  487. package/dist/core/settings-manager-basic-accessors.js +8 -0
  488. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  489. package/dist/core/settings-manager-ui-accessors.d.ts +3 -1
  490. package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
  491. package/dist/core/settings-manager-ui-accessors.js +9 -0
  492. package/dist/core/settings-manager-ui-accessors.js.map +1 -1
  493. package/dist/core/settings-manager.d.ts +1 -1
  494. package/dist/core/settings-manager.d.ts.map +1 -1
  495. package/dist/core/settings-manager.js.map +1 -1
  496. package/dist/core/settings-types.d.ts +4 -0
  497. package/dist/core/settings-types.d.ts.map +1 -1
  498. package/dist/core/settings-types.js.map +1 -1
  499. package/dist/core/skill-catalog.d.ts +35 -0
  500. package/dist/core/skill-catalog.d.ts.map +1 -0
  501. package/dist/core/skill-catalog.js +264 -0
  502. package/dist/core/skill-catalog.js.map +1 -0
  503. package/dist/core/skills-async.d.ts.map +1 -1
  504. package/dist/core/skills-async.js +12 -6
  505. package/dist/core/skills-async.js.map +1 -1
  506. package/dist/core/skills.d.ts +2 -0
  507. package/dist/core/skills.d.ts.map +1 -1
  508. package/dist/core/skills.js +8 -5
  509. package/dist/core/skills.js.map +1 -1
  510. package/dist/core/slash-commands.d.ts.map +1 -1
  511. package/dist/core/slash-commands.js +1 -5
  512. package/dist/core/slash-commands.js.map +1 -1
  513. package/dist/core/system-prompt.js +2 -2
  514. package/dist/core/system-prompt.js.map +1 -1
  515. package/dist/core/tools/ask-user-question/ask-user-question.d.ts +25 -2
  516. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  517. package/dist/core/tools/ask-user-question/ask-user-question.js +47 -18
  518. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  519. package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -1
  520. package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +5 -2
  521. package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -1
  522. package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -1
  523. package/dist/core/tools/ask-user-question/view/components/submit-picker.js +3 -1
  524. package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -1
  525. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  526. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +8 -1
  527. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  528. package/dist/core/tools/bash.d.ts.map +1 -1
  529. package/dist/core/tools/bash.js +2 -0
  530. package/dist/core/tools/bash.js.map +1 -1
  531. package/dist/core/tools/edit.d.ts.map +1 -1
  532. package/dist/core/tools/edit.js +2 -0
  533. package/dist/core/tools/edit.js.map +1 -1
  534. package/dist/core/tools/find.d.ts.map +1 -1
  535. package/dist/core/tools/find.js +3 -1
  536. package/dist/core/tools/find.js.map +1 -1
  537. package/dist/core/tools/grep.js +1 -1
  538. package/dist/core/tools/grep.js.map +1 -1
  539. package/dist/core/tools/hashline-engine/format.d.ts +2 -0
  540. package/dist/core/tools/hashline-engine/format.d.ts.map +1 -1
  541. package/dist/core/tools/hashline-engine/format.js +4 -2
  542. package/dist/core/tools/hashline-engine/format.js.map +1 -1
  543. package/dist/core/tools/hashline-engine/messages.d.ts +2 -0
  544. package/dist/core/tools/hashline-engine/messages.d.ts.map +1 -1
  545. package/dist/core/tools/hashline-engine/messages.js +4 -2
  546. package/dist/core/tools/hashline-engine/messages.js.map +1 -1
  547. package/dist/core/tools/hashline-engine/parser.d.ts.map +1 -1
  548. package/dist/core/tools/hashline-engine/parser.js +16 -4
  549. package/dist/core/tools/hashline-engine/parser.js.map +1 -1
  550. package/dist/core/tools/hashline-engine/tokenizer.d.ts.map +1 -1
  551. package/dist/core/tools/hashline-engine/tokenizer.js +37 -27
  552. package/dist/core/tools/hashline-engine/tokenizer.js.map +1 -1
  553. package/dist/core/tools/hashline.d.ts.map +1 -1
  554. package/dist/core/tools/hashline.js +8 -2
  555. package/dist/core/tools/hashline.js.map +1 -1
  556. package/dist/core/tools/ls.d.ts.map +1 -1
  557. package/dist/core/tools/ls.js +2 -0
  558. package/dist/core/tools/ls.js.map +1 -1
  559. package/dist/core/tools/read.d.ts.map +1 -1
  560. package/dist/core/tools/read.js +8 -1
  561. package/dist/core/tools/read.js.map +1 -1
  562. package/dist/core/tools/search.d.ts.map +1 -1
  563. package/dist/core/tools/search.js +2 -0
  564. package/dist/core/tools/search.js.map +1 -1
  565. package/dist/core/tools/todos.d.ts +3 -12
  566. package/dist/core/tools/todos.d.ts.map +1 -1
  567. package/dist/core/tools/todos.js +19 -4
  568. package/dist/core/tools/todos.js.map +1 -1
  569. package/dist/core/tools/write.d.ts.map +1 -1
  570. package/dist/core/tools/write.js +2 -0
  571. package/dist/core/tools/write.js.map +1 -1
  572. package/dist/index-extensions.d.ts +1 -1
  573. package/dist/index-extensions.d.ts.map +1 -1
  574. package/dist/index-extensions.js +1 -1
  575. package/dist/index-extensions.js.map +1 -1
  576. package/dist/index.d.ts +8 -5
  577. package/dist/index.d.ts.map +1 -1
  578. package/dist/index.js +6 -2
  579. package/dist/index.js.map +1 -1
  580. package/dist/main-runtime-api-key.d.ts +1 -1
  581. package/dist/main-runtime-api-key.js.map +1 -1
  582. package/dist/main-session-options.d.ts +1 -1
  583. package/dist/main-session-options.js.map +1 -1
  584. package/dist/main.d.ts.map +1 -1
  585. package/dist/main.js +10 -2
  586. package/dist/main.js.map +1 -1
  587. package/dist/modes/interactive/components/chat-session-host-rendering.js +1 -1
  588. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  589. package/dist/modes/interactive/components/chat-session-host-state.d.ts +8 -0
  590. package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
  591. package/dist/modes/interactive/components/chat-session-host-state.js +8 -0
  592. package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
  593. package/dist/modes/interactive/components/chat-session-host.d.ts +26 -0
  594. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  595. package/dist/modes/interactive/components/chat-session-host.js +38 -0
  596. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  597. package/dist/modes/interactive/components/chat-transcript.d.ts +37 -0
  598. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  599. package/dist/modes/interactive/components/chat-transcript.js +54 -1
  600. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  601. package/dist/modes/interactive/components/fast-mode-selector.js +1 -1
  602. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -1
  603. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  604. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  605. package/dist/modes/interactive/components/model-selector.js +2 -1
  606. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  607. package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
  608. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  609. package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts +95 -0
  610. package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts.map +1 -0
  611. package/dist/modes/interactive/components/reserved-bottom-overlay.js +234 -0
  612. package/dist/modes/interactive/components/reserved-bottom-overlay.js.map +1 -0
  613. package/dist/modes/interactive/components/session-selector-search.d.ts +4 -0
  614. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  615. package/dist/modes/interactive/components/session-selector-search.js +4 -0
  616. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  617. package/dist/modes/interactive/components/settings-selector-handlers.d.ts.map +1 -1
  618. package/dist/modes/interactive/components/settings-selector-handlers.js +3 -0
  619. package/dist/modes/interactive/components/settings-selector-handlers.js.map +1 -1
  620. package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
  621. package/dist/modes/interactive/components/settings-selector-items.js +7 -0
  622. package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
  623. package/dist/modes/interactive/components/settings-selector-types.d.ts +3 -1
  624. package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
  625. package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
  626. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  627. package/dist/modes/interactive/components/tool-execution.js +12 -1
  628. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  629. package/dist/modes/interactive/interactive-auth-login.js.map +1 -1
  630. package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
  631. package/dist/modes/interactive/interactive-autocomplete.js +5 -4
  632. package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  633. package/dist/modes/interactive/interactive-bash-compact.d.ts.map +1 -1
  634. package/dist/modes/interactive/interactive-bash-compact.js +2 -2
  635. package/dist/modes/interactive/interactive-bash-compact.js.map +1 -1
  636. package/dist/modes/interactive/interactive-extension-custom-ui.js +94 -6
  637. package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
  638. package/dist/modes/interactive/interactive-mode-base.d.ts +41 -3
  639. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  640. package/dist/modes/interactive/interactive-mode-base.js +80 -11
  641. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  642. package/dist/modes/interactive/interactive-mode-surface.d.ts +9 -1
  643. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  644. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  645. package/dist/modes/interactive/interactive-mode-types.d.ts +2 -0
  646. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  647. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  648. package/dist/modes/interactive/interactive-model-catalog-startup.d.ts +8 -1
  649. package/dist/modes/interactive/interactive-model-catalog-startup.d.ts.map +1 -1
  650. package/dist/modes/interactive/interactive-model-catalog-startup.js +13 -3
  651. package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -1
  652. package/dist/modes/interactive/interactive-model-routing.js +3 -4
  653. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  654. package/dist/modes/interactive/interactive-render-chat.js +6 -0
  655. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  656. package/dist/modes/interactive/interactive-resource-disclosure.js +2 -2
  657. package/dist/modes/interactive/interactive-resource-disclosure.js.map +1 -1
  658. package/dist/modes/interactive/interactive-selectors.js +7 -3
  659. package/dist/modes/interactive/interactive-selectors.js.map +1 -1
  660. package/dist/modes/interactive/interactive-session-runtime.js +7 -1
  661. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  662. package/dist/modes/interactive/interactive-slash-commands.js +2 -2
  663. package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
  664. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  665. package/dist/modes/interactive/interactive-startup.js +34 -9
  666. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  667. package/dist/modes/interactive/interactive-tui.d.ts +21 -1
  668. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  669. package/dist/modes/interactive/interactive-tui.js +140 -38
  670. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  671. package/dist/modes/interactive/model-catalog-refresh.d.ts +17 -0
  672. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  673. package/dist/modes/interactive/model-catalog-refresh.js +84 -0
  674. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  675. package/dist/modes/interactive/theme/dark.json +2 -0
  676. package/dist/modes/interactive/theme/light.json +2 -0
  677. package/dist/modes/interactive/theme/theme-class.d.ts +12 -3
  678. package/dist/modes/interactive/theme/theme-class.d.ts.map +1 -1
  679. package/dist/modes/interactive/theme/theme-class.js +6 -1
  680. package/dist/modes/interactive/theme/theme-class.js.map +1 -1
  681. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  682. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  683. package/dist/modes/interactive/theme/theme-controller.js +23 -10
  684. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  685. package/dist/modes/interactive/theme/theme-loading.d.ts.map +1 -1
  686. package/dist/modes/interactive/theme/theme-loading.js +1 -0
  687. package/dist/modes/interactive/theme/theme-loading.js.map +1 -1
  688. package/dist/modes/interactive/theme/theme-schema.d.ts +8 -0
  689. package/dist/modes/interactive/theme/theme-schema.d.ts.map +1 -1
  690. package/dist/modes/interactive/theme/theme-schema.js +3 -1
  691. package/dist/modes/interactive/theme/theme-schema.js.map +1 -1
  692. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  693. package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
  694. package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
  695. package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
  696. package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
  697. package/dist/modes/interactive-engine/protocol.d.ts +1 -0
  698. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  699. package/dist/modes/interactive-engine/protocol.js +1 -0
  700. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  701. package/dist/modes/interactive-engine/remote-component.d.ts +3 -1
  702. package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
  703. package/dist/modes/interactive-engine/remote-component.js +3 -2
  704. package/dist/modes/interactive-engine/remote-component.js.map +1 -1
  705. package/dist/modes/json-event.d.ts +6 -1
  706. package/dist/modes/json-event.d.ts.map +1 -1
  707. package/dist/modes/json-event.js +17 -2
  708. package/dist/modes/json-event.js.map +1 -1
  709. package/dist/modes/rpc/rpc-client.js.map +1 -1
  710. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  711. package/dist/modes/rpc/rpc-command-handler.js +5 -4
  712. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  713. package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
  714. package/dist/modes/rpc/rpc-responses.js.map +1 -1
  715. package/dist/modes/rpc/rpc-types.d.ts +1 -1
  716. package/dist/modes/rpc/rpc-types.js.map +1 -1
  717. package/dist/package-manager-cli.js.map +1 -1
  718. package/dist/server/create-harness.d.ts.map +1 -1
  719. package/dist/server/create-harness.js +11 -0
  720. package/dist/server/create-harness.js.map +1 -1
  721. package/dist/utils/abort.d.ts +9 -0
  722. package/dist/utils/abort.d.ts.map +1 -0
  723. package/dist/utils/abort.js +19 -0
  724. package/dist/utils/abort.js.map +1 -0
  725. package/dist/utils/tools-manager.d.ts +16 -1
  726. package/dist/utils/tools-manager.d.ts.map +1 -1
  727. package/dist/utils/tools-manager.js +19 -19
  728. package/dist/utils/tools-manager.js.map +1 -1
  729. package/docs/custom-provider.md +9 -8
  730. package/docs/development.md +0 -1
  731. package/docs/docs.json +0 -1
  732. package/docs/environment-variables.md +3 -0
  733. package/docs/extensions.md +100 -2
  734. package/docs/json.md +3 -1
  735. package/docs/keybindings.md +8 -3
  736. package/docs/models/artificial-analysis-index.md +2 -2
  737. package/docs/models/model-selection.md +32 -23
  738. package/docs/models/pareto-efficiency.md +25 -24
  739. package/docs/providers.md +95 -2
  740. package/docs/quickstart.md +4 -2
  741. package/docs/rpc.md +21 -7
  742. package/docs/sdk.md +26 -1
  743. package/docs/session-format.md +5 -2
  744. package/docs/settings.md +34 -3
  745. package/docs/skills.md +16 -1
  746. package/docs/terminal-setup.md +15 -2
  747. package/docs/themes.md +22 -4
  748. package/docs/tools.md +3 -1
  749. package/docs/tui.md +2 -0
  750. package/docs/usage.md +5 -4
  751. package/docs/workflows.md +96 -19
  752. package/npm-shrinkwrap.json +211 -232
  753. package/package.json +8 -8
  754. package/dist/builtin/i-have-adhd/CHANGELOG.md +0 -30
  755. package/dist/builtin/i-have-adhd/LICENSE +0 -21
  756. package/dist/builtin/i-have-adhd/README.md +0 -18
  757. package/dist/builtin/i-have-adhd/index.ts +0 -238
  758. package/dist/builtin/i-have-adhd/package.json +0 -56
  759. package/dist/builtin/i-have-adhd/skills/i-have-adhd/SKILL.md +0 -140
  760. package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +0 -359
  761. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +0 -180
  762. package/docs/i-have-adhd.md +0 -36
@@ -11,14 +11,21 @@ import {
11
11
  isBrandFontOnOwnDomain,
12
12
  } from '../shared/constants.mjs';
13
13
  import {
14
+ CSS_NAMED_COLORS,
14
15
  colorToHex,
16
+ compositeColorOver,
15
17
  contrastRatio,
16
18
  getHue,
17
19
  hasChroma,
18
20
  isNeutralColor,
21
+ isNoPaintColorValue,
22
+ oklchToRgb,
23
+ parseAnyColor,
24
+ parseColorMix,
19
25
  parseGradientColors,
20
26
  parseRgb,
21
27
  relativeLuminance,
28
+ splitTopLevelCommas,
22
29
  } from '../shared/color.mjs';
23
30
  import { extractGoogleFontFamilies } from '../shared/fonts.mjs';
24
31
  import { stripHtmlBlocks } from '../shared/page.mjs';
@@ -71,6 +78,34 @@ function checkBorders(tag, widths, colors, radius, opts = {}) {
71
78
  return findings;
72
79
  }
73
80
 
81
+ // ─── Scoped ignores: data-impeccable-ignore ─────────────────────────────────
82
+ //
83
+ // An element-scoped waiver that travels with the markup: any element carrying
84
+ // `data-impeccable-ignore="rule-a rule-b"` (or `*`, or an empty value, for
85
+ // every rule) suppresses matching findings from itself and its entire subtree,
86
+ // in every engine that walks elements — the browser overlay, the extension,
87
+ // and the static scan. This is the DOM twin of the line-based
88
+ // `impeccable-disable` comment directives, which the browser cannot apply (a
89
+ // live DOM has no line numbers), and the generalization of the one-off
90
+ // `data-impeccable-allow-kickers` opt-out.
91
+ //
92
+ // The intended use is curated exhibits: a page that documents anti-patterns by
93
+ // example, or renders a deliberate "before" specimen, marks the container once
94
+ // and every engine skips it while still scanning the page around it.
95
+ function scopedIgnoreActive(el, ruleId) {
96
+ const rule = String(ruleId || '').toLowerCase();
97
+ let cur = el;
98
+ while (cur && cur.nodeType === 1) {
99
+ const attr = typeof cur.getAttribute === 'function' ? cur.getAttribute('data-impeccable-ignore') : null;
100
+ if (attr != null) {
101
+ const rules = String(attr).trim().toLowerCase().split(/[\s,]+/).filter(Boolean);
102
+ if (rules.length === 0 || rules.includes('*') || rules.includes(rule)) return true;
103
+ }
104
+ cur = cur.parentElement;
105
+ }
106
+ return false;
107
+ }
108
+
74
109
  // Returns true if the given text is composed entirely of emoji characters
75
110
  // (plus whitespace / variation selectors). Emojis render as multicolor glyphs
76
111
  // regardless of CSS `color`, so contrast checks against the element's text
@@ -638,6 +673,26 @@ function cssTextHasDarkRootBg(content, customProps) {
638
673
  return false;
639
674
  }
640
675
 
676
+ // Best-effort extraction of the CSS selector whose declaration block contains
677
+ // the given index in raw CSS text. Lets CSS-text findings carry a live-DOM
678
+ // anchor, so the browser pass can resolve scoped ignores against the actual
679
+ // element and drop patterns that render nowhere on the page. Returns null for
680
+ // @-rule preludes, keyframe steps, nested blocks, and anything that does not
681
+ // read as a selector; those findings stay page-level.
682
+ function enclosingCssSelector(cssText, index) {
683
+ if (!cssText || !Number.isFinite(index)) return null;
684
+ const open = cssText.lastIndexOf('{', index);
685
+ if (open === -1) return null;
686
+ const prevClose = Math.max(cssText.lastIndexOf('}', open - 1), cssText.lastIndexOf(';', open - 1));
687
+ const raw = cssText.slice(prevClose + 1, open).trim().replace(/\s+/g, ' ');
688
+ if (!raw || raw.startsWith('@') || /^\d/.test(raw) || /[{}<]/.test(raw)) return null;
689
+ // Keyframe steps: percentage steps fail the digit test above, but `from`
690
+ // and `to` would read as (never-matching) type selectors and get a valid
691
+ // finding wrongly dropped by the zero-match rule downstream.
692
+ if (/^(?:from|to)(?:\s*,\s*(?:from|to))*$/i.test(raw)) return null;
693
+ return raw;
694
+ }
695
+
641
696
  function scanCssTextForGlow(content) {
642
697
  const customProps = collectCssCustomProps(content);
643
698
  const hasDarkBg = cssTextHasDarkRootBg(content, customProps);
@@ -949,6 +1004,7 @@ function scanCssTextForPseudoStripe(rawContent) {
949
1004
  id: 'side-tab',
950
1005
  snippet: `${selector} — absolute ${thicknessPx}px pseudo-element stripe (${edge}: 0)`,
951
1006
  index: selectorStart,
1007
+ selector,
952
1008
  });
953
1009
  }
954
1010
  return findings;
@@ -1011,6 +1067,7 @@ function scanCssTextForInsetStripe(content) {
1011
1067
  findings.push({
1012
1068
  id: 'side-tab',
1013
1069
  snippet: `${selector} — inset box-shadow ${ay === 0 ? ax : ay}px stripe (${edge})`,
1070
+ selector,
1014
1071
  });
1015
1072
  break;
1016
1073
  }
@@ -1063,10 +1120,12 @@ function collectMarqueeKeyframes(content) {
1063
1120
  // bound to a keyframe loop that travels a large horizontal distance.
1064
1121
  // Rotation/opacity animations never qualify (no X travel); JS-driven
1065
1122
  // carousels with user controls have no infinite CSS X-loop to match.
1066
- function scanCssTextForMarquee(content) {
1123
+ // `content` is CSS-bearing text; `markup` (defaulting to the same string
1124
+ // for single-corpus callers) is where the <marquee> tag itself lives.
1125
+ function scanCssTextForMarquee(content, markup = content) {
1067
1126
  const findings = [];
1068
- if (/<marquee\b/i.test(content)) {
1069
- findings.push({ id: 'marquee', snippet: '<marquee> element' });
1127
+ if (/<marquee\b/i.test(markup)) {
1128
+ findings.push({ id: 'marquee', snippet: '<marquee> element', selector: 'marquee' });
1070
1129
  }
1071
1130
  const marqueeKeyframes = collectMarqueeKeyframes(content);
1072
1131
  if (marqueeKeyframes.size === 0) return findings;
@@ -1081,7 +1140,7 @@ function scanCssTextForMarquee(content) {
1081
1140
  const key = `${selector} ${name}`;
1082
1141
  if (seen.has(key)) continue;
1083
1142
  seen.add(key);
1084
- findings.push({ id: 'marquee', snippet: `${selector} — infinite horizontal loop animation "${name}"` });
1143
+ findings.push({ id: 'marquee', snippet: `${selector} — infinite horizontal loop animation "${name}"`, selector });
1085
1144
  }
1086
1145
  }
1087
1146
  return findings;
@@ -1244,10 +1303,14 @@ function selectorHitsLandmark(content, selector, ranges) {
1244
1303
  // element sits inside a header/nav landmark is the hero liveness cliché
1245
1304
  // and is promoted to error severity; occurrences elsewhere keep the
1246
1305
  // registry default severity.
1247
- function scanCssTextForPulsingDot(content) {
1306
+ //
1307
+ // `content` is CSS-bearing text (rules and keyframes); `markup` — defaulting
1308
+ // to the same string for single-corpus callers like the regex source
1309
+ // engine — is where landmark ranges and Tailwind class attributes live.
1310
+ function scanCssTextForPulsingDot(content, markup = content) {
1248
1311
  const customProps = collectCssCustomProps(content);
1249
1312
  const keyframes = collectPulseKeyframes(content);
1250
- const heroRanges = landmarkSourceRanges(content);
1313
+ const heroRanges = landmarkSourceRanges(markup);
1251
1314
  const findings = [];
1252
1315
  const seen = new Set();
1253
1316
 
@@ -1296,7 +1359,7 @@ function scanCssTextForPulsingDot(content) {
1296
1359
 
1297
1360
  if (seen.has(selector)) continue;
1298
1361
  seen.add(selector);
1299
- const inLandmark = selectorHitsLandmark(content, selector, heroRanges);
1362
+ const inLandmark = selectorHitsLandmark(markup, selector, heroRanges);
1300
1363
  findings.push({
1301
1364
  id: 'pulsing-dot',
1302
1365
  snippet: `${selector} — ${w}x${h}px dot with infinite "${pulseName}" animation${inLandmark ? ' in header/nav' : ''}`,
@@ -1306,10 +1369,11 @@ function scanCssTextForPulsingDot(content) {
1306
1369
  }
1307
1370
 
1308
1371
  // Tailwind utilities: animate-ping / animate-pulse on a tiny rounded-full
1309
- // element declared entirely in the class attribute.
1372
+ // element declared entirely in the class attribute. Scanned in the markup
1373
+ // corpus so the match index lines up with the landmark ranges.
1310
1374
  const classRe = /class\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
1311
1375
  let cm;
1312
- while ((cm = classRe.exec(content)) !== null) {
1376
+ while ((cm = classRe.exec(markup)) !== null) {
1313
1377
  const cls = cm[1] || cm[2] || '';
1314
1378
  const anim = cls.match(/\banimate-(ping|pulse)\b/);
1315
1379
  if (!anim) continue;
@@ -1390,36 +1454,82 @@ function scanHtmlForShapeAssembledIllustration(html) {
1390
1454
  return findings;
1391
1455
  }
1392
1456
 
1457
+ // Scoped scan corpora for the page-level pattern checks. CSS-property
1458
+ // regexes run over the whole source string fire on documentation ABOUT
1459
+ // css — `<code>background-clip: text</code>` prose, <pre> samples, HTML
1460
+ // comments — so the checks scan only the strings that actually style the
1461
+ // page:
1462
+ // styleText — <style> block contents plus style="…" attribute values.
1463
+ // Attribute values keep their `style="…"` form so block-scoped
1464
+ // scanners (grid background) keep treating each attribute as one
1465
+ // declaration block, exactly as they did against raw source. Engines
1466
+ // that already read more CSS (linked stylesheets) prepend it.
1467
+ // classText — class attribute values, for utility-class scans.
1468
+ // Markup-shaped checks (inline <svg> scenes, <img> tags, <marquee>,
1469
+ // landmark ranges) and rendered-text checks (theater phrases) keep the
1470
+ // full source. This extraction serves callers without a parsed document
1471
+ // (the browser bundle scanning outerHTML); attribute reads are tag-scoped
1472
+ // so escaped code samples (&lt;div style="…"&gt;) never contribute. The
1473
+ // static engine passes richer corpora built from its parsed document.
1474
+ // Bare CSS input (no markup at all) is its own style text, which keeps
1475
+ // direct checkHtmlPatterns(css) callers behaving as before.
1476
+ function buildHtmlPatternCorpora(html) {
1477
+ const source = String(html || '');
1478
+ if (!/<[a-zA-Z!/]/.test(source)) {
1479
+ return { styleText: source, classText: source };
1480
+ }
1481
+ const styleParts = [];
1482
+ const classParts = [];
1483
+ const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
1484
+ let m;
1485
+ while ((m = styleBlockRe.exec(source)) !== null) styleParts.push(m[1]);
1486
+ const tagRe = /<[a-zA-Z][^>]*>/g;
1487
+ while ((m = tagRe.exec(source)) !== null) {
1488
+ const tag = m[0];
1489
+ const sm = tag.match(/\bstyle\s*=\s*("[^"]*"|'[^']*')/i);
1490
+ if (sm) styleParts.push(`style=${sm[1]}`);
1491
+ const cm = tag.match(/\bclass\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
1492
+ if (cm) classParts.push(cm[1] ?? cm[2] ?? '');
1493
+ }
1494
+ return { styleText: styleParts.join('\n'), classText: classParts.join('\n') };
1495
+ }
1496
+
1393
1497
  /**
1394
1498
  * Regex-on-HTML checks shared between browser and Node page-level detection.
1395
- * These don't need DOM access, just the raw HTML string.
1499
+ * These don't need DOM access, just the raw HTML string. CSS-property and
1500
+ * utility-class patterns scan the scoped corpora (styleText / classText —
1501
+ * see buildHtmlPatternCorpora) so prose about css never flags; only the
1502
+ * markup-shaped and rendered-text checks read the full source.
1396
1503
  */
1397
- function checkHtmlPatterns(html) {
1504
+ function checkHtmlPatterns(html, corpora) {
1505
+ const { styleText, classText } = corpora || buildHtmlPatternCorpora(html);
1398
1506
  const findings = [];
1399
1507
 
1400
1508
  // --- Color ---
1401
1509
 
1402
1510
  // AI color palette: purple/violet
1403
1511
  const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi;
1404
- if (purpleHexRe.test(html)) {
1512
+ if (purpleHexRe.test(styleText)) {
1405
1513
  const purpleTextRe = /(?:(?:^|;)\s*color\s*:\s*(?:.*?)(?:#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9))|gradient.*?#(?:7c3aed|8b5cf6|a855f7|764ba2|667eea))/gi;
1406
- if (purpleTextRe.test(html)) {
1407
- findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet accent colors detected' });
1514
+ purpleTextRe.lastIndex = 0;
1515
+ const purpleMatch = purpleTextRe.exec(styleText);
1516
+ if (purpleMatch) {
1517
+ findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet accent colors detected', selector: enclosingCssSelector(styleText, purpleMatch.index + 1) || undefined });
1408
1518
  }
1409
1519
  }
1410
1520
 
1411
1521
  // Gradient text (background-clip: text + gradient)
1412
1522
  const gradientRe = /(?:-webkit-)?background-clip\s*:\s*text/gi;
1413
1523
  let gm;
1414
- while ((gm = gradientRe.exec(html)) !== null) {
1524
+ while ((gm = gradientRe.exec(styleText)) !== null) {
1415
1525
  const start = Math.max(0, gm.index - 200);
1416
- const context = html.substring(start, gm.index + gm[0].length + 200);
1526
+ const context = styleText.substring(start, gm.index + gm[0].length + 200);
1417
1527
  if (/gradient/i.test(context)) {
1418
- findings.push({ id: 'gradient-text', snippet: 'background-clip: text + gradient' });
1528
+ findings.push({ id: 'gradient-text', snippet: 'background-clip: text + gradient', selector: enclosingCssSelector(styleText, gm.index) || undefined });
1419
1529
  break;
1420
1530
  }
1421
1531
  }
1422
- if (/\bbg-clip-text\b/.test(html) && /\bbg-gradient-to-/.test(html)) {
1532
+ if (/\bbg-clip-text\b/.test(classText) && /\bbg-gradient-to-/.test(classText)) {
1423
1533
  findings.push({ id: 'gradient-text', snippet: 'bg-clip-text + bg-gradient (Tailwind)' });
1424
1534
  }
1425
1535
 
@@ -1428,10 +1538,10 @@ function checkHtmlPatterns(html) {
1428
1538
  // Side-tab accent stripe drawn as an absolutely-positioned pseudo-element
1429
1539
  // (no border property involved, so the element-level border checks and
1430
1540
  // the border-left regexes never see it).
1431
- findings.push(...scanCssTextForPseudoStripe(html));
1541
+ findings.push(...scanCssTextForPseudoStripe(styleText));
1432
1542
 
1433
1543
  // Side-tab accent stripe drawn as a single-edge inset box-shadow.
1434
- findings.push(...scanCssTextForInsetStripe(html));
1544
+ findings.push(...scanCssTextForInsetStripe(styleText));
1435
1545
 
1436
1546
  // --- Layout ---
1437
1547
 
@@ -1439,20 +1549,20 @@ function checkHtmlPatterns(html) {
1439
1549
  const spacingValues = [];
1440
1550
  const spacingRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*(\d+)px/gi;
1441
1551
  let sm;
1442
- while ((sm = spacingRe.exec(html)) !== null) {
1552
+ while ((sm = spacingRe.exec(styleText)) !== null) {
1443
1553
  const v = parseInt(sm[1], 10);
1444
1554
  if (v > 0 && v < 200) spacingValues.push(v);
1445
1555
  }
1446
1556
  const gapRe = /gap\s*:\s*(\d+)px/gi;
1447
- while ((sm = gapRe.exec(html)) !== null) {
1557
+ while ((sm = gapRe.exec(styleText)) !== null) {
1448
1558
  spacingValues.push(parseInt(sm[1], 10));
1449
1559
  }
1450
1560
  const twSpaceRe = /\b(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)\b/g;
1451
- while ((sm = twSpaceRe.exec(html)) !== null) {
1561
+ while ((sm = twSpaceRe.exec(classText)) !== null) {
1452
1562
  spacingValues.push(parseInt(sm[1], 10) * 4);
1453
1563
  }
1454
1564
  const remSpacingRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*([\d.]+)rem/gi;
1455
- while ((sm = remSpacingRe.exec(html)) !== null) {
1565
+ while ((sm = remSpacingRe.exec(styleText)) !== null) {
1456
1566
  const v = Math.round(parseFloat(sm[1]) * 16);
1457
1567
  if (v > 0 && v < 200) spacingValues.push(v);
1458
1568
  }
@@ -1476,21 +1586,21 @@ function checkHtmlPatterns(html) {
1476
1586
 
1477
1587
  // Bounce/elastic animation names
1478
1588
  const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
1479
- const bounceMatch = bounceRe.exec(html);
1589
+ const bounceMatch = bounceRe.exec(styleText);
1480
1590
  if (bounceMatch) {
1481
1591
  const animationToken = bounceMatch[1]
1482
1592
  .split(/[,\s]+/)
1483
1593
  .find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
1484
- findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
1594
+ findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}`, selector: enclosingCssSelector(styleText, bounceMatch.index) || undefined });
1485
1595
  }
1486
1596
 
1487
1597
  // Overshoot cubic-bezier
1488
1598
  const bezierRe = /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g;
1489
1599
  let bm;
1490
- while ((bm = bezierRe.exec(html)) !== null) {
1600
+ while ((bm = bezierRe.exec(styleText)) !== null) {
1491
1601
  const y1 = parseFloat(bm[2]), y2 = parseFloat(bm[4]);
1492
1602
  if (y1 < -0.1 || y1 > 1.1 || y2 < -0.1 || y2 > 1.1) {
1493
- findings.push({ id: 'bounce-easing', snippet: `cubic-bezier(${bm[1]}, ${bm[2]}, ${bm[3]}, ${bm[4]})` });
1603
+ findings.push({ id: 'bounce-easing', snippet: `cubic-bezier(${bm[1]}, ${bm[2]}, ${bm[3]}, ${bm[4]})`, selector: enclosingCssSelector(styleText, bm.index) || undefined });
1494
1604
  break;
1495
1605
  }
1496
1606
  }
@@ -1498,7 +1608,7 @@ function checkHtmlPatterns(html) {
1498
1608
  // Layout property transitions
1499
1609
  const transRe = /transition(?:-property)?\s*:\s*([^;{}]+)/gi;
1500
1610
  let tm;
1501
- while ((tm = transRe.exec(html)) !== null) {
1611
+ while ((tm = transRe.exec(styleText)) !== null) {
1502
1612
  const val = tm[1].toLowerCase();
1503
1613
  if (/\ball\b/.test(val)) continue;
1504
1614
  const found = val.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
@@ -1508,31 +1618,36 @@ function checkHtmlPatterns(html) {
1508
1618
  }
1509
1619
  }
1510
1620
 
1511
- // Pulsing status dots (tiny circular elements on infinite pulse animations)
1512
- findings.push(...scanCssTextForPulsingDot(html));
1621
+ // Pulsing status dots (tiny circular elements on infinite pulse animations).
1622
+ // The CSS rules come from styleText; the markup carries the landmark
1623
+ // ranges and Tailwind class attributes.
1624
+ findings.push(...scanCssTextForPulsingDot(styleText, html));
1513
1625
 
1514
1626
  // Shape-assembled illustrations (large pictorial SVGs built from primitives)
1515
1627
  findings.push(...scanHtmlForShapeAssembledIllustration(html));
1516
1628
 
1517
1629
  // Auto-scrolling marquees (<marquee> or infinite horizontal loop animations)
1518
- findings.push(...scanCssTextForMarquee(html));
1630
+ findings.push(...scanCssTextForMarquee(styleText, html));
1519
1631
 
1520
1632
  // --- Dark glow / chromatic halo shadows ---
1521
1633
 
1522
- const glowHits = scanCssTextForGlow(html);
1634
+ const glowHits = scanCssTextForGlow(styleText);
1523
1635
  if (glowHits.length > 0) {
1524
- findings.push({ id: 'dark-glow', snippet: glowHits[0].snippet });
1636
+ findings.push({ id: 'dark-glow', snippet: glowHits[0].snippet, selector: enclosingCssSelector(styleText, glowHits[0].index) || undefined });
1525
1637
  }
1526
1638
 
1527
1639
  // Radial-gradient background halo (gradient-drawn sibling of dark-glow)
1528
- const haloHits = scanCssTextForRadialHalo(html);
1640
+ const haloHits = scanCssTextForRadialHalo(styleText);
1529
1641
  if (haloHits.length > 0) {
1530
- findings.push({ id: 'radial-halo', snippet: haloHits[0].snippet });
1642
+ findings.push({ id: 'radial-halo', snippet: haloHits[0].snippet, selector: enclosingCssSelector(styleText, haloHits[0].index) || undefined });
1531
1643
  }
1532
1644
 
1533
1645
  // --- Generated-UI tells: repeating-gradient stripes ---
1534
- if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) {
1535
- findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' });
1646
+ {
1647
+ const stripesMatch = /repeating-(?:linear|radial|conic)-gradient\s*\(/i.exec(styleText);
1648
+ if (stripesMatch) {
1649
+ findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes', selector: enclosingCssSelector(styleText, stripesMatch.index) || undefined });
1650
+ }
1536
1651
  }
1537
1652
 
1538
1653
  // --- Generated-UI tells: two-axis grid-line background ---
@@ -1548,9 +1663,9 @@ function checkHtmlPatterns(html) {
1548
1663
  // in for the second axis. Colors like `oklch(96% 0.012 82 / 0.055)` carry
1549
1664
  // nested parens, so match the hairline stop directly rather than parsing
1550
1665
  // whole gradient layers.
1551
- const gridHits = scanCssTextForGridBackground(html);
1666
+ const gridHits = scanCssTextForGridBackground(styleText);
1552
1667
  if (gridHits.length > 0) {
1553
- findings.push({ id: 'codex-grid-background', snippet: gridHits[0].snippet });
1668
+ findings.push({ id: 'codex-grid-background', snippet: gridHits[0].snippet, selector: enclosingCssSelector(styleText, gridHits[0].index) || undefined });
1554
1669
  }
1555
1670
 
1556
1671
  // --- Generated-copy tells: "X theater" framing copy ---
@@ -1567,8 +1682,11 @@ function checkHtmlPatterns(html) {
1567
1682
  // hover:rotate / hover:translate utility on an <img>. Each distinct
1568
1683
  // mechanism is its own finding.
1569
1684
  const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i;
1570
- if (imgHoverCss.test(html)) {
1571
- findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' });
1685
+ {
1686
+ const imgHoverMatch = imgHoverCss.exec(styleText);
1687
+ if (imgHoverMatch) {
1688
+ findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule', selector: enclosingCssSelector(styleText, imgHoverMatch.index + imgHoverMatch[0].indexOf('{') + 1) || undefined });
1689
+ }
1572
1690
  }
1573
1691
  const imgTagRe = /<img\b[^>]*\bclass\s*=\s*"([^"]*)"/gi;
1574
1692
  let im;
@@ -1615,7 +1733,46 @@ function readOwnBackgroundColor(el, computedStyle) {
1615
1733
  return bg;
1616
1734
  }
1617
1735
 
1618
- function resolveBackground(el, win, customPropMap) {
1736
+ // One element's background-color as the cascade walk sees it: computed style
1737
+ // first (with the modern-color fallback), then, in static mode only,
1738
+ // custom-prop resolution and the inline-shorthand peek. Shared by
1739
+ // resolveBackgroundInfo and resolveGradientStops so both walks read the same
1740
+ // surfaces.
1741
+ function readCascadeBackgroundColor(current, style, customPropMap) {
1742
+ let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
1743
+ if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) {
1744
+ // The static engine can return literal "var(--X)" / "oklch(...)" strings.
1745
+ // Resolve through customPropMap so Tailwind v4 color tokens become RGB.
1746
+ if (customPropMap) {
1747
+ bg = parseColorResolved(style.backgroundColor, customPropMap);
1748
+ }
1749
+ if (!bg || bg.a < 0.1) {
1750
+ // Inline-style fallback for colors the static cascade did not surface
1751
+ // on backgroundColor.
1752
+ const rawStyle = current.getAttribute?.('style') || '';
1753
+ const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i);
1754
+ const inlineBg = bgMatch ? bgMatch[1].trim() : '';
1755
+ if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) {
1756
+ bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg);
1757
+ }
1758
+ }
1759
+ }
1760
+ return bg;
1761
+ }
1762
+
1763
+ // Walk up for the surface the element's text is painted on.
1764
+ //
1765
+ // Returns { color, unresolved }:
1766
+ // • color set — the effective surface, overlays composited in.
1767
+ // • unresolved: true — a layer on the way up paints a color this parser
1768
+ // cannot read, so the surface is unknown. Callers
1769
+ // must SKIP their contrast checks. Guessing white
1770
+ // here is what flooded dark themes with false
1771
+ // "on #ffffff" findings: one abstention costs a
1772
+ // single finding, one wrong guess costs a hundred.
1773
+ // • both null/false — no solid color, but a gradient or image is in
1774
+ // play; callers fall back to its color stops.
1775
+ function resolveBackgroundInfo(el, win, customPropMap) {
1619
1776
  let current = el;
1620
1777
  // Translucent layers (0.1 < a < 1) found on the way down to an opaque
1621
1778
  // base. A browser composites these over the base; the old behavior
@@ -1643,67 +1800,114 @@ function resolveBackground(el, win, customPropMap) {
1643
1800
  // body backgrounds.
1644
1801
  // Real browsers serialize wide-gamut computed values as oklab()/oklch()
1645
1802
  // (e.g. any color-mix() result), which plain parseRgb misses.
1646
- let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
1647
- if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) {
1648
- // jsdom returns literal "var(--X)" / "oklch(...)" strings. Resolve
1649
- // through customPropMap so Tailwind v4 color tokens become RGB.
1650
- if (customPropMap) {
1651
- bg = parseColorResolved(style.backgroundColor, customPropMap);
1652
- }
1653
- if (!bg || bg.a < 0.1) {
1654
- // Inline-style fallback. jsdom doesn't decompose background
1655
- // shorthand, so colors set via inline style are otherwise invisible.
1656
- const rawStyle = current.getAttribute?.('style') || '';
1657
- const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i);
1658
- const inlineBg = bgMatch ? bgMatch[1].trim() : '';
1659
- if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) {
1660
- bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg);
1661
- }
1662
- }
1803
+ let bg = readCascadeBackgroundColor(current, style, customPropMap);
1804
+
1805
+ // `background-color: currentcolor` paints with the element's own text
1806
+ // color real paint whose value we know. Real browsers resolve the
1807
+ // keyword before getComputedStyle output; jsdom hands it through
1808
+ // verbatim, and without this substitution the layer would read as
1809
+ // unparseable and force a needless abstention.
1810
+ if ((!bg || bg.a < 0.1) && /^currentcolor$/i.test(String(style.backgroundColor || '').trim())) {
1811
+ // The static cascade resolves var() text tokens before checks run, so
1812
+ // style.color is normally already an rgb string here; parseColorResolved
1813
+ // is defense in depth for any future caller that passes a live
1814
+ // customPropMap (it matches the text-color path in checkElementColors
1815
+ // and reduces to parseAnyColor when the map is null or absent).
1816
+ bg = parseRgb(style.color) || parseColorResolved(style.color, customPropMap);
1663
1817
  }
1664
1818
 
1665
1819
  if (bg && bg.a > 0.1) {
1666
- if (bg.a >= 0.99) return flatten(bg);
1820
+ if (bg.a >= 0.99) return { color: flatten(bg), unresolved: false };
1667
1821
  overlays.push(bg);
1822
+ } else if (!bg && !isNoPaintColorValue(style.backgroundColor)) {
1823
+ // This layer names a color we could not parse (a color space we do not
1824
+ // model, an unresolved var(), a syntax newer than the parser). It may
1825
+ // well be opaque, which would make every ancestor below it invisible —
1826
+ // so the surface is unknown and the walk stops here rather than
1827
+ // reporting an ancestor the visitor never sees.
1828
+ return { color: null, unresolved: true };
1668
1829
  }
1669
- // No solid bg-color at this level. If THIS level has a gradient/url
1670
- // with no underlying solid color we can read:
1671
- // on body/html: assume white. Body-level gradients are almost
1672
- // always decorative texture (paper grain, noise) on top of a
1673
- // solid bg-color the page set via `background: var(--paper)`
1674
- // shorthand which jsdom can't decompose into bg-color. The
1675
- // downstream gradient-stops fallback path produces catastrophic
1676
- // false positives in this case (gradient noise stops have
1677
- // accidental browns/blacks that look like card backgrounds).
1678
- // on other elements: bail to null and let the caller fall back
1679
- // to gradient stops (gradient buttons / hero sections are real
1680
- // bgs worth checking against).
1830
+ // No solid bg-color at this level, but this level paints an image. CSS
1831
+ // stacks background-image layers first-on-top, so which layer leads
1832
+ // decides what the visitor sees:
1833
+ // gradient on top the gradient is the surface. Hand the caller a
1834
+ // null color so it falls back to the gradient's own stops (body
1835
+ // grounds, gradient buttons, hero sections).
1836
+ // url() on top the surface is an image whose pixels this engine
1837
+ // cannot read, and it may fully cover every layer and ancestor
1838
+ // beneath it. Same contract as an unparseable color: abstain, so
1839
+ // the gradient-stop fallback never measures a gradient the image
1840
+ // hides (the shipped miss: `url(photo), linear-gradient(...)`
1841
+ // reported low-contrast against the invisible gradient's stops).
1681
1842
  if (hasGradientOrUrl) {
1682
- if (current.tagName === 'BODY' || current.tagName === 'HTML') {
1683
- return flatten({ r: 255, g: 255, b: 255, a: 1 });
1843
+ const layers = splitTopLevelCommas(bgImage);
1844
+ const topPaintLayer = layers.find(
1845
+ (layer) => /gradient\s*\(/i.test(layer) || /url\s*\(/i.test(layer),
1846
+ );
1847
+ const gradientOnTop = !!topPaintLayer
1848
+ && /gradient\s*\(/i.test(topPaintLayer)
1849
+ && !/^\s*url\s*\(/i.test(topPaintLayer);
1850
+ if (!gradientOnTop) return { color: null, unresolved: true };
1851
+ // Gradient on top of a url() layer: the image shows through wherever
1852
+ // the gradient is not fully opaque, so a translucent wash like
1853
+ // `linear-gradient(rgba(0,0,0,.2), rgba(0,0,0,.2)), url(photo)` paints
1854
+ // a blend with pixels this engine cannot read. Only a gradient whose
1855
+ // every readable stop is opaque provably covers the image; otherwise
1856
+ // the surface is unknown — abstain rather than hand callers gradient
1857
+ // stops (or a stop average) the visitor never sees unmixed.
1858
+ const urlBeneath = layers.some(
1859
+ (layer) => layer !== topPaintLayer && /url\s*\(/i.test(layer),
1860
+ );
1861
+ if (urlBeneath) {
1862
+ const topStops = parseGradientColors(topPaintLayer);
1863
+ const provablyOpaque = topStops.length > 0 && topStops.every((s) => (s.a ?? 1) >= 0.99);
1864
+ if (!provablyOpaque) return { color: null, unresolved: true };
1684
1865
  }
1685
- return null;
1866
+ return { color: null, unresolved: false };
1686
1867
  }
1687
1868
  current = current.parentElement;
1688
1869
  }
1689
- return flatten({ r: 255, g: 255, b: 255, a: 1 });
1870
+ // Every layer up to the document root was genuinely see-through, so the
1871
+ // browser paints its default canvas. This is the ONLY case that earns the
1872
+ // white assumption.
1873
+ return { color: flatten({ r: 255, g: 255, b: 255, a: 1 }), unresolved: false };
1874
+ }
1875
+
1876
+ function resolveBackground(el, win, customPropMap) {
1877
+ return resolveBackgroundInfo(el, win, customPropMap).color;
1690
1878
  }
1691
1879
 
1692
1880
  // Walk parents looking for a gradient background and return its color stops.
1693
1881
  // Used as a fallback when resolveBackground() returns null because the
1694
1882
  // effective background is a gradient (no single solid color to compare against).
1883
+ // Translucent solid layers found between the element and the gradient (frosted
1884
+ // panels, glass washes) are composited over every stop, the same way
1885
+ // resolveBackground flattens them over a solid base — raw stops alone would
1886
+ // false-flag dark text on a light frosted wash over a dark gradient, and miss
1887
+ // the inverse.
1695
1888
  function resolveGradientStops(el, win, customPropMap) {
1696
1889
  let current = el;
1890
+ const overlays = [];
1697
1891
  while (current && current.nodeType === 1) {
1698
1892
  const style = DETECTOR_IS_BROWSER ? getComputedStyle(current) : win.getComputedStyle(current);
1699
1893
  const bgImage = style.backgroundImage || '';
1894
+ // A url() layer anywhere in the stack — alone, or alongside a gradient in
1895
+ // the same declaration (a translucent wash over a texture photo) — paints
1896
+ // pixels the analytic walk cannot know. Measuring the gradient stops over
1897
+ // the wrong base flagged dark ink sitting on a bright gold-leaf image at
1898
+ // 2.6:1; skipping beats a wrong ratio, and the screenshot subsystem owns
1899
+ // image-backed text.
1900
+ if (bgImage && bgImage !== 'none' && /url\s*\(/i.test(bgImage)) return null;
1700
1901
  let stops = null;
1701
1902
  if (bgImage && bgImage !== 'none' && /gradient/i.test(bgImage)) {
1903
+ // parseGradientColors (shared) reads modern-space stops too — oklch,
1904
+ // color-mix and friends via balanced-paren token capture — so browser
1905
+ // computed values that keep the authored syntax stay measurable.
1702
1906
  const parsed = parseGradientColors(bgImage);
1703
1907
  if (parsed.length > 0) stops = parsed;
1704
1908
  }
1705
1909
  if (!stops && !DETECTOR_IS_BROWSER) {
1706
- // jsdom doesn't decompose `background:` shorthand — peek at the raw inline style
1910
+ // Static mode: peek at the raw inline style for gradients the cascade did not surface
1707
1911
  const rawStyle = current.getAttribute?.('style') || '';
1708
1912
  const bgMatch = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i);
1709
1913
  if (bgMatch && /gradient/i.test(bgMatch[1])) {
@@ -1711,7 +1915,23 @@ function resolveGradientStops(el, win, customPropMap) {
1711
1915
  if (parsed.length > 0) stops = parsed;
1712
1916
  }
1713
1917
  }
1714
- if (stops) return compositeGradientStops(stops, current, win, customPropMap);
1918
+ if (stops) {
1919
+ const composited = compositeGradientStops(stops, current, win, customPropMap);
1920
+ if (!composited || overlays.length === 0) return composited;
1921
+ return composited.map(stop => {
1922
+ let acc = stop;
1923
+ for (let i = overlays.length - 1; i >= 0; i--) acc = compositeColorOver(overlays[i], acc);
1924
+ return acc;
1925
+ });
1926
+ }
1927
+ const bg = readCascadeBackgroundColor(current, style, customPropMap);
1928
+ if (bg && bg.a > 0.1) {
1929
+ // An opaque surface above the gradient means the gradient never shows
1930
+ // through here; resolveBackground would have returned it, so reaching
1931
+ // this is defensive — bail rather than measure the wrong layer.
1932
+ if (bg.a >= 0.99) return null;
1933
+ overlays.push(bg);
1934
+ }
1715
1935
  current = current.parentElement;
1716
1936
  }
1717
1937
  return null;
@@ -1931,15 +2151,25 @@ function checkElementColorsDOM(el) {
1931
2151
  const rect = el.getBoundingClientRect();
1932
2152
  if (rect.width < 10 || rect.height < 10) return [];
1933
2153
  const style = getComputedStyle(el);
2154
+ // Invisible at rest: hidden scene variants (opacity-0 carousels, swap
2155
+ // decks) are not user-visible, and measuring their inherited colors against
2156
+ // whatever surface happens to sit behind the stack is noise, not audit.
2157
+ if (style.visibility === 'hidden' || effectiveOpacityDOM(el) <= 0.02) return [];
1934
2158
  const directText = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
1935
2159
  const hasDirectText = directText.trim().length > 0;
1936
- let effectiveBg = resolveBackground(el);
2160
+ const bgInfo = resolveBackgroundInfo(el);
2161
+ let effectiveBg = bgInfo.color;
2162
+ // An unreadable surface anywhere up the chain: skip the gradient-stop
2163
+ // fallback too, so nothing downstream measures against a ground we never
2164
+ // resolved.
2165
+ let surfaceUnresolved = bgInfo.unresolved;
1937
2166
  let ownBg = readOwnBackgroundColor(el, style);
1938
2167
  if (!ownBg || (ownBg.a ?? 1) <= 0.5) {
1939
2168
  const pseudoSurface = readPseudoSurfaceDOM(el, rect);
1940
2169
  if (pseudoSurface) {
1941
2170
  ownBg = pseudoSurface;
1942
2171
  effectiveBg = pseudoSurface;
2172
+ surfaceUnresolved = false;
1943
2173
  }
1944
2174
  }
1945
2175
  return checkColors({
@@ -1951,8 +2181,8 @@ function checkElementColorsDOM(el) {
1951
2181
  // an oklch token near its own oklch background).
1952
2182
  textColor: parseRgb(style.color) || parseAnyColor(style.color),
1953
2183
  bgColor: ownBg,
1954
- effectiveBg,
1955
- effectiveBgStops: effectiveBg ? null : resolveGradientStops(el),
2184
+ effectiveBg: surfaceUnresolved ? null : effectiveBg,
2185
+ effectiveBgStops: surfaceUnresolved || effectiveBg ? null : resolveGradientStops(el),
1956
2186
  fontSize: parseFloat(style.fontSize) || 16,
1957
2187
  fontWeight: parseInt(style.fontWeight) || 400,
1958
2188
  hasDirectText,
@@ -2102,283 +2332,6 @@ function resolveVarRefs(raw, customPropMap, depth = 0) {
2102
2332
  });
2103
2333
  }
2104
2334
 
2105
- // OKLCH → sRGB conversion (Björn Ottosson's matrices). L in 0..1 (or %),
2106
- // C in 0..~0.4 typical, H in degrees. Returns clamped {r,g,b,a:1} in 0..255.
2107
- // Needed because jsdom doesn't compute oklch() values — getComputedStyle
2108
- // returns the literal "oklch(...)" string. Without this, the entire
2109
- // Tailwind v4 color palette (which is OKLCH-based) is invisible to the
2110
- // detector's contrast / color checks.
2111
- function oklchToRgb(L, C, H) {
2112
- const hRad = (H * Math.PI) / 180;
2113
- return oklabToRgb(L, C * Math.cos(hRad), C * Math.sin(hRad));
2114
- }
2115
-
2116
- function oklabToRgb(L, a, b) {
2117
- const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
2118
- const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
2119
- const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
2120
- const lc = l_ * l_ * l_, mc = m_ * m_ * m_, sc = s_ * s_ * s_;
2121
- const rLin = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
2122
- const gLin = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
2123
- const bLin = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
2124
- const enc = (x) => {
2125
- const c = Math.max(0, Math.min(1, x));
2126
- return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
2127
- };
2128
- return {
2129
- r: Math.round(enc(rLin) * 255),
2130
- g: Math.round(enc(gLin) * 255),
2131
- b: Math.round(enc(bLin) * 255),
2132
- a: 1,
2133
- };
2134
- }
2135
-
2136
- function hslToRgb(h, s, l) {
2137
- h = ((h % 360) + 360) % 360;
2138
- const c = (1 - Math.abs(2 * l - 1)) * s;
2139
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
2140
- const m0 = l - c / 2;
2141
- const [r, g, b] =
2142
- h < 60 ? [c, x, 0] :
2143
- h < 120 ? [x, c, 0] :
2144
- h < 180 ? [0, c, x] :
2145
- h < 240 ? [0, x, c] :
2146
- h < 300 ? [x, 0, c] : [c, 0, x];
2147
- return {
2148
- r: Math.round((r + m0) * 255),
2149
- g: Math.round((g + m0) * 255),
2150
- b: Math.round((b + m0) * 255),
2151
- a: 1,
2152
- };
2153
- }
2154
-
2155
- function hwbToRgb(h, w, bl) {
2156
- if (w + bl >= 1) {
2157
- const g = Math.round((w / (w + bl)) * 255);
2158
- return { r: g, g, b: g, a: 1 };
2159
- }
2160
- const base = hslToRgb(h, 1, 0.5);
2161
- const mix = (c) => Math.round(((c / 255) * (1 - w - bl) + w) * 255);
2162
- return { r: mix(base.r), g: mix(base.g), b: mix(base.b), a: 1 };
2163
- }
2164
-
2165
- // Common CSS named colors — the handful that actually show up in generated
2166
- // UIs, not the full 148-name spec list. Includes the achromatic names so a
2167
- // named gray parses (and correctly reads as no-chroma) instead of being
2168
- // treated as an unknown color.
2169
- const CSS_NAMED_COLORS = {
2170
- black: { r: 0, g: 0, b: 0 },
2171
- white: { r: 255, g: 255, b: 255 },
2172
- gray: { r: 128, g: 128, b: 128 },
2173
- grey: { r: 128, g: 128, b: 128 },
2174
- silver: { r: 192, g: 192, b: 192 },
2175
- dimgray: { r: 105, g: 105, b: 105 },
2176
- darkgray: { r: 169, g: 169, b: 169 },
2177
- lightgray: { r: 211, g: 211, b: 211 },
2178
- gainsboro: { r: 220, g: 220, b: 220 },
2179
- whitesmoke: { r: 245, g: 245, b: 245 },
2180
- red: { r: 255, g: 0, b: 0 },
2181
- crimson: { r: 220, g: 20, b: 60 },
2182
- tomato: { r: 255, g: 99, b: 71 },
2183
- coral: { r: 255, g: 127, b: 80 },
2184
- salmon: { r: 250, g: 128, b: 114 },
2185
- orange: { r: 255, g: 165, b: 0 },
2186
- gold: { r: 255, g: 215, b: 0 },
2187
- yellow: { r: 255, g: 255, b: 0 },
2188
- olive: { r: 128, g: 128, b: 0 },
2189
- lime: { r: 0, g: 255, b: 0 },
2190
- green: { r: 0, g: 128, b: 0 },
2191
- teal: { r: 0, g: 128, b: 128 },
2192
- turquoise: { r: 64, g: 224, b: 208 },
2193
- cyan: { r: 0, g: 255, b: 255 },
2194
- aqua: { r: 0, g: 255, b: 255 },
2195
- skyblue: { r: 135, g: 206, b: 235 },
2196
- dodgerblue: { r: 30, g: 144, b: 255 },
2197
- blue: { r: 0, g: 0, b: 255 },
2198
- navy: { r: 0, g: 0, b: 128 },
2199
- indigo: { r: 75, g: 0, b: 130 },
2200
- rebeccapurple: { r: 102, g: 51, b: 153 },
2201
- purple: { r: 128, g: 0, b: 128 },
2202
- violet: { r: 238, g: 130, b: 238 },
2203
- orchid: { r: 218, g: 112, b: 214 },
2204
- magenta: { r: 255, g: 0, b: 255 },
2205
- fuchsia: { r: 255, g: 0, b: 255 },
2206
- hotpink: { r: 255, g: 105, b: 180 },
2207
- pink: { r: 255, g: 192, b: 203 },
2208
- maroon: { r: 128, g: 0, b: 0 },
2209
- };
2210
-
2211
- // Split a string on top-level commas (ignoring commas nested in parens).
2212
- function splitTopLevelCommas(str) {
2213
- const parts = [];
2214
- let depth = 0, start = 0;
2215
- for (let i = 0; i < str.length; i++) {
2216
- const ch = str[i];
2217
- if (ch === '(') depth++;
2218
- else if (ch === ')') depth = Math.max(0, depth - 1);
2219
- else if (ch === ',' && depth === 0) {
2220
- parts.push(str.slice(start, i).trim());
2221
- start = i + 1;
2222
- }
2223
- }
2224
- const tail = str.slice(start).trim();
2225
- if (tail) parts.push(tail);
2226
- return parts;
2227
- }
2228
-
2229
- // Evaluate a CSS color-mix() expression to {r,g,b,a}. Returns null when
2230
- // the expression can't be resolved (unresolved var(), unknown colors).
2231
- //
2232
- // Mixing is done with premultiplied alpha in sRGB regardless of the
2233
- // declared interpolation space. That is exact for the dominant generated-UI
2234
- // pattern — `color-mix(in oklab, <color> N%, transparent)` — where the
2235
- // result is simply <color> at alpha N% in ANY rectangular space, and a
2236
- // close-enough approximation for opaque-opaque mixes (the detector only
2237
- // consumes these values for contrast/chroma thresholds, not for display).
2238
- function parseColorMix(str) {
2239
- const m = String(str).trim().match(/^color-mix\(/i);
2240
- if (!m) return null;
2241
- // Balanced-paren capture of the arguments.
2242
- let depth = 0, end = -1;
2243
- const open = str.indexOf('(');
2244
- for (let i = open; i < str.length; i++) {
2245
- if (str[i] === '(') depth++;
2246
- else if (str[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
2247
- }
2248
- if (end < 0) return null;
2249
- const args = splitTopLevelCommas(str.slice(open + 1, end));
2250
- if (args.length !== 3 || !/^in\s/i.test(args[0])) return null;
2251
-
2252
- const parseComponent = (component) => {
2253
- // Percentage may lead or trail the color per spec.
2254
- let pct = null;
2255
- let colorStr = component;
2256
- const trail = component.match(/\s+([\d.]+)%$/);
2257
- const lead = component.match(/^([\d.]+)%\s+/);
2258
- if (trail) { pct = parseFloat(trail[1]); colorStr = component.slice(0, trail.index).trim(); }
2259
- else if (lead) { pct = parseFloat(lead[1]); colorStr = component.slice(lead[0].length).trim(); }
2260
- let color;
2261
- if (/^transparent$/i.test(colorStr)) color = { r: 0, g: 0, b: 0, a: 0 };
2262
- else color = parseAnyColor(colorStr);
2263
- if (!color) return null;
2264
- return { color, pct };
2265
- };
2266
-
2267
- const c1 = parseComponent(args[1]);
2268
- const c2 = parseComponent(args[2]);
2269
- if (!c1 || !c2) return null;
2270
- let p1 = c1.pct, p2 = c2.pct;
2271
- if (p1 == null && p2 == null) { p1 = 50; p2 = 50; }
2272
- else if (p1 == null) p1 = 100 - p2;
2273
- else if (p2 == null) p2 = 100 - p1;
2274
- const sum = p1 + p2;
2275
- if (sum <= 0) return null;
2276
- // Per spec: weights normalize to sum; when sum < 100 the result alpha is
2277
- // additionally scaled by sum/100.
2278
- const w1 = p1 / sum, w2 = p2 / sum;
2279
- const alphaScale = sum < 100 ? sum / 100 : 1;
2280
- const a1 = c1.color.a ?? 1, a2 = c2.color.a ?? 1;
2281
- const a = (a1 * w1 + a2 * w2) * alphaScale;
2282
- if (a <= 0) return { r: 0, g: 0, b: 0, a: 0 };
2283
- const mix = (ch) => Math.round((c1.color[ch] * a1 * w1 + c2.color[ch] * a2 * w2) / (a1 * w1 + a2 * w2));
2284
- return { r: mix('r'), g: mix('g'), b: mix('b'), a: Math.min(1, a) };
2285
- }
2286
-
2287
- // Composite a translucent color over an opaque(ish) base (simple
2288
- // source-over in sRGB). Returns an opaque {r,g,b,a:1}.
2289
- function compositeColorOver(top, base) {
2290
- const a = top.a ?? 1;
2291
- return {
2292
- r: Math.round(top.r * a + base.r * (1 - a)),
2293
- g: Math.round(top.g * a + base.g * (1 - a)),
2294
- b: Math.round(top.b * a + base.b * (1 - a)),
2295
- a: 1,
2296
- };
2297
- }
2298
-
2299
- // Extended color parser: rgb/rgba/hex/oklch/oklab/hsl/hwb/color-mix/common
2300
- // named colors. Returns null on no match. Use this when the input might be
2301
- // any CSS color form; use plain parseRgb when you only expect computed rgb()
2302
- // values from real browsers.
2303
- function parseAnyColor(s) {
2304
- if (!s || typeof s !== 'string') return null;
2305
- const str = s.trim();
2306
- if (str === 'transparent' || str === 'currentcolor' || str === 'inherit') return null;
2307
- if (/^color-mix\(/i.test(str)) return parseColorMix(str);
2308
- let m;
2309
- m = str.match(/rgba?\(\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)(?:\s*[,/]\s*([\d.]+))?\s*\)/);
2310
- if (m) return { r: Math.round(+m[1]), g: Math.round(+m[2]), b: Math.round(+m[3]), a: m[4] !== undefined ? +m[4] : 1 };
2311
- m = str.match(/^#([0-9a-f]{3,8})$/i);
2312
- if (m) {
2313
- const h = m[1];
2314
- if (h.length === 3 || h.length === 4) {
2315
- return {
2316
- r: parseInt(h[0] + h[0], 16),
2317
- g: parseInt(h[1] + h[1], 16),
2318
- b: parseInt(h[2] + h[2], 16),
2319
- a: h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1,
2320
- };
2321
- }
2322
- if (h.length === 6 || h.length === 8) {
2323
- return {
2324
- r: parseInt(h.slice(0, 2), 16),
2325
- g: parseInt(h.slice(2, 4), 16),
2326
- b: parseInt(h.slice(4, 6), 16),
2327
- a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
2328
- };
2329
- }
2330
- }
2331
- // OKLCH parser. Tailwind v4's CSS minifier squishes the space after
2332
- // `%` ("21.5%.02 50"), so the separator between L and C may be absent.
2333
- // Match L (with optional %), then C and H separated permissively.
2334
- m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
2335
- if (m) {
2336
- const Lnum = parseFloat(m[1]);
2337
- const L = m[2] === '%' ? Lnum / 100 : Lnum;
2338
- const rgb = oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
2339
- if (m[5] !== undefined) {
2340
- const alpha = parseFloat(m[5]);
2341
- rgb.a = m[6] === '%' ? alpha / 100 : alpha;
2342
- }
2343
- return rgb;
2344
- }
2345
- // OKLAB — a/b are signed axes; percentages map 100% → 0.4.
2346
- m = str.match(/oklab\(\s*([\d.]+)(%?)\s+(-?[\d.]+)(%?)\s+(-?[\d.]+)(%?)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
2347
- if (m) {
2348
- const L = m[2] === '%' ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
2349
- const a = m[4] === '%' ? parseFloat(m[3]) * 0.004 : parseFloat(m[3]);
2350
- const b = m[6] === '%' ? parseFloat(m[5]) * 0.004 : parseFloat(m[5]);
2351
- const rgb = oklabToRgb(L, a, b);
2352
- if (m[7] !== undefined) {
2353
- const alpha = parseFloat(m[7]);
2354
- rgb.a = m[8] === '%' ? alpha / 100 : alpha;
2355
- }
2356
- return rgb;
2357
- }
2358
- // HSL/HSLA — comma or space syntax, optional deg on hue.
2359
- m = str.match(/hsla?\(\s*(-?[\d.]+)(?:deg)?\s*[,\s]\s*([\d.]+)%\s*[,\s]\s*([\d.]+)%(?:\s*[,/]\s*([\d.]+)(%)?)?\s*\)/i);
2360
- if (m) {
2361
- const rgb = hslToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
2362
- if (m[4] !== undefined) {
2363
- const alpha = parseFloat(m[4]);
2364
- rgb.a = m[5] === '%' ? alpha / 100 : alpha;
2365
- }
2366
- return rgb;
2367
- }
2368
- // HWB — hue whiteness% blackness%.
2369
- m = str.match(/hwb\(\s*(-?[\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
2370
- if (m) {
2371
- const rgb = hwbToRgb(parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100);
2372
- if (m[4] !== undefined) {
2373
- const alpha = parseFloat(m[4]);
2374
- rgb.a = m[5] === '%' ? alpha / 100 : alpha;
2375
- }
2376
- return rgb;
2377
- }
2378
- const named = CSS_NAMED_COLORS[str.toLowerCase()];
2379
- if (named) return { ...named, a: 1 };
2380
- return null;
2381
- }
2382
2335
 
2383
2336
  // Resolve var() refs in a color string (via customPropMap), then parse.
2384
2337
  // Returns null on any failure. Used in jsdom-mode paths where
@@ -2468,7 +2421,11 @@ function isKickerCandidate(opts) {
2468
2421
  || isSmallCaps;
2469
2422
  if (!isUppercased) return false;
2470
2423
  if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
2471
- const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
2424
+ // Proportional only, no absolute floor: the wild's most common recipe is
2425
+ // 0.08em at a sub-13px size, which computes to under 1px and sailed past
2426
+ // the old Math.max(1, ...) floor (observed live: a page whose kickers were
2427
+ // literally class="kicker" produced zero findings).
2428
+ const minTrackedSpacing = kickerFontSize * 0.06;
2472
2429
  if (!(kickerLetterSpacing >= minTrackedSpacing)) return false;
2473
2430
  return true;
2474
2431
  }
@@ -2737,9 +2694,20 @@ function checkElementGlowDOM(el) {
2737
2694
  if (!boxShadow && !textShadow) return [];
2738
2695
  // Use parent's background — glow radiates outward, so the surrounding context matters
2739
2696
  // If resolveBackground returns null (gradient), try to infer from the gradient colors
2740
- let parentBg = el.parentElement ? resolveBackground(el.parentElement) : resolveBackground(el);
2741
- if (!parentBg) {
2742
- // Gradient background sample its colors to determine if it's dark
2697
+ const parentBgInfo = resolveBackgroundInfo(el.parentElement || el);
2698
+ // Unknown surface (an unreadable layer on the way up): skip only the
2699
+ // gradient hunt below, which would walk PAST that layer and score the
2700
+ // glow against a background the visitor never sees. checkGlow still runs
2701
+ // with a null surface: the zero-offset chromatic halo tell holds on ANY
2702
+ // background, and the static loop already passes the unresolved walk's
2703
+ // null color straight through (detect-html.mjs uses resolveBackground).
2704
+ let parentBg = parentBgInfo.color;
2705
+ if (!parentBg && !parentBgInfo.unresolved) {
2706
+ // Gradient background — sample its colors to determine if it's dark.
2707
+ // Modern-syntax parsing matters here: body-level gradients now reach this
2708
+ // fallback in browser mode, and their stops usually serialize as oklch —
2709
+ // which the shared parseGradientColors reads via its color-function
2710
+ // token capture.
2743
2711
  let cur = el.parentElement;
2744
2712
  while (cur && cur.nodeType === 1) {
2745
2713
  const bgImage = getComputedStyle(cur).backgroundImage || '';
@@ -2787,10 +2755,13 @@ function checkElementAIPaletteDOM(el) {
2787
2755
  const hue = getHue(textColor);
2788
2756
  const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
2789
2757
  if (isAIPalette) {
2790
- const parentBg = el.parentElement ? resolveBackground(el.parentElement) : null;
2791
- // Also check gradient parents
2792
- let effectiveBg = parentBg;
2793
- if (!effectiveBg) {
2758
+ const parentBgInfo = el.parentElement
2759
+ ? resolveBackgroundInfo(el.parentElement)
2760
+ : { color: null, unresolved: false };
2761
+ // Unknown surface: leave effectiveBg null (no finding) rather than
2762
+ // hunting gradient ancestors past a layer we could not read.
2763
+ let effectiveBg = parentBgInfo.color;
2764
+ if (!effectiveBg && !parentBgInfo.unresolved) {
2794
2765
  let cur = el.parentElement;
2795
2766
  while (cur && cur.nodeType === 1) {
2796
2767
  const gi = getComputedStyle(cur).backgroundImage || '';
@@ -3585,10 +3556,19 @@ function checkElementBorders(tag, style, overrides, resolvedRadius, el = null) {
3585
3556
  }
3586
3557
 
3587
3558
  function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInheritRule) {
3559
+ // Invisible at rest, static twin of the browser walk's skip: opacity does
3560
+ // not inherit, so walk ancestors multiplying declared opacity down.
3561
+ if (style.visibility === 'hidden') return [];
3562
+ let effOpacity = 1;
3563
+ for (let cur = el; cur && cur.nodeType === 1 && effOpacity > 0.02; cur = cur.parentElement) {
3564
+ effOpacity *= parseFloat(window.getComputedStyle(cur).opacity || '1');
3565
+ }
3566
+ if (effOpacity <= 0.02) return [];
3588
3567
  const directText = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
3589
3568
  const hasDirectText = directText.trim().length > 0;
3590
3569
 
3591
- const effectiveBg = resolveBackground(el, window, customPropMap);
3570
+ const bgInfo = resolveBackgroundInfo(el, window, customPropMap);
3571
+ const effectiveBg = bgInfo.color;
3592
3572
  // jsdom returns literal "var(--X)" / "oklch(...)" for color, so plain
3593
3573
  // parseRgb misses Tailwind-tokenized text colors. Resolve through the
3594
3574
  // customPropMap first; fall back to parseRgb for vanilla rgb() pages.
@@ -3634,11 +3614,13 @@ function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInhe
3634
3614
  // element itself has no usable own background, that pseudo is the real
3635
3615
  // surface for contrast purposes.
3636
3616
  let finalEffectiveBg = effectiveBg;
3617
+ let surfaceUnresolved = bgInfo.unresolved;
3637
3618
  if ((!ownBg || (ownBg.a ?? 1) <= 0.5) && typeof window.getPseudoSurface === 'function') {
3638
3619
  const pseudoSurface = window.getPseudoSurface(el);
3639
3620
  if (pseudoSurface) {
3640
3621
  ownBg = pseudoSurface;
3641
3622
  finalEffectiveBg = pseudoSurface;
3623
+ surfaceUnresolved = false;
3642
3624
  }
3643
3625
  }
3644
3626
 
@@ -3646,8 +3628,9 @@ function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInhe
3646
3628
  tag,
3647
3629
  textColor,
3648
3630
  bgColor: ownBg,
3649
- effectiveBg: finalEffectiveBg,
3650
- effectiveBgStops: finalEffectiveBg ? null : resolveGradientStops(el, window, customPropMap),
3631
+ // Unknown surface: hand the checks nothing rather than a guess.
3632
+ effectiveBg: surfaceUnresolved ? null : finalEffectiveBg,
3633
+ effectiveBgStops: surfaceUnresolved || finalEffectiveBg ? null : resolveGradientStops(el, window, customPropMap),
3651
3634
  fontSize: parseFloat(style.fontSize) || 16,
3652
3635
  fontWeight: parseInt(style.fontWeight) || 400,
3653
3636
  hasDirectText,
@@ -3852,12 +3835,6 @@ function checkTypography() {
3852
3835
  if (isBrandFontOnOwnDomain(font)) continue;
3853
3836
  findings.push({ type: 'overused-font', detail: `Primary font: ${font} (${Math.round(share * 100)}% of text)` });
3854
3837
  }
3855
-
3856
- // Single-font check: only one distinct primary font across all text
3857
- if (fontUsage.size === 1) {
3858
- const only = [...fontUsage.keys()][0];
3859
- findings.push({ type: 'single-font', detail: `only font used is ${only}` });
3860
- }
3861
3838
  }
3862
3839
 
3863
3840
  const sizes = new Set();
@@ -4119,14 +4096,6 @@ function checkPageTypography(doc, win) {
4119
4096
  findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` });
4120
4097
  }
4121
4098
 
4122
- // Single font
4123
- if (fonts.size === 1) {
4124
- const els = doc.querySelectorAll('*');
4125
- if (els.length >= 20) {
4126
- findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` });
4127
- }
4128
- }
4129
-
4130
4099
  // Flat type hierarchy
4131
4100
  const sizes = new Set();
4132
4101
  const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div');
@@ -4757,6 +4726,11 @@ function isRenderedForBrowserRule(el) {
4757
4726
  function checkElementTextOverflowDOM(el) {
4758
4727
  const tag = el.tagName.toLowerCase();
4759
4728
  if (TEXT_OVERFLOW_SKIP_TAGS.has(tag)) return [];
4729
+ // scrollWidth/clientWidth are CSS box-model metrics; on SVG content Chrome
4730
+ // returns arbitrary non-zero values for both (a <text> reported 78/48 while
4731
+ // its rendered length sat comfortably inside its box), so the delta is
4732
+ // noise, not overflow. SVG clips to its own viewport anyway.
4733
+ if (el.namespaceURI === 'http://www.w3.org/2000/svg') return [];
4760
4734
  if (!isRenderedForBrowserRule(el)) return [];
4761
4735
  // Only the element that actually owns overflowing text — not its ancestors,
4762
4736
  // which inherit a wider scrollWidth from the spilling descendant.
@@ -5141,6 +5115,22 @@ function isPaintedForOcclusion(el) {
5141
5115
  // path is pure geometry and runs anywhere on the page.
5142
5116
  const OCCLUSION_TEXT_SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'title']);
5143
5117
 
5118
+ // An element whose effective opacity multiplies out to ~0 paints nothing at
5119
+ // rest: it is not user-visible, so visual findings on it (contrast, occlusion)
5120
+ // measure a state nobody sees. Browser-only — the walk needs live computed
5121
+ // styles. Cycling scenes that fade such elements in later are the screenshot
5122
+ // subsystem's territory, not the analytic walk's.
5123
+ function effectiveOpacityDOM(el) {
5124
+ let o = 1;
5125
+ // Walk all the way through body and html: `body { opacity: 0 }` page-fade
5126
+ // wrappers hide every descendant just as thoroughly as a local wrapper.
5127
+ for (let cur = el; cur && cur.nodeType === 1; cur = cur.parentElement) {
5128
+ o *= parseFloat(getComputedStyle(cur).opacity || '1');
5129
+ if (o <= 0.02) return 0;
5130
+ }
5131
+ return o;
5132
+ }
5133
+
5144
5134
  function checkTextOcclusionDOM() {
5145
5135
  const findings = [];
5146
5136
  const seenVictims = new Set();
@@ -5168,6 +5158,11 @@ function checkTextOcclusionDOM() {
5168
5158
  }
5169
5159
  return false;
5170
5160
  };
5161
+ // The classic occluder shape this rules out is an opacity-0 interaction
5162
+ // layer — a range scrubber stretched over a before/after comparison — which
5163
+ // elementFromPoint still returns and whose UA background-color otherwise
5164
+ // reads as an opaque box.
5165
+ const effectiveOpacity = effectiveOpacityDOM;
5171
5166
 
5172
5167
  // Collect renderable text owners in / near the first viewport for the
5173
5168
  // elementFromPoint probe. SVG <text> counts too.
@@ -5180,6 +5175,7 @@ function checkTextOcclusionDOM() {
5180
5175
  const text = inSvg ? (el.textContent || '').trim() : elementDirectText(el);
5181
5176
  if (text.length < 2) continue;
5182
5177
  if (!isPaintedForOcclusion(el)) continue;
5178
+ if (effectiveOpacity(el) <= 0.02) continue;
5183
5179
  let rect; try { rect = el.getBoundingClientRect(); } catch { continue; }
5184
5180
  if (rect.width < 6 || rect.height < 6) continue;
5185
5181
  // Viewport-bound probe: keep text whose box overlaps the live viewport.
@@ -5213,6 +5209,7 @@ function checkTextOcclusionDOM() {
5213
5209
  if (top === el || el.contains(top) || top.contains(el)) continue;
5214
5210
  const topCs = getComputedStyle(top);
5215
5211
  if (isFloated(topCs) || isMarqueeish(top, topCs) || isPinnedOverlay(top)) continue;
5212
+ if (effectiveOpacity(top) <= 0.02) continue;
5216
5213
  const topTag = top.tagName.toLowerCase();
5217
5214
  // Text sitting under a raw image/video is contrast territory (deduped
5218
5215
  // against the pixel low-contrast rule); leave those alone here.
@@ -5423,6 +5420,7 @@ export {
5423
5420
  CSS_NAMED_COLORS,
5424
5421
  checkBorders,
5425
5422
  isEmojiOnlyText,
5423
+ scopedIgnoreActive,
5426
5424
  checkColors,
5427
5425
  checkHoverContrast,
5428
5426
  checkElementHoverContrast,
@@ -5448,9 +5446,11 @@ export {
5448
5446
  cssLengthToPx,
5449
5447
  scanCssTextForPulsingDot,
5450
5448
  scanHtmlForShapeAssembledIllustration,
5449
+ buildHtmlPatternCorpora,
5451
5450
  checkHtmlPatterns,
5452
5451
  readOwnBackgroundColor,
5453
5452
  resolveBackground,
5453
+ resolveBackgroundInfo,
5454
5454
  resolveGradientStops,
5455
5455
  parseRadiusToPx,
5456
5456
  resolveBorderRadiusPx,