sygnal 6.5.0 → 7.0.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 (104) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +2 -0
  3. package/dist/astro/index.cjs.js +423 -27
  4. package/dist/astro/index.cjs.js.map +1 -1
  5. package/dist/astro/index.mjs +423 -27
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/server.cjs.js.map +1 -1
  8. package/dist/astro/server.mjs.map +1 -1
  9. package/dist/devtools.cjs.js +141 -3
  10. package/dist/devtools.cjs.js.map +1 -1
  11. package/dist/devtools.esm.js +141 -4
  12. package/dist/devtools.esm.js.map +1 -1
  13. package/dist/diagnostics.cjs.js +169 -14
  14. package/dist/diagnostics.cjs.js.map +1 -1
  15. package/dist/diagnostics.esm.js +168 -15
  16. package/dist/diagnostics.esm.js.map +1 -1
  17. package/dist/guide/accessibility.md +2 -0
  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/ui/tabs.md +5 -4
  28. package/dist/guide/ui/tooltip.md +24 -0
  29. package/dist/index.cjs.js +781 -320
  30. package/dist/index.cjs.js.map +1 -1
  31. package/dist/index.d.ts +319 -43
  32. package/dist/index.esm.js +781 -320
  33. package/dist/index.esm.js.map +1 -1
  34. package/dist/sygnal.min.js +1 -1
  35. package/dist/sygnal.min.js.map +1 -1
  36. package/dist/ui.cjs.js +96 -11
  37. package/dist/ui.cjs.js.map +1 -1
  38. package/dist/ui.esm.js +96 -12
  39. package/dist/ui.esm.js.map +1 -1
  40. package/dist/vike/config/package.json +1 -1
  41. package/dist/vike/onRenderClient.cjs.js +63 -43
  42. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  43. package/dist/vike/onRenderClient.mjs +63 -43
  44. package/dist/vike/onRenderClient.mjs.map +1 -1
  45. package/dist/vike/onRenderHtml.cjs.js +21 -25
  46. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  47. package/dist/vike/onRenderHtml.mjs +21 -25
  48. package/dist/vike/onRenderHtml.mjs.map +1 -1
  49. package/dist/vite/plugin.cjs.js +410 -27
  50. package/dist/vite/plugin.cjs.js.map +1 -1
  51. package/dist/vite/plugin.mjs +410 -27
  52. package/dist/vite/plugin.mjs.map +1 -1
  53. package/llms.txt +17 -16
  54. package/package.json +1 -1
  55. package/src/astro/index.d.ts +8 -0
  56. package/src/astro/index.ts +18 -0
  57. package/src/core/actions.ts +28 -21
  58. package/src/core/define.ts +2 -0
  59. package/src/core/hooks.ts +8 -2
  60. package/src/core/hosts/collection.ts +4 -3
  61. package/src/core/instance.ts +60 -16
  62. package/src/core/runtime.ts +5 -1
  63. package/src/core/view.ts +2 -0
  64. package/src/cycle/dom/utils.ts +4 -2
  65. package/src/devtools.d.ts +13 -0
  66. package/src/devtools.ts +3 -0
  67. package/src/extra/ai/chat/behavior.ts +3 -3
  68. package/src/extra/ai/commandBar.ts +3 -3
  69. package/src/extra/ai/compose/behavior.ts +3 -3
  70. package/src/extra/ai/transports/shared.ts +8 -2
  71. package/src/extra/behaviors.ts +21 -0
  72. package/src/extra/devGraph.ts +111 -0
  73. package/src/extra/devMcp.ts +1 -1
  74. package/src/extra/devtools.ts +18 -0
  75. package/src/extra/diagnostics/checks/fetch.ts +21 -1
  76. package/src/extra/diagnostics/checks/forms.ts +4 -3
  77. package/src/extra/diagnostics/checks/index.ts +2 -1
  78. package/src/extra/diagnostics/checks/inspect.ts +68 -6
  79. package/src/extra/diagnostics/checks/next.ts +3 -0
  80. package/src/extra/diagnostics/checks/public.d.ts +133 -12
  81. package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
  82. package/src/extra/diagnostics/codes.ts +19 -0
  83. package/src/extra/fetchDriver.ts +112 -7
  84. package/src/extra/fetchEventSource.ts +113 -0
  85. package/src/extra/form.ts +68 -26
  86. package/src/extra/formHelpers.ts +3 -1
  87. package/src/extra/lens.ts +3 -2
  88. package/src/extra/queryCache.ts +4 -3
  89. package/src/extra/socketDriver.ts +10 -4
  90. package/src/extra/testing.ts +68 -11
  91. package/src/index.d.ts +199 -30
  92. package/src/index.ts +1 -0
  93. package/src/shared.ts +3 -0
  94. package/src/ui/dialog.ts +3 -2
  95. package/src/ui/tooltips.ts +92 -0
  96. package/src/ui.d.ts +35 -0
  97. package/src/ui.ts +1 -0
  98. package/src/vike/onRenderClient.ts +44 -46
  99. package/src/vike/onRenderHtml.ts +5 -26
  100. package/src/vike/shell.ts +33 -0
  101. package/src/vite/graph.ts +255 -0
  102. package/src/vite/mcp.ts +3 -3
  103. package/src/vite/plugin.d.ts +19 -0
  104. package/src/vite/plugin.ts +144 -25
@@ -17,6 +17,15 @@
17
17
  * onRender (PLAN-4 CT-1) the controls the instance renders: its own vtree, up to the
18
18
  * isolated child components, through ./controls' vnode -> control map
19
19
  * onDispose prunes the instance
20
+ * (PLAN-10 D522, read from the instance view at inspect() time, no core change) viewOnly (the
21
+ * component function has no statics), entry (an app's root), app (which run() the
22
+ * instance belongs to), shown: false (a hidden Switchable page and below), the
23
+ * instance ids on children entries and in events[type].emitterIds / selectorIds.
24
+ * D530: contextConsumes from InstanceView.reads (the keys the latest render read);
25
+ * diagnostics keep their `timestamp`.
26
+ * `key` (PLAN-10 P-2, D544): the static 'file#name' (D521) that sygnal/vite stamps on
27
+ * the component function in dev (a non-enumerable `__sygnalKey`), when it is there;
28
+ * the runtime itself has no source file
20
29
  * (PLAN-4 2-C) the action log (./actionLog: actionHooks, a layer of checks/next.ts) feeds
21
30
  * inspect({ actions }): the recent actions (the last RECENT_MAX, any instance)
22
31
  * Selectors come from the DOM check (real DOM) or from renderComponent (mock
@@ -205,6 +214,8 @@ const slim = (d: any): InspectDiagnostic => {
205
214
  if (d.component) out.component = d.component
206
215
  if (d.fix) out.fix = d.fix
207
216
  if (d.docsUrl) out.docsUrl = d.docsUrl
217
+ // PLAN-10 P-1: when it was reported (runtime diagnostics are reported once per process)
218
+ if (typeof d.timestamp === 'number') out.timestamp = d.timestamp
208
219
  if (d.data !== undefined) {
209
220
  // keep it JSON-safe (data can hold errors, vnodes, ...)
210
221
  try { out.data = JSON.parse(JSON.stringify(d.data)) } catch (_) { /* drop */ }
@@ -316,12 +327,55 @@ function childrenOf(r: Rec, all: Rec[]): InspectChild[] {
316
327
  if (c.parentId !== r.id) continue
317
328
  const key = `${c.name}\u0000${via[c.kind]}`
318
329
  const entry = out.get(key)
319
- if (entry) entry.count = (entry.count || 1) + 1
320
- else out.set(key, {name: c.name, via: via[c.kind], count: 1})
330
+ if (entry) entry.count = entry.ids!.push(c.id)
331
+ else out.set(key, {name: c.name, via: via[c.kind], count: 1, ids: [c.id]})
321
332
  }
322
333
  return [...out.values()]
323
334
  }
324
335
 
336
+ /** the statics sygnal-check reads (its STATIC_PROPS): a component function with none is view-only */
337
+ const STATICS = ['intent', 'model', 'initialState', 'isolatedState', 'context', 'calculated', 'connections', 'resources', 'route', 'head', 'uses', 'timers', 'persist', 'viewTransitions', 'browser', 'agent']
338
+
339
+ /** PLAN-10 D522: no statics and nothing the core added (renderComponent's test intent, a behavior's model) */
340
+ function isViewOnly(c: any): boolean {
341
+ const def = c && c.__next && c.__next.def
342
+ if (!def || def.intent || (def.handlers && def.handlers.size) || def.initialState !== undefined) return false
343
+ const fn = def.view
344
+ return !!fn && STATICS.every(k => fn[k] === undefined)
345
+ }
346
+
347
+ /** PLAN-10 P-2 (D544): the static key sygnal/vite's dev transform stamped on the component function */
348
+ function stampedKey(c: any): string | undefined {
349
+ const fn = c && c.__next && c.__next.def && c.__next.def.view
350
+ const k = fn && fn.__sygnalKey
351
+ return typeof k === 'string' ? k : undefined
352
+ }
353
+
354
+ /** PLAN-10 D530: the context keys the instance's latest render read (InstanceView.reads); every key when it read them all */
355
+ function contextReadsOf(c: any): string[] {
356
+ const v = c && c.__next
357
+ const reads: ReadonlySet<string> | null | undefined = v && v.reads
358
+ if (!reads) return []
359
+ const out = [...reads].filter(k => k !== '\u0000all')
360
+ if (reads.has('\u0000all')) { try { for (const k of Object.keys(v.context || {})) if (!out.includes(k)) out.push(k) } catch (_) { /* no context */ } }
361
+ return out
362
+ }
363
+
364
+ /** PLAN-10 D522: the app (run()) of each instance: the index of its root among the live roots, oldest first */
365
+ function appsOf(all: Rec[]): Map<Rec, number> {
366
+ const byId = new Map(all.map(r => [r.id, r]))
367
+ const roots = all.filter(r => r.parentId === null).sort((a, b) => Number(a.id) - Number(b.id))
368
+ const index = new Map(roots.map((r, i) => [r, i]))
369
+ const out = new Map<Rec, number>()
370
+ for (const r of all) {
371
+ let top: Rec | undefined = r
372
+ for (let n = 0; top && top.parentId !== null && n < 10000; n++) top = byId.get(top.parentId)
373
+ const i = top && index.get(top)
374
+ if (i !== undefined) out.set(r, i)
375
+ }
376
+ return out
377
+ }
378
+
325
379
  /** The app graph of the live component instances (see ./public.d.ts). */
326
380
  export function inspect(options: InspectOptions = {}): InspectGraph {
327
381
  // report SYG105 (info) for EVENTS types selected but never emitted so far
@@ -340,22 +394,29 @@ export function inspect(options: InspectOptions = {}): InspectGraph {
340
394
  const ids = options.ids && new Set(options.ids.map(String))
341
395
  const recs = ids ? all.filter(r => ids.has(r.id)) : all
342
396
  const names = new Set(recs.map(r => r.name))
397
+ const apps = appsOf(all)
343
398
 
344
399
  const components: InspectComponent[] = recs.map(r => {
345
400
  const c = r.instance
346
401
  const calculated = c && c.calculated && typeof c.calculated === 'object' ? Object.keys(c.calculated) : []
347
402
  const selectors = selectorsOf(r, options, diags)
348
403
  const controls = controlsOf(r, selectors)
404
+ const key = stampedKey(c)
349
405
  return {
350
406
  name: r.name,
351
407
  id: r.id,
408
+ ...(key ? {key} : {}),
352
409
  parentId: r.parentId,
353
410
  kind: r.kind,
411
+ ...(isViewOnly(c) ? {viewOnly: true as const} : {}),
412
+ ...(r.parentId === null ? {entry: true as const} : {}),
413
+ ...(apps.has(r) ? {app: apps.get(r)} : {}),
414
+ ...(c && c.__next && c.__next.shown === false ? {shown: false as const} : {}),
354
415
  actions: actionsOf(r),
355
416
  stateKeys: stateKeysOf(c, calculated),
356
417
  calculated,
357
418
  contextProvides: c && isPlainObject(c.context) ? Object.keys(c.context) : [],
358
- contextConsumes: null,
419
+ contextConsumes: contextReadsOf(c),
359
420
  eventsEmitted: [...r.eventsEmitted],
360
421
  eventsSelected: [...r.eventsSelected],
361
422
  children: childrenOf(r, all),
@@ -368,11 +429,12 @@ export function inspect(options: InspectOptions = {}): InspectGraph {
368
429
 
369
430
  const cache = cacheOf(recs) || {}
370
431
  const events: InspectGraph['events'] = {}
371
- const ev = (type: string) => events[type] || (events[type] = {emitters: [], selectors: []})
432
+ // names once each (as before); PLAN-10 D522: every instance's id in emitterIds / selectorIds
433
+ const ev = (type: string) => events[type] || (events[type] = {emitters: [], selectors: [], emitterIds: [], selectorIds: []})
372
434
  const add = (list: string[], name: string) => { if (!list.includes(name)) list.push(name) }
373
435
  for (const c of components) {
374
- for (const t of c.eventsEmitted) add(ev(t).emitters, c.name)
375
- for (const t of c.eventsSelected) add(ev(t).selectors, c.name)
436
+ for (const t of c.eventsEmitted) { const e = ev(t); add(e.emitters, c.name); e.emitterIds!.push(c.id) }
437
+ for (const t of c.eventsSelected) { const e = ev(t); add(e.selectors, c.name); e.selectorIds!.push(c.id) }
376
438
  }
377
439
  // emitted on the bus by something that isn't a listed component (e.g. a driver, a pruned component)
378
440
  if (!ids) for (const t of busTypes) ev(t)
@@ -28,6 +28,7 @@ import {watchMutation} from './state'
28
28
  import {checkRequest} from './replies'
29
29
  import {check as checkFetch} from './fetch'
30
30
  import {checkSentCommand} from './elementCommands'
31
+ import {audited} from './timingAudit'
31
32
  import {checkCollection} from './collections'
32
33
  import {eventEmitted, actionListener as recentActions} from './inspect'
33
34
  import {actionHooks} from './actionLog'
@@ -444,6 +445,8 @@ export function nextHooks(_api: any): any {
444
445
  onAction(iv: any, a: any) { log.onAction(iv, a) },
445
446
  wrapHandler(iv: any, type: string, sink: string, fn: any) {
446
447
  let h = log.wrapHandler(iv, type, sink, fn) || fn
448
+ // 7.0 timing audit (SYG257, opt-in; checks/timingAudit.ts): checked per call, so turning it on later counts
449
+ if (on() && sink != 'STATE' && typeof h == 'function') h = audited(fac(iv), type, sink, h)
447
450
  // SYG222: the STATE reducers, wrapped (today the check rewrote the instance's model)
448
451
  if (on() && sink == 'STATE' && typeof fn == 'function') h = watchMutation(fac(iv), type, h)
449
452
  return h === fn ? undefined : h
@@ -40,6 +40,18 @@ export interface DiagnosticCheck {
40
40
  */
41
41
  export function configureStrict(on?: boolean): void
42
42
 
43
+ /**
44
+ * 7.0 timing audit (SYG257, info), for migrating 6.x code: report each output sink that reads a
45
+ * top-level state field its action's STATE changed (since 7.0 it gets the new value; 6.x gave it
46
+ * the old one; read props.previousState for the old one). true / false, or undefined for the
47
+ * default (globalThis.__SYGNAL_TIMING_AUDIT__ === true). Also renderComponent(C, { timingAudit })
48
+ * and sygnal({ diagnostics: { timingAudit: true } }). Diagnostics must be on.
49
+ */
50
+ export function configureTimingAudit(on?: boolean): void
51
+
52
+ /** Whether the timing audit is on. */
53
+ export function isTimingAuditEnabled(): boolean
54
+
43
55
  /** Whether the runtime strict checks are on. */
44
56
  export function isStrictEnabled(): boolean
45
57
 
@@ -96,9 +108,23 @@ export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined
96
108
 
97
109
  /** How an action is dispatched. */
98
110
  /** 'agent' (static, PLAN-6 K-1): only the component's `agent` declaration dispatches it */
99
- export type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'agent' | 'unknown'
111
+ /**
112
+ * 'behavior' (static, PLAN-10 A-8): a `uses` entry's action nothing else triggers that the behavior
113
+ * may dispatch itself where the checker can't list it (a package behavior, an open model, ...)
114
+ */
115
+ export type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'agent' | 'behavior' | 'unknown'
100
116
 
101
- export interface InspectAction {
117
+ /**
118
+ * PLAN-10 A-4 (static only): a source position, 1-based. `file` (relative to the working
119
+ * directory) only when it isn't the component's file.
120
+ */
121
+ export interface InspectLocation {
122
+ line: number
123
+ column: number
124
+ file?: string
125
+ }
126
+
127
+ export interface InspectAction extends Partial<InspectLocation> {
102
128
  name: string
103
129
  /**
104
130
  * 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
@@ -112,18 +138,56 @@ export interface InspectAction {
112
138
  trigger: InspectActionTrigger
113
139
  /** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
114
140
  sinks: string[]
141
+ /** the `uses` key of the behavior that owns it ('pager' for 'pager.NEXT') */
142
+ behavior?: string
143
+ /** static, PLAN-10 A-4: the model entry (line/column on the action: the intent property, else this) */
144
+ modelAt?: InspectLocation
145
+ /**
146
+ * static, PLAN-10 A-7 (actions with a STATE sink only): the top-level state keys the STATE
147
+ * reducer(s) may write (a behavior's: its slice); [] when it never changes state; null when unknown
148
+ */
149
+ writes?: string[] | null
150
+ /** static, PLAN-10 A-7: a STATE reducer may return undefined (removes a Collection item) */
151
+ removes?: true
115
152
  }
116
153
 
117
- export interface InspectChild {
154
+ export interface InspectChild extends Partial<InspectLocation> {
118
155
  name: string
119
- via: 'tag' | 'collection' | 'switchable' | 'slot'
156
+ /**
157
+ * 'layout' (static, PLAN-10 A-9): Vike's shell, a +Wrapper / +Layout rendering the next +Layout in
158
+ * or a +Page it applies to (Wrapper(s) > Layout(s) > Page); no location (no JSX site)
159
+ */
160
+ via: 'tag' | 'collection' | 'switchable' | 'slot' | 'layout'
120
161
  /** Collection: the `from` state field, when known */
121
162
  from?: string | null
122
163
  /** runtime: number of live instances behind this entry (e.g. Collection items) */
123
164
  count?: number
165
+ /** runtime, PLAN-10 D522: the ids of those instances */
166
+ ids?: string[]
167
+ /**
168
+ * static, PLAN-10 A-3: the id of the component it renders (resolved through the import); null when
169
+ * it isn't in the graph (a package, a widget, a file not scanned, a dynamic value)
170
+ */
171
+ id?: string | null
172
+ /** static, PLAN-10 A-1: a literal `state` lens (`<Greeting state="host" />`) */
173
+ state?: string
174
+ /** static, PLAN-10 A-2: the package the tag is imported from ('sygnal/ui') */
175
+ external?: string
176
+ /** static, PLAN-10 A-2: a widget tag (defineWidget, fromZag, fromReact, the sygnal/ui Zag parts) */
177
+ widget?: true
178
+ }
179
+
180
+ /** PLAN-10 A-5 (static): a CHILD.select(Child) call in an intent */
181
+ export interface InspectChildSelect extends InspectLocation {
182
+ /** the selected component's name as written */
183
+ child: string | null
184
+ /** its id; null when it isn't in the graph */
185
+ id: string | null
186
+ /** the intent actions whose stream contains the call (the receivers of the child's PARENT sink) */
187
+ actions: string[]
124
188
  }
125
189
 
126
- export interface InspectSelector {
190
+ export interface InspectSelector extends Partial<InspectLocation> {
127
191
  /** CSS selector passed to DOM.select() (chained selects joined with a space) */
128
192
  selector: string
129
193
  /** event types listened to; null when unknown (runtime, real DOM) */
@@ -139,10 +203,12 @@ export interface InspectSelector {
139
203
  isolationHit: string | null
140
204
  /** PLAN-4 CT-1: the control this selector is (its key), when it is `[data-control="<Key>"]` */
141
205
  control?: string
206
+ /** the `uses` key of the behavior that listens to it */
207
+ behavior?: string
142
208
  }
143
209
 
144
210
  /** PLAN-4 CT-1: a control (controls()) a component renders */
145
- export interface InspectControl {
211
+ export interface InspectControl extends Partial<InspectLocation> {
146
212
  /** the control's key (rendered as data-control="<name>") */
147
213
  name: string
148
214
  /** the intrinsic tag of a tag spec; null for a spec object */
@@ -157,10 +223,14 @@ export interface InspectDiagnostic {
157
223
  code: string
158
224
  severity: DiagnosticSeverity
159
225
  component?: string
226
+ /** static, PLAN-10 A-3: the id of the component it is attached to */
227
+ componentId?: string
160
228
  message: string
161
229
  fix?: string
162
230
  docsUrl?: string
163
231
  data?: any
232
+ /** runtime only: when it was reported (ms since the epoch; each is reported once per process) */
233
+ timestamp?: number
164
234
  /** static only */
165
235
  file?: string
166
236
  line?: number
@@ -215,6 +285,12 @@ export interface InspectComponent {
215
285
  name: string
216
286
  /** runtime: instance number (as a string); static: 'file:line' of the definition */
217
287
  id: string
288
+ /**
289
+ * static, PLAN-10 D521: 'file#name', stable when lines move (hosts key selection and positions by it);
290
+ * a second component of the same name in one file gets 'file#name#2', and so on. Runtime (PLAN-10 P-2,
291
+ * D544): the same key when sygnal/vite's dev server stamped the component function, else absent
292
+ */
293
+ key?: string
218
294
  /** runtime: the parent instance's id; static: null (see the parents' `children`) */
219
295
  parentId: string | null
220
296
  /** static only: source file, relative to the working directory */
@@ -224,6 +300,32 @@ export interface InspectComponent {
224
300
  * component renders it, else how it is first rendered
225
301
  */
226
302
  kind: 'root' | 'child' | 'collection-item' | 'switchable'
303
+ /** PLAN-10 A-1 / D522: rendered as a component (or started as an entry) but has no statics */
304
+ viewOnly?: true
305
+ /**
306
+ * static, PLAN-10 A-6: started by run(), defineElement() or a Vike +Page / +Layout / +Wrapper; 'unknown' on the
307
+ * other roots when an entry call's component can't be resolved (picked at run time).
308
+ * runtime (D522): true on an app's root instance
309
+ */
310
+ entry?: true | 'unknown'
311
+ /**
312
+ * static, Vike only (PLAN-10 A-9): on a +Page's component its route ('/orders/@id': from the
313
+ * filesystem, or the literal string a +route file / the config's `route` sets); null for a Route
314
+ * Function or a route the checker can't read (with `routeDynamic`) and for _error/+Page. On a
315
+ * +Layout's / +Wrapper's: the URL prefix of the directory it applies to
316
+ */
317
+ route?: string | null
318
+ /**
319
+ * static, Vike only (PLAN-10 A-9): every route of a component several +Page files export (`route` is
320
+ * the first, in path order; null for a dynamic one or the error page); omitted otherwise
321
+ */
322
+ routes?: (string | null)[]
323
+ /** static, Vike only (PLAN-10 A-9): the route is a Route Function or can't be read (route: null) */
324
+ routeDynamic?: true
325
+ /** runtime, PLAN-10 D522: the app (run()) it belongs to: its root's index among the live roots, oldest first */
326
+ app?: number
327
+ /** runtime, PLAN-10 D522: false on a hidden Switchable page and below it; omitted when shown */
328
+ shown?: false
227
329
  actions: InspectAction[]
228
330
  /** state keys (runtime: current state; static: initialState) */
229
331
  stateKeys: string[]
@@ -231,14 +333,27 @@ export interface InspectComponent {
231
333
  calculated: string[]
232
334
  /** context fields this component provides (Component.context) */
233
335
  contextProvides: string[]
234
- /** context fields its view reads (static only; null at runtime) */
336
+ /** context fields its view reads (static: the view source; runtime, D530: what its latest render read) */
235
337
  contextConsumes?: string[] | null
338
+ /**
339
+ * static, PLAN-10 A-7: top-level state keys the view (and its child lenses, calculated and
340
+ * context functions) reads literally; omitted when none
341
+ */
342
+ stateReads?: string[]
343
+ /** static, PLAN-10 A-7: stateReads may be incomplete (the state used whole somewhere) */
344
+ stateReadsPartial?: true
236
345
  /** EVENTS types this component emits */
237
346
  eventsEmitted: string[]
238
347
  /** EVENTS types this component selects ('*' = EVENTS.select() without a type) */
239
348
  eventsSelected: string[]
240
349
  children: InspectChild[]
241
350
  selectors: InspectSelector[]
351
+ /** static, PLAN-10 A-4: where the `context` static's entries are */
352
+ contextEntries?: Array<InspectLocation & { name: string }>
353
+ /** static, PLAN-10 A-4: the EVENTS emit and select sites of its intent and model */
354
+ eventSites?: Array<InspectLocation & { type: string; role: 'emit' | 'select' }>
355
+ /** static, PLAN-10 A-5: CHILD.select(Child) in its intent */
356
+ childSelects?: InspectChildSelect[]
242
357
  /** PLAN-4 CT-1: the controls it renders (runtime: seen in its renders); omitted when none */
243
358
  controls?: InspectControl[]
244
359
  /** static only, PLAN-4 GS-2 (G-224): the element commands (ELEMENT sink) its model sends; omitted when none */
@@ -253,7 +368,7 @@ export interface InspectComponent {
253
368
  }
254
369
 
255
370
  /** PLAN-4 GS-2: an element command a model entry sends (static inspect, sygnal-check --graph) */
256
- export interface InspectCommand {
371
+ export interface InspectCommand extends Partial<InspectLocation> {
257
372
  /** the model entry that sends it */
258
373
  action: string
259
374
  /** the command's method (its first key) */
@@ -275,13 +390,15 @@ export interface InspectAgent {
275
390
  description: string | null
276
391
  /** the declaration has a read projection */
277
392
  read: boolean
278
- actions: Array<{ name: string; consequential: boolean; input: boolean }>
393
+ actions: Array<{ name: string; consequential: boolean; input: boolean } & Partial<InspectLocation>>
394
+ /** the read-only `agent.queries` tools; omitted when none */
395
+ queries?: Array<{ name: string; input: boolean } & Partial<InspectLocation>>
279
396
  /** spreads: what is listed may be incomplete */
280
397
  partial?: true
281
398
  }
282
399
 
283
400
  /** PLAN-4 GS-7: a literal timer spec of a `timers` static (static inspect, sygnal-check --graph) */
284
- export interface InspectTimer {
401
+ export interface InspectTimer extends Partial<InspectLocation> {
285
402
  name: string
286
403
  every?: number | null
287
404
  after?: number | null
@@ -316,8 +433,12 @@ export interface InspectGraph {
316
433
  /** which implementation produced the graph */
317
434
  source?: 'runtime' | 'static'
318
435
  components: InspectComponent[]
319
- /** EVENTS bus: type -> names of the components that emit / select it */
320
- events: Record<string, { emitters: string[]; selectors: string[] }>
436
+ /**
437
+ * EVENTS bus: type -> names of the components that emit / select it (each name once); emitterIds /
438
+ * selectorIds (PLAN-10 A-3, D522, D528): the id of every such component (static) or instance
439
+ * (runtime), not parallel to the names
440
+ */
441
+ events: Record<string, { emitters: string[]; selectors: string[]; emitterIds?: string[]; selectorIds?: string[] }>
321
442
  /** diagnostics not tied to a listed component */
322
443
  diagnostics: InspectDiagnostic[]
323
444
  /** runtime, PLAN-3 5-3: makeFetchDriver cache entries, by sink name (empty when no cache) */
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The 7.0 timing audit (SYG257, info): since 7.0 a model entry's STATE runs first and every other
3
+ * sink gets the state it committed (6.x gave them the state from before the action). For code
4
+ * written for 6.x, this reports each output sink that reads a top-level state field its action's
5
+ * STATE changed: it gets the new value now. Fix: read `props.previousState.<field>` where the old
6
+ * value is meant; a read meant to see the new value needs nothing. Opt-in, because in 7.0-native
7
+ * code reading the new value is the intent. Turn it on with any of:
8
+ * - configureTimingAudit(true) (this entry)
9
+ * - renderComponent(C, { timingAudit: true }) (tests)
10
+ * - sygnal({ diagnostics: { timingAudit: true } }) (sygnal/vite, dev)
11
+ * - globalThis.__SYGNAL_TIMING_AUDIT__ = true
12
+ * The flag lives on the core bridge (shared by every copy of this entry and renderComponent).
13
+ * Mechanism: the dev hook layer's wrapHandler (checks/next.ts) wraps each non-STATE handler and
14
+ * gives it the state through a Proxy that notes reads of changed top-level fields. Top-level
15
+ * fields only (a nested change shows as its top-level field changing); once per component name,
16
+ * action, sink and field. Nothing in production builds.
17
+ */
18
+ import {bridge, devReport, once} from './shared'
19
+
20
+ /** Turn the timing audit on (true), off (false), or back to the default (globalThis.__SYGNAL_TIMING_AUDIT__). */
21
+ export function configureTimingAudit(on?: boolean): void {
22
+ const core = bridge()
23
+ if (core) core.timingAudit = on
24
+ }
25
+
26
+ /** Whether the timing audit is on. */
27
+ export function isTimingAuditEnabled(): boolean {
28
+ const flag = bridge()?.timingAudit
29
+ return flag === undefined ? (globalThis as any).__SYGNAL_TIMING_AUDIT__ === true : flag === true
30
+ }
31
+
32
+ /** handler `h` of `f`'s action `type`, sink `sink`, audited (f: the checks' instance facade) */
33
+ export const audited = (f: any, type: string, sink: string, h: any) => (s: any, d: any, nx: any, p: any, ...r: any[]) => {
34
+ const was = p?.previousState
35
+ if (!isTimingAuditEnabled() || !was || was === s || !s || typeof s != 'object' || typeof was != 'object' || Array.isArray(s)) return h(s, d, nx, p, ...r)
36
+ const view = new Proxy(s, {get(t: any, k: any, rcv: any) {
37
+ if (typeof k == 'string' && k in was && was[k] !== t[k] && once(`SYG257:${f.name}:${type}:${sink}:${k}`))
38
+ devReport('SYG257', {
39
+ component: f,
40
+ message: `${f.name}'s '${type}' ${sink} read state.${k}, which the same action's STATE changed: since 7.0 it gets the new value (6.x gave it the old one)`,
41
+ fix: `If it meant the value from before the action, read props.previousState.${k} (the 4th argument); if it meant the new value, nothing to change`,
42
+ data: {action: type, sink, field: k},
43
+ })
44
+ return Reflect.get(t, k, rcv)
45
+ }})
46
+ return h(view, d, nx, p, ...r)
47
+ }
@@ -152,6 +152,9 @@ export const CODE_TITLES: Record<string, string> = {
152
152
  SYG127: 'Behavior collision or unresolvable uses entry',
153
153
  SYG128: 'Duplicate control key',
154
154
  SYG129: "CHILD.select() of a component this one doesn't render",
155
+ // PLAN-10 X-1 (D512): graph-wide checks, static only (sygnal-check)
156
+ SYG170: 'Context field read with no provider',
157
+ SYG171: 'PARENT sink no parent reads',
155
158
  SYG112: 'Reply action has no model entry',
156
159
  SYG130: 'href() names no route or leaves out a param',
157
160
  SYG131: 'Route params the pattern does not use',
@@ -307,6 +310,8 @@ export const CODE_TITLES: Record<string, string> = {
307
310
  SYG634: 'latest: true with a computed key',
308
311
  // PLAN-3 5-5 (D88)
309
312
  SYG635: 'Caching asked for without a queryCache',
313
+ // PLAN-11 U-1: progress requests (dev entry, fetch.ts)
314
+ SYG636: "progress that can't be reported as asked",
310
315
  // PLAN-4 GS-2: element commands (reported by the dev entry, checks/elementCommands.ts)
311
316
  SYG640: 'Element command target not found',
312
317
  SYG641: 'Unknown element command',
@@ -366,6 +371,9 @@ export const CODE_TITLES: Record<string, string> = {
366
371
  // PLAN-9 X-2: static only (sygnal-check rules/syg253-catalog.js)
367
372
  SYG253: 'Misspelled catalog declaration key',
368
373
  SYG254: 'Catalog descriptions or options overlap',
374
+ SYG256: 'Output guard rejects the updated state',
375
+ SYG257: 'Output reads a field its action changed (7.0 timing audit)',
376
+ SYG454: 'State binding changed without a new component identity',
369
377
  // PLAN-9 H: chat client tools and ask (src/extra/ai/chat/behavior.ts, compose/ask.ts, compose/render.ts)
370
378
  SYG160: 'Result part rendered outside a card',
371
379
  SYG684: 'Client tool shadows an agent tool',
@@ -427,6 +435,9 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
427
435
  SYG128: 'error',
428
436
  // PLAN-4 4-G2: static only (sygnal-check); listed so the entry knows its title
429
437
  SYG129: 'warn',
438
+ // PLAN-10 X-1: static only (sygnal-check)
439
+ SYG170: 'warn',
440
+ SYG171: 'warn',
430
441
  SYG221: 'error',
431
442
  // PLAN-4 GS-4 (state.ts)
432
443
  SYG222: 'warn',
@@ -475,6 +486,8 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
475
486
  SYG634: 'info',
476
487
  // PLAN-3 5-5 (fetch.ts)
477
488
  SYG635: 'warn',
489
+ // PLAN-11 U-1 (fetch.ts)
490
+ SYG636: 'warn',
478
491
  // PLAN-4 GS-2 (checks/elementCommands.ts, through the core bridge; renderComponent too)
479
492
  SYG640: 'warn',
480
493
  SYG641: 'error',
@@ -591,6 +604,12 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
591
604
  // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
592
605
  SYG253: 'warn',
593
606
  SYG254: 'info',
607
+ // PLAN-12: static only; proven after-state output guard contradiction.
608
+ SYG256: 'warn',
609
+ // 7.0: the opt-in timing audit (checks/next.ts; configureTimingAudit)
610
+ SYG257: 'info',
611
+ // PLAN-12: static only for legacy (6.x) binding contracts. Sygnal 7 remounts known targets.
612
+ SYG454: 'warn',
594
613
  }
595
614
 
596
615
  export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined {