sygnal 5.4.0 → 6.1.0

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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -34,9 +34,8 @@ export interface DiagnosticCheck {
34
34
  * Strict mode (canonical forms, SYG5xx): turn the runtime strict checks on
35
35
  * (true), off (false), or back to the default (undefined → on only when
36
36
  * globalThis.__SYGNAL_STRICT__ === true). Off by default. Diagnostics must
37
- * also be on. Runtime rules: SYG501 (positional view args), SYG502 (reducer
38
- * returned the unchanged state instead of ABORT), SYG504 ('ACTION | SINK'
39
- * keys); `sygnal-check --strict` checks all of SYG501-507 statically.
37
+ * also be on. Runtime rules: SYG501 (positional view args), SYG504 ('ACTION | SINK'
38
+ * keys); `sygnal-check --strict` checks SYG501-508 statically (SYG502 is retired in 6.0).
40
39
  * In tests: renderComponent(C, { strict: true }).
41
40
  */
42
41
  export function configureStrict(on?: boolean): void
@@ -96,15 +95,19 @@ export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined
96
95
  // ---------------------------------------------------------------------------
97
96
 
98
97
  /** How an action is dispatched. */
99
- export type InspectActionTrigger = 'intent' | 'next' | 'builtin' | 'unknown'
98
+ /** 'agent' (static, PLAN-6 K-1): only the component's `agent` declaration dispatches it */
99
+ export type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'agent' | 'unknown'
100
100
 
101
101
  export interface InspectAction {
102
102
  name: string
103
103
  /**
104
104
  * 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
105
- * INITIALIZE, HYDRATE, DISPOSE or READY. 'next': dispatched with next()
106
- * (statically: a next('NAME') literal; at runtime: a model-only action whose
107
- * STATE reducer was seen running). 'unknown': none of these is known.
105
+ * INITIALIZE, DISPOSE or READY. 'reply': named as a reply action by a request
106
+ * (`ok: 'NAME'` / `error: 'NAME'`) or a `connections` entry (statically: a
107
+ * string literal; at runtime: a request the instance was seen sending).
108
+ * 'next': dispatched with next() (statically: a next('NAME') literal; at
109
+ * runtime: a model-only action whose STATE reducer was seen running).
110
+ * 'unknown': none of these is known.
108
111
  */
109
112
  trigger: InspectActionTrigger
110
113
  /** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
@@ -134,6 +137,20 @@ export interface InspectSelector {
134
137
  matched: boolean | null
135
138
  /** the child component whose (isolated) elements it matches instead, if any */
136
139
  isolationHit: string | null
140
+ /** PLAN-4 CT-1: the control this selector is (its key), when it is `[data-control="<Key>"]` */
141
+ control?: string
142
+ }
143
+
144
+ /** PLAN-4 CT-1: a control (controls()) a component renders */
145
+ export interface InspectControl {
146
+ /** the control's key (rendered as data-control="<name>") */
147
+ name: string
148
+ /** the intrinsic tag of a tag spec; null for a spec object */
149
+ element: string | null
150
+ /** a spec object's kind (e.g. 'widget') */
151
+ kind?: string
152
+ /** whether the component's intent listens to it; null when unknown */
153
+ listened: boolean | null
137
154
  }
138
155
 
139
156
  export interface InspectDiagnostic {
@@ -178,7 +195,76 @@ export interface InspectComponent {
178
195
  eventsSelected: string[]
179
196
  children: InspectChild[]
180
197
  selectors: InspectSelector[]
198
+ /** PLAN-4 CT-1: the controls it renders (runtime: seen in its renders); omitted when none */
199
+ controls?: InspectControl[]
200
+ /** static only, PLAN-4 GS-2 (G-224): the element commands (ELEMENT sink) its model sends; omitted when none */
201
+ commands?: InspectCommand[]
202
+ /** static only, PLAN-4 GS-7 (G-224): the literal timer specs of its `timers` static; omitted when none */
203
+ timers?: InspectTimer[]
204
+ /** static only, PLAN-6 K-1: the component's `agent` declaration (sygnal/ai); omitted when none */
205
+ agent?: InspectAgent
181
206
  diagnostics: InspectDiagnostic[]
207
+ /** runtime, PLAN-3 5-3: the instance's `resources` and their state */
208
+ resources?: InspectResource[]
209
+ }
210
+
211
+ /** PLAN-4 GS-2: an element command a model entry sends (static inspect, sygnal-check --graph) */
212
+ export interface InspectCommand {
213
+ /** the model entry that sends it */
214
+ action: string
215
+ /** the command's method (its first key) */
216
+ method: string
217
+ /** the control's key or the selector; null when not static */
218
+ target: string | null
219
+ /** the control it targets (its key) */
220
+ control?: string
221
+ /** intent actions listening on the target for a native event the command causes (close, toggle...) */
222
+ triggers?: string[]
223
+ }
224
+
225
+ /** PLAN-6 K-1: a component's `agent` declaration (static inspect, sygnal-check --graph) */
226
+ export interface InspectAgent {
227
+ /** the literal agent.name; null when absent or not a literal */
228
+ name: string | null
229
+ /** the tool-name prefix (agent.name, else the component name, snake_case); null when not static */
230
+ tools: string | null
231
+ description: string | null
232
+ /** the declaration has a read projection */
233
+ read: boolean
234
+ actions: Array<{ name: string; consequential: boolean; input: boolean }>
235
+ /** spreads: what is listed may be incomplete */
236
+ partial?: true
237
+ }
238
+
239
+ /** PLAN-4 GS-7: a literal timer spec of a `timers` static (static inspect, sygnal-check --graph) */
240
+ export interface InspectTimer {
241
+ name: string
242
+ every?: number | null
243
+ after?: number | null
244
+ /** the action a frame timer dispatches */
245
+ frame?: string | null
246
+ action?: string | null
247
+ background?: true
248
+ }
249
+
250
+ /** PLAN-3 5-3: a resource of a component instance (runtime inspect()) */
251
+ export interface InspectResource {
252
+ name: string
253
+ status: 'idle' | 'loading' | 'success' | 'error'
254
+ refreshing: boolean
255
+ /** `data` is set (a success, or kept through a refetch) */
256
+ hasData: boolean
257
+ /** the error message while 'error' */
258
+ error?: string
259
+ }
260
+
261
+ /** PLAN-3 5-3: a makeFetchDriver cache entry (runtime inspect(), t.cache) */
262
+ export interface InspectCacheEntry {
263
+ key: string
264
+ age?: number
265
+ stale: boolean
266
+ subscribers: number
267
+ data: any
182
268
  }
183
269
 
184
270
  export interface InspectGraph {
@@ -190,6 +276,26 @@ export interface InspectGraph {
190
276
  events: Record<string, { emitters: string[]; selectors: string[] }>
191
277
  /** diagnostics not tied to a listed component */
192
278
  diagnostics: InspectDiagnostic[]
279
+ /** runtime, PLAN-3 5-3: makeFetchDriver cache entries, by sink name (empty when no cache) */
280
+ cache?: Record<string, InspectCacheEntry[]>
281
+ /** runtime, PLAN-4 2-C (GS-10): with `inspect({ actions })`, the most recent actions, oldest first */
282
+ recentActions?: InspectRecentAction[]
283
+ }
284
+
285
+ /** PLAN-4 2-C (GS-10): one recent action (inspect({ actions })) */
286
+ export interface InspectRecentAction {
287
+ type: string
288
+ /** the action's data, when it is JSON-safe and small (a DOM event is left out) */
289
+ data?: any
290
+ /** the component's name */
291
+ component: string
292
+ /** the instance's id (InspectComponent.id) */
293
+ instance: string
294
+ /** the sinks that produced a value for it (not ABORT) */
295
+ sinks: string[]
296
+ cause: 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior' | 'agent'
297
+ /** ms since the dev entry started recording (or the last resetChecks()) */
298
+ at: number
193
299
  }
194
300
 
195
301
  export interface InspectOptions {
@@ -199,6 +305,8 @@ export interface InspectOptions {
199
305
  selectors?: Record<string, InspectSelector[]>
200
306
  /** diagnostics to attach (default: the devtools' collected diagnostics, when available) */
201
307
  diagnostics?: Array<{ code: string; severity: DiagnosticSeverity; component?: string; message: string; [key: string]: any }>
308
+ /** PLAN-4 2-C: add `recentActions`: true for every kept one (the last 200), a number for the last n */
309
+ actions?: boolean | number
202
310
  }
203
311
 
204
312
  /**
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Reply actions (PLAN-3 §1.1, workstream 1-D).
3
+ *
4
+ * SYG112 — a request names a reply action the sending component has no
5
+ * model entry for (error: the reply is dropped). Dev-entry code
6
+ * (DEV_CODE_SEVERITY).
7
+ * SYG508 — strict: the component reads the replies to its own request with
8
+ * X.select('cat') / X.errors('cat') where reply actions would do.
9
+ *
10
+ * Mechanism:
11
+ * onSink (checks/next.ts: checkRequest) every value an instance sends to a
12
+ * reply-capable sink (`__sygnalReplies === true`: makeFetchDriver,
13
+ * driverFromAsync, the socket driver) is checked as it is sent.
14
+ * The names it sees feed inspect()'s 'reply' trigger.
15
+ * sources (strict only) wraps those sources for the intent, recording the
16
+ * select()/errors() categories the instance reads.
17
+ * A request's `category` is compared for SYG508 (driverFromAsync's custom
18
+ * `selector` property is not known at run time; sygnal-check covers it).
19
+ * Reported once per component name and action / category.
20
+ *
21
+ * `{ connections }` values (the `connections` static, which the core sends to
22
+ * the same sink, or one a model entry sends) are checked the same way:
23
+ * each connection's message / open / close / error and its SSE `events` names.
24
+ */
25
+ import type {DiagnosticCheck} from '../index'
26
+ import {STRICT_CODE_SEVERITY} from '../codes'
27
+ import {devReport, reportSafely, once, nameOf, suggest, replySeen} from './shared'
28
+ import {isStrictEnabled} from './strict'
29
+
30
+ const REPLY_KEYS = ['ok', 'error']
31
+ // PLAN-3 5-4b: on the router's sink, `{ route }` (the route static) and `{ block }` name reply actions
32
+ const ROUTER_KEYS = ['route', 'block']
33
+ const CONNECTION_KEYS = ['message', 'open', 'close', 'error']
34
+
35
+ /** [action, key, connection name?] for every reply action name a sink value names */
36
+ function replyNames(req: any, router?: boolean): Array<[string, string, string?]> {
37
+ const out: Array<[string, string, string?]> = []
38
+ for (const key of router ? REPLY_KEYS.concat(ROUTER_KEYS) : REPLY_KEYS) if (typeof req[key] === 'string') out.push([req[key], key])
39
+ const conns = req.connections
40
+ if (conns && typeof conns === 'object') {
41
+ for (const name of Object.keys(conns)) {
42
+ const spec = conns[name]
43
+ if (!spec || typeof spec !== 'object') continue
44
+ for (const key of CONNECTION_KEYS) if (typeof spec[key] === 'string') out.push([spec[key], key, name])
45
+ const events = spec.events
46
+ if (events && typeof events === 'object') {
47
+ for (const ev of Object.keys(events)) if (typeof events[ev] === 'string') out.push([events[ev], `events.${ev}`, name])
48
+ }
49
+ }
50
+ }
51
+ return out
52
+ }
53
+
54
+ /** instance → 'SINK\0category' → the select/errors call that read it */
55
+ const selected = new WeakMap<object, Map<string, string>>()
56
+
57
+
58
+ function readingSource(component: any, sink: string, source: any) {
59
+ const wrapped = Object.create(source)
60
+ for (const method of ['select', 'errors']) {
61
+ if (typeof source[method] !== 'function') continue
62
+ wrapped[method] = (category?: any, ...rest: any[]) => {
63
+ if (typeof category === 'string') {
64
+ let reads = selected.get(component)
65
+ if (!reads) selected.set(component, reads = new Map())
66
+ reads.set(`${sink}\0${category}`, `${sink}.${method}('${category}')`)
67
+ }
68
+ return source[method](category, ...rest)
69
+ }
70
+ }
71
+ return wrapped
72
+ }
73
+
74
+ export function checkRequest(component: any, sink: string, req: any, modelActions: string[]) {
75
+ if (!req || typeof req !== 'object') return
76
+ const name = nameOf(component)
77
+ let replies = false
78
+ for (const [action, key, connection] of replyNames(req, !!component?.sources?.[sink]?.__sygnalRouter)) {
79
+ replies = true
80
+ let seen = replySeen.get(component)
81
+ if (!seen) replySeen.set(component, seen = new Set())
82
+ seen.add(action)
83
+ if (modelActions.includes(action) || !once(`SYG112:${name}:${action}`)) continue
84
+ const near = suggest(action, modelActions.filter(a => !a.startsWith('__')))
85
+ devReport('SYG112', {
86
+ component,
87
+ message: (connection
88
+ ? `${sink} connection '${connection}' names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so those events are dropped`
89
+ : `A ${sink} request names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so the reply is dropped`) +
90
+ (near ? ` (did you mean '${near}'?)` : ''),
91
+ fix: near
92
+ ? `Use the existing entry: ${key.startsWith('events.') ? `events: { '${key.slice(7)}': '${near}' }` : `${key}: '${near}'`}`
93
+ : `Add '${action}' to ${name}.model, e.g. ${action}: (state, data) => ({ ...state, ... }), or fix the name`,
94
+ data: {action, key, sink, ...(connection ? {connection} : {}), ...(near ? {suggestion: near} : {}), modelActions},
95
+ })
96
+ }
97
+
98
+ // SYG508 (strict): a request without reply actions whose replies this instance reads back with select()/errors()
99
+ if (replies || 'abort' in req || typeof req.category !== 'string' || !isStrictEnabled()) return
100
+ const read = selected.get(component)?.get(`${sink}\0${req.category}`)
101
+ if (!read || !once(`SYG508:${name}:${sink}:${req.category}`)) return
102
+ // (inside a sink stream: reportSafely, so 'error' mode can't break the stream)
103
+ reportSafely('SYG508', {
104
+ severity: STRICT_CODE_SEVERITY.SYG508,
105
+ component,
106
+ message: `${name} sends a ${sink} request with category: '${req.category}' and reads the reply back with ${read}`,
107
+ fix: `Use reply actions instead: name them in the request, ${sink}: (state) => ({ url, ok: 'LOADED', error: 'FAILED' }), ` +
108
+ `handle LOADED (the parsed body) and FAILED ({ error, status, body, request }) in the model, and remove ${read} from the intent`,
109
+ data: {sink, category: req.category, read},
110
+ })
111
+ }
112
+
113
+ export const repliesCheck: DiagnosticCheck = {
114
+ id: 'replies',
115
+
116
+ sources(component, sources) {
117
+ if (!isStrictEnabled() || typeof Proxy != 'function') return
118
+ const cache = new Map<string, any>()
119
+ return new Proxy(sources, {
120
+ get(t: any, k: any) {
121
+ const v = t[k]
122
+ if (typeof k != 'string' || !v || v.__sygnalReplies !== true) return v
123
+ if (!cache.has(k)) cache.set(k, readingSource(component, k, v))
124
+ return cache.get(k)
125
+ },
126
+ })
127
+ },
128
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Router and declaration statics (PLAN-3 5-4b). Dev-entry-only codes (DEV_CODE_SEVERITY):
3
+ *
4
+ * SYG130 — href() names no route, or leaves out a param its pattern needs (the link is broken).
5
+ * (A `{ to }` command with the same problem is SYG620, reported by the router itself
6
+ * in production too, and not navigated.)
7
+ * SYG131 — href() or a `{ to, params }` command passes params the route's pattern doesn't use.
8
+ * SYG132 — G-167: a component declares a static the core sends to a driver (`route`,
9
+ * `resources`, `connections`, `head`) but never sends it: it has no model (the core
10
+ * wires statics in its model setup), or it is a root without initialState (a static is
11
+ * computed from the state stream, which a root without state never emits).
12
+ * SYG133 — the SPA router (makeRouter().driver without `navigate`) runs inside a Vike app,
13
+ * where Vike owns links and history.
14
+ *
15
+ * Mechanism: the router's href() calls `__SYGNAL_DIAGNOSTICS__.routerHref(routes, name, params)`
16
+ * when it exists (installRouterHooks() sets it); onModel looks at the component's sources for
17
+ * `__sygnalStatic` / `__sygnalRouter` markers.
18
+ */
19
+ import type {DiagnosticCheck} from '../index'
20
+ import {bridge, devReport, once, nameOf, suggest} from './shared'
21
+
22
+ const PARAM = /:(\w+)/g
23
+ const paramsOf = (pat: string) => (pat.match(PARAM) || []).map(p => p.slice(1))
24
+
25
+ function checkHref(routes: Record<string, string>, name: string, params: any) {
26
+ const names = Object.keys(routes).filter(k => routes[k] != '*')
27
+ const pat = routes[name]
28
+ if (pat == null || pat == '*') {
29
+ if (!once(`SYG130:${name}`)) return
30
+ const near = suggest(String(name), names)
31
+ devReport('SYG130', {
32
+ message: `href('${name}') names no route, so the link points to '/'` + (near ? ` (did you mean '${near}'?)` : ''),
33
+ fix: `Use one of the route names: ${names.join(', ')}`,
34
+ data: {name, routes: names, ...(near ? {suggestion: near} : {})},
35
+ })
36
+ return
37
+ }
38
+ const wanted = paramsOf(pat)
39
+ const given = params && typeof params == 'object' ? params : {}
40
+ const missing = wanted.filter(k => given[k] == null)
41
+ const extra = Object.keys(given).filter(k => !wanted.includes(k))
42
+ if (missing.length && once(`SYG130:${name}:${missing}`)) {
43
+ devReport('SYG130', {
44
+ message: `href('${name}') leaves out ${missing.map(k => `'${k}'`).join(', ')} (${pat}), so that segment is empty`,
45
+ fix: `href('${name}', { ${wanted.join(', ')} })`,
46
+ data: {name, pattern: pat, missing},
47
+ })
48
+ }
49
+ if (extra.length && once(`SYG131:${name}:${extra}`)) {
50
+ devReport('SYG131', {
51
+ message: `Route '${name}' (${pat}) has no param ${extra.map(k => `'${k}'`).join(', ')}; it is ignored`,
52
+ fix: wanted.length ? `Pass only ${wanted.join(', ')}; put other values in the query: href('${name}', params, { ${extra[0]} })` : `Put it in the query: href('${name}', {}, { ${extra[0]} })`,
53
+ data: {name, pattern: pat, extra},
54
+ })
55
+ }
56
+ }
57
+
58
+ /** install the hook the router's href() calls (removed by the returned function) */
59
+ export function installRouterHooks(): () => void {
60
+ const core = bridge()
61
+ if (!core) return () => {}
62
+ core.routerHref = checkHref
63
+ return () => { if (core.routerHref === checkHref) core.routerHref = undefined }
64
+ }
65
+
66
+ export const routerCheck: DiagnosticCheck = {
67
+ id: 'router',
68
+
69
+ onModel(component) {
70
+ const sources = component?.sources
71
+ if (!sources || typeof sources != 'object') return
72
+ const name = nameOf(component)
73
+ for (const n of component.sourceNames || Object.keys(sources)) {
74
+ const src = sources[n]
75
+ const k = src?.__sygnalStatic
76
+ if (typeof k == 'string' && component.view?.[k] != null) {
77
+ // statics are computed from state (G-167: with or without a model): a root whose state
78
+ // never emits (a model but no initialState) never sends one
79
+ if (!component.isSubComponent && component.initialState === undefined && once(`SYG132:${name}:${k}`)) {
80
+ devReport('SYG132', {
81
+ component,
82
+ message: `${name} declares ${name}.${k}, but a root without initialState has no state, so ${k} is never sent to ${n}`,
83
+ fix: `Add ${name}.initialState (e.g. { }${k == 'route' ? ', or { route: router.current() }' : ''})`,
84
+ data: {static: k, source: n, reason: 'state'},
85
+ })
86
+ }
87
+ }
88
+ const opts = src?.__sygnalRouter
89
+ const g: any = globalThis
90
+ if (opts && typeof opts == 'object' && !opts.navigate && g.document?.getElementById?.('vike_pageContext') && once('SYG133')) {
91
+ devReport('SYG133', {
92
+ component,
93
+ message: `The SPA router (${n}) runs inside a Vike app: Vike owns links and history, so both would handle a click`,
94
+ fix: "Pass Vike's navigate: makeRouter({ routes, navigate }) with import { navigate } from 'vike/client/router', or use Vike's routing",
95
+ data: {source: n},
96
+ })
97
+ }
98
+ }
99
+ },
100
+ }
@@ -9,7 +9,7 @@
9
9
  * xstream equivalent and the SYG301 docs link. The error is also reported
10
10
  * (collected / printed per the diagnostics mode; in 'error' mode report()
11
11
  * throws the DiagnosticError itself). It works anywhere a stream is used
12
- * (intent, peers, drivers), with no hook in component.ts.
12
+ * (intent, peers, drivers), with no hook in the core.
13
13
  *
14
14
  * Caveat: `typeof stream.pipe === 'function'` is true while this entry is
15
15
  * loaded (dev only).
@@ -11,6 +11,8 @@
11
11
  */
12
12
  import '../index'
13
13
  import type {DiagnosticCheck, DiagnosticDetails, Diagnostic} from '../index'
14
+ import {CODE_TITLES, DEV_CODE_SEVERITY, registerCodes} from '../codes'
15
+ import type {DiagnosticSeverity} from '../codes'
14
16
 
15
17
  interface CoreBridge {
16
18
  registerCheck(check: DiagnosticCheck): () => void
@@ -38,6 +40,16 @@ export const reportSafely = (code: string, details: DiagnosticDetails): Diagnost
38
40
  }
39
41
  }
40
42
 
43
+ registerCodes(Object.keys(DEV_CODE_SEVERITY).map(code =>
44
+ [code, DEV_CODE_SEVERITY[code], CODE_TITLES[code]] as [string, DiagnosticSeverity, string]))
45
+
46
+ /**
47
+ * reportSafely() for the dev-entry-only codes (DEV_CODE_SEVERITY, G-143): the main bundle's
48
+ * core doesn't know their severity, so it is passed explicitly.
49
+ */
50
+ export const devReport = (code: string, details: DiagnosticDetails): Diagnostic | undefined =>
51
+ reportSafely(code, {severity: DEV_CODE_SEVERITY[code], ...details})
52
+
41
53
  /** Tunables (test seam: configureChecks()). */
42
54
  export const timing = {
43
55
  /** delay after a render before DOM checks run (lets nested renders patch) */
@@ -48,8 +60,76 @@ export const timing = {
48
60
  minRenders: 3,
49
61
  }
50
62
 
51
- /** Built-in actions Sygnal dispatches itself; never "missing" or "unreachable". */
52
- export const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'HYDRATE', 'DISPOSE', 'READY'])
63
+ /**
64
+ * Built-in actions Sygnal dispatches itself; never "missing" or "unreachable".
65
+ * (HYDRATE is not one since 6.0, D66: nothing dispatches it, so it is an ordinary action.)
66
+ */
67
+ export const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'DISPOSE', 'READY', 'RESOURCE'])
68
+
69
+ /** Sinks the core handles itself: their values never go to a reply-action driver. */
70
+ // (PLAN-4 GS-2: ELEMENT too; `{ scrollIntoView: Row, block: 'center' }` names no action)
71
+ const NON_REPLY_SINK = /^(STATE|EFFECT|EVENTS|PARENT|READY|DOM|CHILD|ELEMENT|PERSIST)$/
72
+
73
+ // `ok: 'X'` / `"error": "X"` in function source (minified code keeps string literals and keys)
74
+ const keyedNames = (keys: string) => new RegExp(`(?:^|[{,\\s])["']?(?:${keys})["']?\\s*:\\s*(["'\`])([\\w$.:/-]+)\\1`, 'g')
75
+ // (PLAN-3 5-4b: and a router `{ block: 'ACTION' }`)
76
+ const REPLY_IN_SOURCE = keyedNames('ok|error|block')
77
+ const CONNECTION_IN_SOURCE = keyedNames('message|open|close|error')
78
+ // G-224: a `timers` static's `action: 'TICK'` / `frame: 'FRAME'`
79
+ const TIMER_IN_SOURCE = keyedNames('action|frame')
80
+ // PLAN-5 B-3: a `browser` static's `action: 'SEEN'` / `error: 'GEO_FAILED'`
81
+ const BROWSER_IN_SOURCE = keyedNames('action|error')
82
+
83
+ const namesIn = (fn: any, re: RegExp, out: Set<string>) => {
84
+ if (typeof fn !== 'function') return
85
+ let src = ''
86
+ try { src = Function.prototype.toString.call(fn) } catch (_) { return }
87
+ re.lastIndex = 0
88
+ for (let m = re.exec(src); m; m = re.exec(src)) out.add(m[2])
89
+ }
90
+
91
+ /**
92
+ * Reply actions a component's requests and `connections` static can name, read from the
93
+ * source of its non-STATE sink functions (`HTTP: (s) => ({ url, ok: 'LOADED', error: 'FAILED' })`)
94
+ * and of `connections` (message/open/close/error). A heuristic for SYG102 (PLAN-3): a name
95
+ * built at run time is missed (SYG102 stays info, so that only costs a false hint).
96
+ */
97
+ export function replyNamesOf(component: any): Set<string> {
98
+ const out = new Set<string>()
99
+ const model = component && component.model
100
+ if (model && typeof model === 'object') {
101
+ for (const key of Object.keys(model)) {
102
+ const value = model[key]
103
+ const bar = key.indexOf('|')
104
+ if (bar >= 0) {
105
+ if (!NON_REPLY_SINK.test(key.slice(bar + 1).trim())) namesIn(value, REPLY_IN_SOURCE, out)
106
+ } else if (value && typeof value === 'object') {
107
+ for (const sink of Object.keys(value)) {
108
+ if (!NON_REPLY_SINK.test(sink)) namesIn(value[sink], REPLY_IN_SOURCE, out)
109
+ }
110
+ }
111
+ }
112
+ }
113
+ namesIn(component?.view?.connections, CONNECTION_IN_SOURCE, out)
114
+ // G-224: makeTimerDriver() replies the actions the `timers` static names (a function of the
115
+ // state, or an object of them)
116
+ const timers = component?.view?.timers
117
+ if (timers && typeof timers === 'object') for (const k in timers) namesIn(timers[k], TIMER_IN_SOURCE, out)
118
+ else namesIn(timers, TIMER_IN_SOURCE, out)
119
+ // PLAN-5 B-3: the browser driver replies the actions the `browser` static names
120
+ const browser = component?.view?.browser
121
+ if (browser && typeof browser === 'object') for (const k in browser) namesIn(browser[k], BROWSER_IN_SOURCE, out)
122
+ else namesIn(browser, BROWSER_IN_SOURCE, out)
123
+ // PLAN-3 5-4b: the router replies the action a `route` static names
124
+ const route = component?.view?.route
125
+ if (typeof route === 'string') out.add(route)
126
+ // PLAN-4 GS-5: persist() adds RESTORE (sent after hydration and by sync)
127
+ if (component?.view?.persist) out.add('RESTORE')
128
+ return out
129
+ }
130
+
131
+ /** Reply actions a component instance's requests were seen naming (replies.ts → inspect). */
132
+ export const replySeen = new WeakMap<object, Set<string>>()
53
133
 
54
134
  /** Synthetic/internal actions (`__TEST_ACTION__`, `__NOOP_ACTION__`, ...) and built-ins. */
55
135
  export const isInternalAction = (action: string): boolean =>
@@ -59,6 +139,10 @@ let seen = new Set<string>()
59
139
 
60
140
  /** true the first time `key` is seen (dedupes reports, e.g. per component name). */
61
141
  export const once = (key: string): boolean => (seen.has(key) ? false : (seen.add(key), true))
142
+ /** undo once(key): the report it guarded wasn't made (G-470: diagnostics off) */
143
+ export const unsee = (key: string): void => void seen.delete(key)
144
+ /** whether once(`key`) has reported already (a check can skip its work: SYG435) */
145
+ export const onced = (key: string): boolean => seen.has(key)
62
146
 
63
147
  /**
64
148
  * Forget only the once() dedupe set (G-051: renderComponent calls it through the core bridge
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SYG115 — unknown DOM event shorthand (warn, G-143).
3
+ *
4
+ * The core's DOM source Proxy (the DOM source wrapper) turns ANY unknown property
5
+ * into an event shorthand: `DOM.<name>(sel)` = `DOM.select(sel).events('<name>')`. A made-up
6
+ * name such as `DOM.key('.x')` or `DOM.enter('.x')` therefore listens for an event the browser
7
+ * never fires, and the action silently never happens.
8
+ *
9
+ * Mechanism: the `sources` hook (called before each component's intent) wraps the DOM source
10
+ * in a second Proxy. Calling a shorthand whose name is not a known DOM event reports SYG115
11
+ * (once per component name and event), then behaves exactly like the core shorthand. Only the
12
+ * call is reported, not the property read, so tools that inspect the source (pretty-printers)
13
+ * don't trigger it. `DOM.select(sel).events(name)` is never checked: it is the explicit form
14
+ * for custom events.
15
+ */
16
+ import type {DiagnosticCheck} from '../index'
17
+ import {devReport, once, nameOf, suggest} from './shared'
18
+
19
+ // Standard DOM / UI event names (lower case). In a browser, `on<name>` on an element or the
20
+ // window also counts, so newer events are accepted there too.
21
+ const KNOWN = new Set((
22
+ 'abort afterprint animationcancel animationend animationiteration animationstart auxclick ' +
23
+ 'beforeinput beforeprint beforetoggle beforeunload blur cancel canplay canplaythrough change click close ' +
24
+ 'compositionend compositionstart compositionupdate contextlost contextmenu contextrestored copy cuechange cut ' +
25
+ 'dblclick drag dragend dragenter dragleave dragover dragstart drop durationchange emptied ended error ' +
26
+ 'focus focusin focusout formdata fullscreenchange fullscreenerror gotpointercapture hashchange input invalid ' +
27
+ 'keydown keypress keyup languagechange load loadeddata loadedmetadata loadstart lostpointercapture message ' +
28
+ 'messageerror mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup offline online pagehide ' +
29
+ 'pageshow paste pause play playing pointercancel pointerdown pointerenter pointerleave pointermove pointerout ' +
30
+ 'pointerover pointerrawupdate pointerup popstate progress ratechange rejectionhandled reset resize scroll ' +
31
+ 'scrollend search securitypolicyviolation seeked seeking select selectionchange selectstart slotchange stalled ' +
32
+ 'storage submit suspend timeupdate toggle touchcancel touchend touchmove touchstart transitioncancel ' +
33
+ 'transitionend transitionrun transitionstart unhandledrejection unload visibilitychange volumechange waiting wheel'
34
+ ).split(' '))
35
+
36
+ const isKnown = (name: string): boolean => {
37
+ // PLAN-5 0-S6: a hyphenated name is a custom element's event (DOM['wa-hover']('.food')): no
38
+ // standard DOM event has a hyphen, and a bracketed name is no typo for one
39
+ if (KNOWN.has(name) || name.includes('-')) return true
40
+ const on = 'on' + name
41
+ const g: any = globalThis
42
+ try {
43
+ return !!((g.HTMLElement && on in g.HTMLElement.prototype) || (g.window && on in g.window) ||
44
+ (g.Document && on in g.Document.prototype))
45
+ } catch (_) { return false }
46
+ }
47
+
48
+ // Names agents reach for that are enriched-stream methods or key names, not events
49
+ const KEY_FIX = (sel: string, key?: string) =>
50
+ `Listen for the real event and read the key: DOM.keydown(${sel}).key()` +
51
+ (key ? `.filter(k => k === '${key}')` : '')
52
+ const SPECIAL: Record<string, (sel: string) => string> = {
53
+ key: sel => KEY_FIX(sel),
54
+ keys: sel => KEY_FIX(sel),
55
+ enter: sel => KEY_FIX(sel, 'Enter'),
56
+ escape: sel => KEY_FIX(sel, 'Escape'),
57
+ esc: sel => KEY_FIX(sel, 'Escape'),
58
+ value: sel => `Listen for the real event and read the value: DOM.input(${sel}).value()`,
59
+ checked: sel => `Listen for the real event and read the box: DOM.change(${sel}).checked()`,
60
+ data: sel => `Listen for the real event and read the attribute: DOM.click(${sel}).data('name')`,
61
+ doubleclick: sel => `Use DOM.dblclick(${sel})`,
62
+ }
63
+
64
+ const fixFor = (name: string, selector: any): string => {
65
+ const sel = typeof selector === 'string' ? `'${selector}'` : 'sel'
66
+ const special = SPECIAL[name.toLowerCase()]
67
+ if (special) return special(sel)
68
+ const s = suggest(name, KNOWN)
69
+ return (s ? `Did you mean DOM.${s}(${sel})? ` : '') +
70
+ `If '${name}' is a custom event you dispatch yourself, use the explicit form DOM.select(${sel}).events('${name}')`
71
+ }
72
+
73
+ const wrapped = new WeakMap<object, any>()
74
+
75
+ function checkedDOM(component: any, dom: any): any {
76
+ if (!dom || typeof dom != 'object') return dom
77
+ let proxy = wrapped.get(dom)
78
+ if (proxy) return proxy
79
+ const name = nameOf(component)
80
+ proxy = new Proxy(dom, {
81
+ get(t: any, k: any) {
82
+ const v = t[k]
83
+ if (typeof k != 'string' || k in t || typeof v != 'function' || isKnown(k)) return v
84
+ return function (this: any, selector: any, ...rest: any[]) {
85
+ if (once(`SYG115:${name}:${k}`)) {
86
+ devReport('SYG115', {
87
+ component,
88
+ message: `${name}.intent uses DOM.${k}(${typeof selector == 'string' ? `'${selector}'` : '…'}), but '${k}' is not a DOM event, so it never fires`,
89
+ fix: fixFor(k, selector),
90
+ data: {event: k, selector},
91
+ })
92
+ }
93
+ return v.call(this, selector, ...rest)
94
+ }
95
+ },
96
+ })
97
+ wrapped.set(dom, proxy)
98
+ return proxy
99
+ }
100
+
101
+ export const shorthandCheck: DiagnosticCheck = {
102
+ id: 'shorthand',
103
+
104
+ sources(component, sources) {
105
+ if (!sources || typeof Proxy != 'function') return
106
+ const domName = component?.DOMSourceName || 'DOM'
107
+ if (!sources[domName]) return
108
+ return new Proxy(sources, {
109
+ get(t: any, k: any) {
110
+ const v = t[k]
111
+ return k === domName ? checkedDOM(component, v) : v
112
+ },
113
+ })
114
+ },
115
+ }