sygnal 6.6.0 → 7.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 (91) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/README.md +2 -0
  3. package/dist/astro/index.cjs.js +32 -9
  4. package/dist/astro/index.cjs.js.map +1 -1
  5. package/dist/astro/index.mjs +32 -9
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/server.cjs.js +32 -43
  8. package/dist/astro/server.cjs.js.map +1 -1
  9. package/dist/astro/server.mjs +32 -43
  10. package/dist/astro/server.mjs.map +1 -1
  11. package/dist/devtools.cjs.js.map +1 -1
  12. package/dist/devtools.esm.js.map +1 -1
  13. package/dist/diagnostics.cjs.js +154 -6
  14. package/dist/diagnostics.cjs.js.map +1 -1
  15. package/dist/diagnostics.esm.js +153 -7
  16. package/dist/diagnostics.esm.js.map +1 -1
  17. package/dist/guide/accessibility.md +3 -1
  18. package/dist/guide/ai-chat.md +6 -6
  19. package/dist/guide/behaviors.md +1 -1
  20. package/dist/guide/element-commands.md +4 -2
  21. package/dist/guide/forms-reference.md +89 -3
  22. package/dist/guide/forms.md +8 -8
  23. package/dist/guide/http.md +77 -7
  24. package/dist/guide/migrating-to-7.md +314 -0
  25. package/dist/guide/recipes/ai-summarize.md +2 -1
  26. package/dist/guide/recipes/i18n.md +1 -1
  27. package/dist/guide/resources.md +2 -0
  28. package/dist/guide/timers.md +3 -3
  29. package/dist/guide/ui/tabs.md +5 -4
  30. package/dist/guide/ui/tooltip.md +24 -0
  31. package/dist/index.cjs.js +954 -388
  32. package/dist/index.cjs.js.map +1 -1
  33. package/dist/index.d.ts +300 -46
  34. package/dist/index.esm.js +953 -389
  35. package/dist/index.esm.js.map +1 -1
  36. package/dist/sygnal.min.js +1 -1
  37. package/dist/sygnal.min.js.map +1 -1
  38. package/dist/ui.cjs.js +96 -11
  39. package/dist/ui.cjs.js.map +1 -1
  40. package/dist/ui.esm.js +96 -12
  41. package/dist/ui.esm.js.map +1 -1
  42. package/dist/vike/config/package.json +1 -1
  43. package/dist/vite/plugin.cjs.js +19 -9
  44. package/dist/vite/plugin.cjs.js.map +1 -1
  45. package/dist/vite/plugin.mjs +19 -9
  46. package/dist/vite/plugin.mjs.map +1 -1
  47. package/llms.txt +23 -22
  48. package/package.json +1 -1
  49. package/src/astro/index.d.ts +8 -0
  50. package/src/astro/index.ts +18 -0
  51. package/src/core/actions.ts +28 -21
  52. package/src/core/define.ts +2 -0
  53. package/src/core/hooks.ts +2 -2
  54. package/src/core/hosts/collection.ts +4 -3
  55. package/src/core/hosts/switchable.ts +26 -8
  56. package/src/core/instance.ts +66 -17
  57. package/src/core/runtime.ts +5 -1
  58. package/src/core/statics.ts +5 -5
  59. package/src/extra/ai/chat/behavior.ts +3 -3
  60. package/src/extra/ai/commandBar.ts +3 -3
  61. package/src/extra/ai/compose/behavior.ts +3 -3
  62. package/src/extra/ai/transports/shared.ts +10 -2
  63. package/src/extra/behaviors.ts +24 -3
  64. package/src/extra/diagnostics/checks/fetch.ts +21 -1
  65. package/src/extra/diagnostics/checks/forms.ts +4 -3
  66. package/src/extra/diagnostics/checks/index.ts +1 -0
  67. package/src/extra/diagnostics/checks/next.ts +49 -3
  68. package/src/extra/diagnostics/checks/props.ts +6 -1
  69. package/src/extra/diagnostics/checks/public.d.ts +12 -0
  70. package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
  71. package/src/extra/diagnostics/codes.ts +24 -0
  72. package/src/extra/fetchDriver.ts +115 -7
  73. package/src/extra/fetchEventSource.ts +117 -0
  74. package/src/extra/form.ts +68 -26
  75. package/src/extra/formHelpers.ts +3 -1
  76. package/src/extra/lens.ts +3 -2
  77. package/src/extra/queryCache.ts +4 -3
  78. package/src/extra/socketDriver.ts +60 -9
  79. package/src/extra/ssr.ts +30 -41
  80. package/src/extra/testing.ts +100 -18
  81. package/src/extra/timers.ts +5 -2
  82. package/src/index.d.ts +299 -45
  83. package/src/index.ts +3 -0
  84. package/src/shared.ts +3 -0
  85. package/src/ui/dialog.ts +3 -2
  86. package/src/ui/tooltips.ts +92 -0
  87. package/src/ui.d.ts +35 -0
  88. package/src/ui.ts +1 -0
  89. package/src/vite/graph.ts +11 -3
  90. package/src/vite/plugin.d.ts +7 -0
  91. package/src/vite/plugin.ts +10 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,264 @@
2
2
 
3
3
  All notable changes to Sygnal are listed here. Versions follow [semantic versioning](https://semver.org). Releases before 5.4.0 are described in the [GitHub releases](https://github.com/tpresley/sygnal/releases) and tags.
4
4
 
5
+ ## 7.1.0 — 2026-10-20
6
+
7
+ Fixes and additions from building an app (Temper's Tune UI) on 7.0 ([PLAN-13](dev-plans/PLAN-13.md)):
8
+ an invalidation / navigation race, declaration statics that see the context, per-page Switchable
9
+ state, SSE resume and dead-stream detection, reply helpers for hand-written drivers, and more
10
+ precise `sygnal-check` findings. All additive.
11
+
12
+ **Measured impact.** Size: the kanban gate is (a) 42,982 B with `nativeGlobalThis: false` (7.0.0:
13
+ 42,844 B; budget 43,000 B, 18 B headroom) and (b) 38,987 B by default. Statics with context cost
14
+ 19 B and the per-page Switchable map 126 B (it reuses the owner's binding watch for a page whose
15
+ record is deleted); everything else is in drivers, the dev diagnostics entry, the test helper or
16
+ `sygnal-check`, and adds nothing to an app that doesn't use it. The perf count gate is within its
17
+ limits.
18
+
19
+ **Companion packages.**
20
+ - **`sygnal-check` 0.12.0:** SYG117 (`next()` to an action with no entry here), SYG302 (`import xs
21
+ from 'xstream'`, with `--fix`), SYG429 (a Switchable state map key that names no page); reply names
22
+ through request helpers and consts; SYG256 sees past helper calls in other fields; `next` followed
23
+ into helpers, imported ones included; context reads in the statics count for SYG170; `replyKeys`
24
+ in the config file. Its caret range doesn't reach 0.12.0 from `^0.11.0`, so update the dev
25
+ dependency.
26
+ - **`create-sygnal-app` 2.10.0:** templates depend on `sygnal` ^7.1.0 and `sygnal-check` ^0.12.0, use
27
+ a system font stack (no Google Fonts) and scaffold a `.gitignore`.
28
+ - **VS Code extension 0.9.0:** bundles `sygnal-check` 0.12.0.
29
+ - **Claude Code plugin 0.2.1:** the `sygnal-dev` skill covers 7.1 (statics with context, per-page
30
+ Switchable state, SSE resume, reply helpers, `import { xs } from 'sygnal'`).
31
+
32
+ ### Added
33
+
34
+ - **SYG117: `next()` to an action the component has no entry for (PLAN-13 N-1).** `next('X')`
35
+ reaches only its own component's model, so a child's `next('ENTER')` for an action only its
36
+ parent handles silently did nothing. The dev checks (`sygnal/diagnostics`, 0 production bytes)
37
+ warn on the first such `next()` per component and name, naming the ancestor that handles it and
38
+ the fix (send it up with `PARENT` or `EVENTS`), or the closest entry for a typo. `sygnal-check`
39
+ reports a literal `next('X')` with no entry `X` in the same component (with a rename fix for a typo).
40
+
41
+ - **sygnal-check reads reply names through request helpers and consts (PLAN-13 C-1).** An object
42
+ argument with a reply key of a call a sink returns is a request (`req('/a', { ok: 'GOT_A' })`,
43
+ `api.get_run(args, { ok: 'RUN' })`, any callee), a returned const is followed (`HTTP: () => SNAP`),
44
+ and so are `connections` specs and SSE `events` maps held in consts. SYG102 no longer reports
45
+ their reply actions as unreachable, SYG112 checks the names, SYG152 and `--graph` see the requests.
46
+
47
+ - **SYG256 sees past helper calls in other fields (PLAN-13 C-2).** A call in the STATE reducer
48
+ stopped the proof, so a leave-guard like `EDIT: { STATE: (s, d) => ({ ...s, dirty: true, edit:
49
+ { ...s.edit, [keyOf(s)]: d } }), ROUTER: (s) => s.dirty ? ABORT : … }` went unreported. A call
50
+ (or a function) inside a plain field's value now only makes that field unknown; calls in a test,
51
+ a spread or a computed key of the written object, and any assignment, still keep the check out.
52
+
53
+ - **create-sygnal-app: no Google Fonts, and a `.gitignore` (PLAN-13 T-1).** The vite, vite-pwa and
54
+ astro templates loaded Inter from Google Fonts; every template now uses a system font stack and
55
+ loads nothing remote. No template wrote a `.gitignore` (npm leaves it out of the package): each
56
+ ships `gitignore`, scaffolded as `.gitignore` (`node_modules`, `dist`, `.vite`, `.astro`,
57
+ `*.local`, `dev-dist` for the PWA). The release smoke checks both.
58
+
59
+ - **SYG302: `import xs from 'xstream'` (PLAN-13 X-1).** Under Node ESM and Vite 8 (a `"type":
60
+ "module"` package) xstream's CommonJS default import is the module object, so a custom driver's
61
+ `xs.create` is undefined. The docs, llms.txt and the skill use `import { xs } from 'sygnal'`
62
+ (the accessibility guide's sample was the last default import), the custom-drivers guide says
63
+ why, and `sygnal-check` reports the default import (info) with a `--fix`.
64
+
65
+ - **`makeReplies()` and `senderOf()` for drivers you write (PLAN-13 D-1).** The reply-action
66
+ protocol `makeFetchDriver`, `driverFromAsync` and the socket driver use is exported and typed
67
+ (`ReplySender`, `ReplyChannel`): spread `replies` into the source, `reply(senderOf(req), type,
68
+ data)` as often as needed (progress, then a result), and abort a disposed sender's work in
69
+ `onStop`. The custom-drivers guide has an upload driver that replies more than once.
70
+
71
+ - **SSE event ids, resuming and dead streams (PLAN-13 S-1).** On a `connections` spec: `meta:
72
+ true` gives SSE `message` / `events` actions `{ data, id, event }`; `resume: true | 'param'`
73
+ keeps the last event id and makes a reconnect the driver makes (after EventSource gave up, e.g. a
74
+ proxy 502, or a non-2xx on the `headers` path) resume from it, as `Last-Event-ID` over fetch or
75
+ `?lastEventId=` (or `?param=`) on the EventSource URL; `staleAfterMs` (SSE and WebSocket) closes
76
+ a connection that stays silent that long as a drop (`close` gets `stale: true`) so the reconnect
77
+ policy applies; over fetch, heartbeat comments count. `t.push(sink, data, { id })` gives a test
78
+ event its id. No bytes for apps without a socket driver.
79
+
80
+ - **Declaration statics see the context (PLAN-13 F-1).** `resources`, `timers`, `connections`,
81
+ `head` and `browser` get `(state, context)` and are computed again when the component's context
82
+ changes (by identity, D603; an unrelated write re-runs nothing). A routed page reads `route` from
83
+ context (`App.context = { route: (s) => s.route }`, `TaskPage.resources = { task: (s, { route })
84
+ => … }`) instead of keeping a copy of the params in its own state. Server rendering passes the
85
+ context to `resources` and `head` too; the types take it as the second parameter; `sygnal-check`
86
+ counts context reads in the statics for SYG170. Core: +19 B (kanban gzip).
87
+
88
+ - **Per-page Switchable state (PLAN-13 F-3).** `state` takes a map from page names to bindings
89
+ (`state={{ projects: 'pages.projects', settings: byId('teams', state.teamId) }}`): each listed
90
+ page binds its own key, path, lens or byId selection with a tag child's lifetime rules (another
91
+ target makes only that page again; a deleted target disposes it on its last state), and the
92
+ pages not listed share the owner's state. A key that names no page is the new **SYG429** (dev
93
+ checks and `sygnal-check`, with a rename fix). Types infer the page names from `of`. Core:
94
+ +126 B (kanban gzip), leaving 18 B of the 43,000 B budget with F-1.
95
+
96
+ - **Deep-link tests start on their route (PLAN-13 F-2).** `renderComponent(App, { router, url })`
97
+ applies the root's `route` entry's STATE reducer to the initial state with the `url`'s route
98
+ before the first render: `initialState`'s `router.current()` ran at module load, so a test used
99
+ to render `/` first and send its requests. The entry's other sinks (a guard's redirect) still run
100
+ once, when the router answers. Tests only; no core change.
101
+
102
+ - **sygnal-check follows `next` into helpers (PLAN-13 C-3).** A model that passes `next` to a
103
+ function (`advance(state, next)`), imported or in the same file and with any parameter name, has
104
+ that function's `next('X')` calls counted as its own, 3 calls deep: SYG102 sees the targets, and
105
+ SYG117 reports a bad one in the helper's file. A helper it can't read (a package) makes the
106
+ targets unknown, so SYG102 drops to info. The language server re-checks the component when the
107
+ helper changes.
108
+
109
+ - **`replyKeys` in sygnal-check's config (PLAN-13 C-4).** A custom driver's own reply keys
110
+ (`cancelled: 'UP_CANCELLED'`) can be listed in `sygnal-check.config.json`, for every sink
111
+ (`["cancelled"]`) or per sink (`{ "UPLOAD": ["progress", "cancelled"] }`): their actions count for
112
+ SYG102 and are checked by SYG112. Without the setting, a request key the checker doesn't know
113
+ whose value names a model entry makes that entry's SYG102 info, pointing at `replyKeys`.
114
+
115
+ ### Fixed
116
+
117
+ - **Follow-ups from trying the PLAN-13 build in Temper.** A Switchable page with a per-page
118
+ `state` map no longer reports SYG106 for the Switchable's own `state` prop (every page did, so
119
+ `expectNoDiagnostics()` failed). The `sygnal-check` command line reads `replyKeys` (check,
120
+ `--fix`, `--graph`, `--base`; the API and the language server already did). SSE `resume` replaces
121
+ an id parameter the URL already has instead of adding a second one. An `every` timer ticks once
122
+ per interval under a test clock that freezes `Date.now()` while timers run (Playwright's
123
+ `setFixedTime()`): its drift-free delays had grown by one interval per tick (1, 3, 6, 10 s).
124
+ - **An invalidation no longer resends a resource's old request after a navigation (PLAN-13 R-1).**
125
+ When a write's `ok` action navigated (`ROUTER`) and its `invalidates` (a URL prefix or a tag)
126
+ matched a resource of a lens- or key-bound page, the page's new request was sent and then the
127
+ invalidation sent the old one in the same lane, cancelling the new one: the new page showed the
128
+ old record. A load now stops when the resource's request changed (or it was paused) while it
129
+ wrote its loading state; refresh, polling and focus refetches go through the same path.
130
+
131
+ ## 7.0.0 — 2026-10-19
132
+
133
+ **Read [Migrating to 7.0](https://sygnal.js.org/guide/migrating-to-7/) before upgrading.** 7.0
134
+ has one reducer timing contract and no option to switch back; apps that aren't ready stay on 6.x.
135
+
136
+ **Measured impact.** Size: the kanban gate is (a) 42,844 B with `nativeGlobalThis: false` (6.6.0:
137
+ 42,495 B; budget raised from 42,700 B to 43,000 B for the binding lifetimes) and (b) 38,834 B by
138
+ default. Most of the growth is the binding-lifetime bookkeeping; the single timing contract is
139
+ smaller than the opt-in switch it replaced (−181 B against the 7.0 candidate). The timing audit is
140
+ in the dev diagnostics entry (0 production bytes), and the upload, SSE, form and `tooltips()`
141
+ features add nothing to an app that doesn't use them. The perf count gate is within its limits.
142
+
143
+ **Companion packages.**
144
+ - **`sygnal-check` 0.11.0:** checks reducer timing and binding changes against the Sygnal version
145
+ installed for each source package (7: every output gets the committed state, changed known
146
+ targets remount; 6: the 6.x contracts). New **SYG256** (an output guard that rejects the state
147
+ its own action writes, in a Sygnal 7 project); **SYG454** (an unkeyed dynamic `byId` selection)
148
+ only for an installed Sygnal 6. SYG731 accepts state announced through text, `aria-label`,
149
+ `aria-live` or `role="status"`; SYG609 also reports intent sources with no driver; `progress`
150
+ counts as a reply-action key (SYG102 / SYG112). Explanations for SYG257 and SYG636. Its caret
151
+ range doesn't reach 0.11.0 from `^0.10.0`, so update the dev dependency.
152
+ - **`create-sygnal-app` 2.9.0:** every template depends on `sygnal` ^7.0.0 and `sygnal-check`
153
+ ^0.11.0 and has `npm run check` (TypeScript templates: sygnal-check and `tsc --noEmit`).
154
+ Scaffolding into a directory that holds only `.git` and/or `.DS_Store` keeps that metadata, and
155
+ `.` gets a valid package name from the directory.
156
+ - **VS Code extension 0.8.0:** bundles `sygnal-check` 0.11.0 for Sygnal 7 and existing Sygnal 6
157
+ apps; workspace checkers need 0.11.0 or later (earlier ones fall back to the bundled checker).
158
+ - **Claude Code plugin 0.2.0:** the `sygnal-dev` skill describes Sygnal 7; a project's
159
+ `sygnal-check` older than 0.11.0 is refused only when the project has Sygnal 7.
160
+
161
+ ### Breaking: STATE runs first, and output sinks see its result
162
+
163
+ In every model entry STATE now runs first, whatever the key order, and every other sink (EFFECT,
164
+ EVENTS, PARENT, ELEMENT, HTTP and other drivers) gets the committed state, calculated fields
165
+ included, as `state` and `props.state`. The new `props.previousState` (fourth argument) is the
166
+ state from before the action; a STATE reducer gets the old state in both. 6.x gave every reducer
167
+ the state from before the action.
168
+
169
+ - An output that computed the update itself now applies it twice: in
170
+ `INC: { STATE: s => ({ ...s, n: s.n + 1 }), PARENT: s => ({ n: s.n + 1 }) }`, PARENT becomes
171
+ `s => ({ n: s.n })`.
172
+ - A value STATE clears or deletes (a submitted draft, a removed record) is read from
173
+ `previousState`.
174
+ - A guard that admits the action (a loading or status flag STATE sets) tests `previousState`.
175
+ Keep the output guard: STATE returning `ABORT` doesn't cancel the other sinks.
176
+ - When STATE returns `ABORT` or the same state, or throws, the other sinks still run, with
177
+ `state === previousState`.
178
+ - When STATE removes the component's own state (`STATE: () => undefined` in a Collection item, or
179
+ a child bound with `state="x"` / `byId()` deleting its record), the other sinks get its last
180
+ state, so `REMOVE: { STATE: () => undefined, EVENTS: event('REMOVED', (s) => s.id) }` works as
181
+ before.
182
+ - Behaviors: the first argument is the slice after STATE, `props.state` the host state after
183
+ STATE, `props.previousState` the host state before the action; the old slice is
184
+ `props.previousState[key]`. Every built-in behavior (form, chat, compose, commandBar, undo,
185
+ pager, selection, sortable, and the `sygnal/ui` dialog, popover, tooltip, tabs, accordion and
186
+ disclosure) is on this contract.
187
+ - Libraries that support 6 and 7 read old values as `props.previousState ?? state`.
188
+
189
+ ### New: the timing audit (SYG257)
190
+
191
+ An opt-in dev check for upgrading 6.x code: with `renderComponent(C, { timingAudit: true })`,
192
+ `sygnal({ diagnostics: { timingAudit: true } })` in `vite.config`, `configureTimingAudit(true)`
193
+ from `'sygnal/diagnostics'` or `globalThis.__SYGNAL_TIMING_AUDIT__ = true`, every output sink that
194
+ reads a top-level state field its action's STATE changed reports **SYG257** (info, once per
195
+ component, action, sink and field). Keep `state` where the new value is meant; read
196
+ `previousState` where the old one was. It is off by default because in code written for 7.0
197
+ reading the new value is the intent, and it adds nothing to production builds.
198
+
199
+ sygnal-check's **SYG256** (warning) reports an output guard that contradicts the same action's
200
+ literal STATE write, the loading/admission pitfall above. sygnal-check reads the installed
201
+ Sygnal version per package; **SYG454** (a `byId()` selection without a key) applies only to
202
+ Sygnal 6.
203
+
204
+ ### Breaking: a child follows the record its state binding points at
205
+
206
+ When a child's `state` path, or its `byId()` field, id or idKey, changes, Sygnal disposes the old
207
+ child and mounts a new one: `DISPOSE` runs with the last state, pending requests, delayed `next()`
208
+ actions and subscriptions are cancelled, initialization reruns and local state starts from its
209
+ defaults. The child's DOM is replaced, so focus, caret, scroll position and in-progress input
210
+ inside it are lost. The same applies to Collection/VirtualCollection `from` and Switchable
211
+ `state` (all items or pages, hidden ones included, even when ids overlap). Deleting the bound
212
+ record disposes the child too, after the deleting action's outputs; re-adding the same id gets a
213
+ new child, never the old pending work. A new `byId()` call with the same arguments, field edits
214
+ and reordering keep the child. `key={selected.id}` is no longer needed for a selection.
215
+
216
+ **A stable explicit key no longer keeps a child across a target change**: lift state that must
217
+ survive the change into the parent. A child with a custom lens keeps its binding until its key
218
+ changes.
219
+
220
+ ### New: uploads, SSE auth, form files and types, `tooltips()`
221
+
222
+ - **Upload and download progress** on `makeFetchDriver` requests: `progress: 'UPLOADING'` (a reply
223
+ action) or `progress: true` (`HTTP.progress(category)`) sends `{ phase, loaded, total, fraction }`
224
+ steps, throttled to one every 50 ms per phase. Such a request goes over XMLHttpRequest, the only
225
+ way to see upload progress in every browser; fetch-only `init` options it can't honour are
226
+ SYG636. Tests: `t.progress('HTTP', { loaded, total }, 'UPLOADING')`.
227
+ - **`multipart: { file, title }`** on a request: the multipart/form-data body as data, the way
228
+ `json` is; a model never builds a `FormData`.
229
+ - **The `form` behavior** takes file inputs (a field's value is its `File`, `multiple` an array,
230
+ validated by the schema), keeps the submit's progress in `state.form.progress`
231
+ (`progress: 'form.PROGRESS'`) and counts start-overs in `state.form.resets` (key a file input
232
+ with it). `request: { url }` (or `(data, state) => request`) sends a valid submit itself, as
233
+ JSON or multipart, with `form.DONE` / `form.ERRORS` wired: the common save needs no model entry.
234
+ `form.RESET` from a click goes back to the start values (it used to take the event as values).
235
+ - **Form types from the schema**: a typed Standard Schema (zod, valibot, arktype) types
236
+ `state.form.values` and `fields.x.value`; `FormOutput<typeof uses.form>` is the submit data.
237
+ - **SSE connections take `headers`** (`{ authorization: 'Bearer ' + state.token }`): such a stream
238
+ is read with fetch (`Last-Event-ID`, `retry`), since EventSource can't send headers.
239
+ - **`tooltips()`** in `sygnal/ui`: one behavior and one state slice for every tooltip of a
240
+ component (glossary terms, icon toolbars), one tip at a time.
241
+ - `Component<State, any>` keeps `state` typed; SYG731 accepts state announced through text,
242
+ `aria-label`, `aria-live` or `role="status"`; sygnal-check's SYG609 also reports intent sources
243
+ with no driver; a test wait that times out on the fake clock names the pending timer; every
244
+ create-sygnal-app template has `npm run check` (TypeScript: sygnal-check and `tsc --noEmit`).
245
+
246
+ ### Also included
247
+
248
+ - A child that fails to instantiate is no longer retried on every parent render.
249
+ - State-wait timeout messages report queued simulated inputs; the testing guide separates
250
+ serialized simulated input from synchronous native bursts.
251
+ - TypeScript tests can use `t.query<HTMLInputElement>(selector)` and `t.queryAll<…>(selector)`
252
+ (type assertions, not runtime checks); typed control tokens keep their inference.
253
+ - The combined Rollup build exits normally (compiler watcher lifecycle fix).
254
+ - The release workflow checks the CHANGELOG headings of the new versions before the gates and
255
+ any publish.
256
+
257
+ ### Changed
258
+
259
+ - **The app graph's banner line says why it's missing.** When the dev server's graph route is on but the project has no `sygnal-check` 0.10.0 or later, `sygnal/vite` prints `➜ Sygnal graph: needs sygnal-check 0.10.0 or later in this project (npm i -D sygnal-check)` instead of nothing (the URL already answered 404 with the same hint).
260
+ - **`astro dev` prints the graph page's address** under Astro's own banner (Astro never runs Vite's URL printing), and the Astro integration takes the `graph` option (`sygnal({ graph: false })`).
261
+ - **The repository's examples link `sygnal-check`** (`file:../../sygnal-check`, as they link `sygnal`), so `npm run dev` in any of them serves the app graph; `examples/mcp-app` moves off `sygnal-check` ^0.7.0.
262
+
5
263
  ## 6.6.0 — 2026-10-16
6
264
 
7
265
  The app graph as an interactive page: `sygnal-check --graph=html`, live with `--watch` or at `/__sygnal/graph` in the Vite dev server, in a VS Code panel and in Sygnal DevTools. See [The App Graph](https://sygnal.js.org/guide/app-graph/).
package/README.md CHANGED
@@ -9,6 +9,8 @@ A reactive component framework with pure functions, zero side effects, and autom
9
9
 
10
10
  ---
11
11
 
12
+ > **Upgrading from 6.x?** Sygnal 7.0 changes reducer timing: output sinks (EFFECT, EVENTS, PARENT, HTTP, ...) now see the state after the action's STATE update. See [Migrating to 7.0](https://sygnal.js.org/guide/migrating-to-7/).
13
+
12
14
  ## Why Sygnal?
13
15
 
14
16
  - **Pure components** — Views are plain functions. All side effects are handled by drivers, outside your code.
@@ -867,17 +867,25 @@ function graphMiddleware(server, root, engine, ping = 25000) {
867
867
  res.end(req.method === 'HEAD' ? undefined : body);
868
868
  };
869
869
  }
870
- /** Adds the page's URL under Vite's own when the dev server prints them. */
871
- function printGraphUrl(server) {
870
+ /** The banner line when the project has no sygnal-check with the graph page (it used to print nothing). */
871
+ const GRAPH_MISSING_LINE = ' ➜ Sygnal graph: needs sygnal-check 0.10.0 or later in this project (npm i -D sygnal-check)';
872
+ /**
873
+ * Adds the page's URL under Vite's own when the dev server prints them, or, when the project's
874
+ * sygnal-check is missing or too old for the page (`available` false), a line saying what to install.
875
+ */
876
+ function printGraphUrl(server, available = true) {
872
877
  const print = server?.printUrls;
873
878
  if (typeof print !== 'function')
874
879
  return;
875
880
  server.printUrls = function (...args) {
876
881
  const out = print.apply(this, args);
877
882
  try {
883
+ const logger = server.config?.logger || console;
878
884
  const local = server.resolvedUrls?.local?.[0];
879
- if (local)
880
- (server.config?.logger || console).info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`);
885
+ if (!available)
886
+ logger.info(GRAPH_MISSING_LINE);
887
+ else if (local)
888
+ logger.info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`);
881
889
  }
882
890
  catch (_) { }
883
891
  return out;
@@ -1016,7 +1024,8 @@ function printGraphUrl(server) {
1016
1024
  * skipped silently when missing; a sygnal-check without the page keeps the checker as in 5)
1017
1025
  * builds it with createLiveProject on start and after each change: one build gives the page,
1018
1026
  * the check overlay / console findings of 5, and the component stamps. Its URL is printed
1019
- * under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"). The modules of the
1027
+ * under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"; without a sygnal-check
1028
+ * 0.10.0+ in the project, a line saying so instead). The modules of the
1020
1029
  * scanned files get a tail setting a non-enumerable `__sygnalKey` (the static 'file#name'
1021
1030
  * key, D521) on each top-level component function, which the runtime inspect() reports as
1022
1031
  * `key`; with DevTools on, the dev entries also import 'virtual:sygnal/graph'
@@ -1084,7 +1093,7 @@ function sygnal(options = {}) {
1084
1093
  // Files whose `import ... from 'sygnal'` resolves to the run() wrapper
1085
1094
  const runtimeImporters = new Set();
1086
1095
  // Statements run before the app starts: dev flag (+ strict flag)
1087
- const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '');
1096
+ const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '') + (diagnostics.timingAudit ? AUDIT_FLAG : '');
1088
1097
  const devImports = `import 'sygnal/diagnostics';import '${DEV_CLIENT}';`;
1089
1098
  // D77: the DevTools bridge, dev only. First, so the diagnostics entry finds it.
1090
1099
  // E-1: the MCP endpoint needs the bridge, so `mcp` turns it on
@@ -1282,8 +1291,7 @@ function sygnal(options = {}) {
1282
1291
  // W-2 (D543): the page and its stream, behind the MCP endpoint's guard; its URL in the banner
1283
1292
  if (graphOn && server?.middlewares) {
1284
1293
  server.middlewares.use(graphMiddleware(server, root, engine));
1285
- if (graphAvailable(root))
1286
- printGraphUrl(server);
1294
+ printGraphUrl(server, graphAvailable(root));
1287
1295
  }
1288
1296
  },
1289
1297
  transform(code, id, opts) {
@@ -1568,7 +1576,7 @@ function normalizeDiagnostics(value) {
1568
1576
  if (!MODES.includes(mode)) {
1569
1577
  throw new Error(`[vite-plugin-sygnal] diagnostics mode must be one of ${MODES.map(m => `'${m}'`).join(', ')}; got ${JSON.stringify(mode)}`);
1570
1578
  }
1571
- return { mode, strict: !!o.strict, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] };
1579
+ return { mode, strict: !!o.strict, timingAudit: !!o.timingAudit, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] };
1572
1580
  }
1573
1581
  /** Module id without a query or hash. */
1574
1582
  function cleanId(id) {
@@ -1946,6 +1954,8 @@ function startChecker(server, root, mod, setup, opts, engine) {
1946
1954
  }
1947
1955
  const DEV_FLAG = 'if (globalThis.__SYGNAL_DEV__ === undefined) globalThis.__SYGNAL_DEV__ = true;';
1948
1956
  const STRICT_FLAG = 'if (globalThis.__SYGNAL_STRICT__ === undefined) globalThis.__SYGNAL_STRICT__ = true;';
1957
+ // 7.0: the opt-in timing audit (SYG257, 'sygnal/diagnostics' checks/timingAudit.ts)
1958
+ const AUDIT_FLAG = 'if (globalThis.__SYGNAL_TIMING_AUDIT__ === undefined) globalThis.__SYGNAL_TIMING_AUDIT__ = true;';
1949
1959
  // A top-level (column 0) run(Component, ...) statement: bare, or declaring a
1950
1960
  // new binding. Assignments to existing variables, nested calls and indented
1951
1961
  // calls (e.g. inside a function or test callback) do not match (B-007). It is
@@ -2084,10 +2094,12 @@ const rootDir = (root) =>
2084
2094
  // @ts-ignore — Node's process
2085
2095
  !root ? process.cwd() : typeof root === 'string' ? root : node_url.fileURLToPath(root);
2086
2096
  function sygnalAstroIntegration(options = {}) {
2097
+ let projectRoot;
2087
2098
  return {
2088
2099
  name: SYGNAL_RENDERER_NAME,
2089
2100
  hooks: {
2090
2101
  'astro:config:setup': ({ addRenderer, updateConfig, command, config }) => {
2102
+ projectRoot = rootDir(config?.root);
2091
2103
  addRenderer({
2092
2104
  name: SYGNAL_RENDERER_NAME,
2093
2105
  clientEntrypoint: 'sygnal/astro/client',
@@ -2117,6 +2129,7 @@ function sygnalAstroIntegration(options = {}) {
2117
2129
  check: options.check,
2118
2130
  nativeGlobalThis: options.nativeGlobalThis,
2119
2131
  devtools: options.devtools,
2132
+ graph: options.graph,
2120
2133
  })];
2121
2134
  }
2122
2135
  else if (options.nativeGlobalThis !== false) {
@@ -2129,6 +2142,16 @@ function sygnalAstroIntegration(options = {}) {
2129
2142
  vite.plugins = [...(vite.plugins || []), onErrorPlugin(hook)];
2130
2143
  updateConfig({ vite });
2131
2144
  },
2145
+ // W-2: Astro prints its own banner (Vite's printUrls never runs), so the graph page's line goes here
2146
+ 'astro:server:start': ({ address, logger }) => {
2147
+ if (options.graph === false || !projectRoot)
2148
+ return;
2149
+ const print = (msg) => (logger ? logger.info(msg.replace(/^\s*➜\s*/, '')) : console.log(msg));
2150
+ if (!graphAvailable(projectRoot))
2151
+ print(GRAPH_MISSING_LINE);
2152
+ else if (address?.port)
2153
+ print(` ➜ Sygnal graph: http://localhost:${address.port}${GRAPH_PATH}`);
2154
+ },
2132
2155
  },
2133
2156
  };
2134
2157
  }