sygnal 6.2.0 → 6.3.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 (76) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +3 -1
  3. package/dist/ai.cjs.js +36 -0
  4. package/dist/ai.cjs.js.map +1 -1
  5. package/dist/ai.esm.js +1 -1
  6. package/dist/astro/server.cjs.js +3 -3
  7. package/dist/astro/server.cjs.js.map +1 -1
  8. package/dist/astro/server.mjs +3 -3
  9. package/dist/astro/server.mjs.map +1 -1
  10. package/dist/diagnostics.cjs.js +164 -3
  11. package/dist/diagnostics.cjs.js.map +1 -1
  12. package/dist/diagnostics.esm.js +164 -3
  13. package/dist/diagnostics.esm.js.map +1 -1
  14. package/dist/guide/agent.md +7 -1
  15. package/dist/guide/ai-cards.md +319 -0
  16. package/dist/guide/ai-chat.md +4 -0
  17. package/dist/guide/ai-compose.md +881 -0
  18. package/dist/guide/ai-decisions.md +16 -0
  19. package/dist/guide/http.md +1 -0
  20. package/dist/index.cjs.js +5695 -347
  21. package/dist/index.cjs.js.map +1 -1
  22. package/dist/index.d.ts +638 -8
  23. package/dist/index.esm.js +5685 -347
  24. package/dist/index.esm.js.map +1 -1
  25. package/dist/sygnal.min.js +1 -1
  26. package/dist/sygnal.min.js.map +1 -1
  27. package/dist/vike/config/package.json +1 -1
  28. package/dist/vike/onRenderClient.cjs.js +3 -2
  29. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  30. package/dist/vike/onRenderClient.mjs +3 -2
  31. package/dist/vike/onRenderClient.mjs.map +1 -1
  32. package/llms.txt +6 -4
  33. package/package.json +2 -1
  34. package/src/ai.d.ts +642 -7
  35. package/src/ai.ts +6 -0
  36. package/src/collection.ts +2 -1
  37. package/src/core/instance.ts +6 -3
  38. package/src/core/markers/lazy.ts +7 -3
  39. package/src/core/markers/portal.ts +5 -2
  40. package/src/core/markers/suspense.ts +3 -2
  41. package/src/core/markers/transition.ts +6 -3
  42. package/src/core/registry.ts +9 -5
  43. package/src/cycle/dom/IsolateModule.ts +25 -22
  44. package/src/extra/ai/chat/behavior.ts +354 -81
  45. package/src/extra/ai/chat/driver.ts +7 -4
  46. package/src/extra/ai/compose/agent.ts +55 -0
  47. package/src/extra/ai/compose/ask.ts +147 -0
  48. package/src/extra/ai/compose/behavior.ts +183 -0
  49. package/src/extra/ai/compose/catalog.ts +211 -0
  50. package/src/extra/ai/compose/composed.ts +61 -0
  51. package/src/extra/ai/compose/decisions.ts +1649 -0
  52. package/src/extra/ai/compose/diag.ts +33 -0
  53. package/src/extra/ai/compose/index.ts +23 -0
  54. package/src/extra/ai/compose/layout.ts +441 -0
  55. package/src/extra/ai/compose/llm.ts +1120 -0
  56. package/src/extra/ai/compose/render.ts +159 -0
  57. package/src/extra/ai/compose/testing.ts +71 -0
  58. package/src/extra/ai/compose/types.ts +159 -0
  59. package/src/extra/ai/decide.ts +97 -1
  60. package/src/extra/ai/index.ts +6 -1
  61. package/src/extra/diagnostics/checks/next.ts +86 -2
  62. package/src/extra/diagnostics/codes.ts +54 -0
  63. package/src/extra/fetchDriver.ts +58 -27
  64. package/src/extra/lens.ts +15 -0
  65. package/src/extra/ssr.ts +3 -3
  66. package/src/extra/testing.ts +2 -0
  67. package/src/extra/widget.ts +3 -3
  68. package/src/index.d.ts +41 -0
  69. package/src/index.ts +8 -0
  70. package/src/lazy.ts +3 -2
  71. package/src/portal.ts +5 -2
  72. package/src/pragma/index.ts +2 -1
  73. package/src/suspense.ts +5 -2
  74. package/src/switchable.ts +2 -1
  75. package/src/transition.ts +5 -2
  76. package/src/vike/onRenderClient.ts +3 -2
@@ -276,6 +276,10 @@ export const CODE_TITLES: Record<string, string> = {
276
276
  SYG423: 'Context change skipped a view whose output it changes',
277
277
  SYG424: 'Duplicate Collection item id',
278
278
  SYG425: 'isolatedState slice lacks initialState keys',
279
+ // PLAN-9 (D471): checks/next.ts; dev entry and sygnal-check
280
+ SYG426: 'Deprecated lowercase marker tag',
281
+ // PLAN-9 (D470): checks/next.ts; dev entry
282
+ SYG453: 'Two children with the same identity share one instance',
279
283
  SYG601: "Retired in 6.0: Missing or invalid sources",
280
284
  SYG602: 'Intent is not a function',
281
285
  SYG603: 'Intent returned an invalid value',
@@ -339,6 +343,29 @@ export const CODE_TITLES: Record<string, string> = {
339
343
  // PLAN-6 A-2: experimentalExposeWebMcp (src/extra/ai/webmcp.ts; dev)
340
344
  SYG674: 'WebMCP not available',
341
345
  SYG676: 'WebMCP registerTool rejected',
346
+ // ---- PLAN-9 compose (A: src/extra/ai/compose/; dev, reported with the severity passed) ----
347
+ SYG161: 'Catalog container render is a component with a model',
348
+ SYG162: 'Catalog part with neither component nor render',
349
+ SYG250: 'Layout names an unknown part or slot',
350
+ SYG251: 'Invalid param value in a layout',
351
+ SYG252: 'Record param from() does not return an array of { value }',
352
+ SYG450: 'Catalog in names an unknown container or slot',
353
+ SYG451: 'Composed has no catalog or from is not a composition',
354
+ SYG452: 'Layout lists a node key twice',
355
+ SYG681: 'Compose backend option problem',
356
+ // PLAN-9 B-1/B-2: compose.decisions() (src/extra/ai/compose/decisions.ts; explicit severities)
357
+ SYG680: 'Part is LLM-only under a decision backend',
358
+ SYG682: 'Decision model answered outside the criteria',
359
+ // PLAN-9 B-3: compose.llm() (src/extra/ai/compose/llm.ts; the compose status becomes 'error')
360
+ SYG683: 'LLM layout invalid after repair',
361
+ // PLAN-9 X-2: static only (sygnal-check rules/syg253-catalog.js)
362
+ SYG253: 'Misspelled catalog declaration key',
363
+ SYG254: 'Catalog descriptions or options overlap',
364
+ // PLAN-9 H: chat client tools and ask (src/extra/ai/chat/behavior.ts, compose/ask.ts, compose/render.ts)
365
+ SYG160: 'Result part rendered outside a card',
366
+ SYG684: 'Client tool shadows an agent tool',
367
+ SYG685: 'Answer for a tool call that is not open',
368
+ SYG686: 'Ask catalog without a result part',
342
369
  // PLAN-4 GS-3: a11y lane (static only, sygnal-check)
343
370
  SYG701: 'Click listener on a non-interactive element',
344
371
  SYG702: 'Form field without an accessible label',
@@ -457,6 +484,10 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
457
484
  SYG423: 'warn',
458
485
  SYG424: 'warn',
459
486
  SYG425: 'warn',
487
+ // PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
488
+ SYG426: 'warn',
489
+ // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
490
+ SYG453: 'warn',
460
491
  SYG612: 'error',
461
492
  // PLAN-4 D131 (element.ts, through the core bridge; a console warning in dev with diagnostics off)
462
493
  SYG644: 'warn',
@@ -497,6 +528,16 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
497
528
  // PLAN-6 A-2 (experimentalExposeWebMcp reports them with an explicit severity)
498
529
  SYG674: 'info',
499
530
  SYG676: 'warn',
531
+ // PLAN-9 B-1/B-2 (compose.decisions() reports them with an explicit severity)
532
+ SYG680: 'info',
533
+ SYG682: 'warn',
534
+ // PLAN-9 B-3 (compose.llm)
535
+ SYG683: 'error',
536
+ // PLAN-9 H (chat client tools and ask; reported with the severity passed)
537
+ SYG160: 'warn',
538
+ SYG684: 'warn',
539
+ SYG685: 'warn',
540
+ SYG686: 'warn',
500
541
  // PLAN-5 V-1: VirtualCollection (checks/virtual.ts, through the core bridge)
501
542
  SYG430: 'warn',
502
543
  SYG431: 'warn',
@@ -528,6 +569,19 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
528
569
  SYG153: 'warn',
529
570
  SYG730: 'warn',
530
571
  SYG731: 'warn',
572
+ // ---- PLAN-9 compose (A: src/extra/ai/compose/, reported with the severity passed, dev) ----
573
+ SYG161: 'error',
574
+ SYG162: 'error',
575
+ SYG250: 'warn',
576
+ SYG251: 'warn',
577
+ SYG252: 'warn',
578
+ SYG450: 'warn',
579
+ SYG451: 'warn',
580
+ SYG452: 'warn',
581
+ SYG681: 'error',
582
+ // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
583
+ SYG253: 'warn',
584
+ SYG254: 'info',
531
585
  }
532
586
 
533
587
  export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined {
@@ -81,6 +81,12 @@ import {validateWith} from './standardSchema';
81
81
  * the socket driver's backoff; the failure (with `attempts`) is delivered once, at the end.
82
82
  * - `validate: schema` (a Standard Schema): the parsed body is validated (and transformed); a
83
83
  * failure is an error with `issues`.
84
+ * - D472 (G-757): `batch: [request, …]` sends those requests in parallel instead of this one (each
85
+ * inherits its fields; its own `json` / `body` / `parse` win) and `combine(bodies, requests)`
86
+ * merges their parsed bodies into the one reply; `split(request, error)` turns a failed one (or
87
+ * the request itself) into requests sent again in parallel and combined, at most 3 deep. One
88
+ * failure fails the request once (`error.status` / `error.body`). No cache entry sharing, no
89
+ * per-part retry. The test fake (`_whole`) sends and answers it as one request. decide() uses it.
84
90
  * - dispose / sink completion aborts everything in flight.
85
91
  * - `fetch` is read at request time (options.fetch, else globalThis.fetch), so test stubs work.
86
92
  * - PLAN-3 5-1 (H-9): renderComponent's HTTP fake runs this driver over an in-memory fetch;
@@ -137,6 +143,15 @@ const autoParse = async (res: any) => {
137
143
  return text ? JSON.parse(text) : null;
138
144
  };
139
145
 
146
+ /** a non-2xx response */
147
+ const bad = (res: any) => res.ok === false || res.status < 200 || res.status > 299;
148
+ /** the failure of a non-2xx response: `status`, `body` (parsed with 'auto') */
149
+ const httpError = async (res: any, href: string) => {
150
+ let body: any;
151
+ try { body = await autoParse(res); } catch (_) {}
152
+ return Object.assign(new Error(`HTTP ${res.status}${res.statusText ? ' ' + res.statusText : ''}: ${href}`), {status: res.status, body});
153
+ };
154
+
140
155
  const PARSERS: Record<string, (res: any) => any> = {
141
156
  auto: autoParse,
142
157
  json: res => res.json(),
@@ -144,6 +159,8 @@ const PARSERS: Record<string, (res: any) => any> = {
144
159
  response: res => res,
145
160
  };
146
161
 
162
+ const parserOf = (pz: any) => typeof pz == 'function' ? pz : PARSERS[pz] || autoParse;
163
+
147
164
  const pickInit = (init: any) => {
148
165
  const out: any = {};
149
166
  if (init && typeof init == 'object') INIT_KEYS.forEach(k => { if (init[k] !== undefined) out[k] = init[k]; });
@@ -379,15 +396,23 @@ export function makeFetchDriver(options: any = {}) {
379
396
  if (abort) return cancel(r => r.scope === scope && (!('category' in req) || r.category === category));
380
397
  if (rn !== undefined || (!pf && (latest ?? options.latest))) cancel(rk === undefined ? r => r.rk === undefined && r.scope === scope && r.category === category : r => r.sender === sender && r.rk === rk);
381
398
 
382
- const href = withQuery((options.baseUrl || '') + (url ?? ''), query);
383
- const base = options.init, own = init;
384
- const h = mergeHeaders(options.headers, base?.headers, own?.headers, headers);
385
- let b = body ?? own?.body;
386
- if (json !== undefined) {
387
- b = JSON.stringify(json);
388
- if (!Object.keys(h).some(k => k.toLowerCase() == 'content-type')) h['content-type'] = 'application/json';
389
- }
390
- const m = method || own?.method || base?.method || (b !== undefined ? 'POST' : 'GET');
399
+ const base = options.init;
400
+ // a request's URL and fetch() init (the batch's sub-requests inherit the request's fields)
401
+ const prep = ({url, method, headers, query, json, body, init: own}: any) => {
402
+ const h = mergeHeaders(options.headers, base?.headers, own?.headers, headers);
403
+ let b = body ?? own?.body;
404
+ if (json !== undefined) {
405
+ b = JSON.stringify(json);
406
+ if (!Object.keys(h).some(k => k.toLowerCase() == 'content-type')) h['content-type'] = 'application/json';
407
+ }
408
+ return [withQuery((options.baseUrl || '') + (url ?? ''), query), {...pickInit(base), ...pickInit(own), method: method || own?.method || base?.method || (b !== undefined ? 'POST' : 'GET'), headers: h, body: b}];
409
+ };
410
+ const [href, fetchInit] = prep(req);
411
+ const m = fetchInit.method;
412
+ // D472: `batch` (sub-requests sent in parallel) and `split` (a failed request sent again in
413
+ // parts, at most 3 deep), their parsed bodies merged by `combine` into one reply. The test
414
+ // fake (_whole) answers such a request as one
415
+ const {batch, split, combine = (x: any) => x} = options._whole ? {} as any : req;
391
416
  const k = C?.key(req, rn !== undefined || pf);
392
417
  const id = ++seq;
393
418
  const r: any = {category, scope, sender, rk};
@@ -421,18 +446,10 @@ export function makeFetchDriver(options: any = {}) {
421
446
  const tries = pol.count ?? 3;
422
447
  F = {id, k, q: req, s: new Set(), n: 0};
423
448
  if (k !== undefined) C.f.set(k, F);
424
- const pz = parse || options.parse || 'auto';
425
- const parser = typeof pz == 'function' ? pz : PARSERS[pz] || autoParse;
449
+ const parser = parserOf(parse || options.parse);
426
450
  // the global fetch is called as a method: a detached window.fetch throws "Illegal invocation"
427
451
  const g: any = globalThis;
428
452
  const doFetch = options.fetch || (typeof g.fetch == 'function' && ((u: string, i: any) => g.fetch(u, i)));
429
- const fetchInit = {
430
- ...pickInit(base),
431
- ...pickInit(own),
432
- method: m,
433
- headers: h,
434
- body: b,
435
- };
436
453
  // the outcome, to every request waiting on this fetch (the failure once, after the last attempt)
437
454
  const end = (e: any, x?: any, status?: number) => {
438
455
  done(F);
@@ -454,23 +471,37 @@ export function makeFetchDriver(options: any = {}) {
454
471
  // 5-1 (H-9): renderComponent's fake learns which request (and resource) the next fetch is for
455
472
  options._tap?.(req, rn, pf);
456
473
  // called synchronously, so a test sees the call right after the sink emitted
457
- p = Promise.resolve(doFetch(href, {...fetchInit, signal: ctl.signal}));
474
+ p = batch ? Promise.resolve() : Promise.resolve(doFetch(href, {...fetchInit, signal: ctl.signal}));
475
+ if (batch || split) {
476
+ // D472: parts in parallel; a failed one split again when `split` says how
477
+ const one = (q: any, d: number, first?: any): Promise<any> => {
478
+ const [u, i] = prep({...req, ...q});
479
+ return (first || Promise.resolve(doFetch(u, {...i, signal: ctl.signal}))).then(async (res: any) => {
480
+ if (bad(res)) {
481
+ const e = await httpError(res, u);
482
+ const qs = d < 3 && live() && split?.(q, e);
483
+ if (qs?.length > 1) return all(qs, d + 1);
484
+ throw e;
485
+ }
486
+ return parserOf(q.parse || parse || options.parse)(res);
487
+ });
488
+ };
489
+ const all = (qs: any[], d: number) => Promise.all(qs.map(q => one(q, d))).then(bodies => combine(bodies, qs));
490
+ p = (batch ? all(batch, 0) : one(req, 0, p)).then(v => ({b: v}));
491
+ }
458
492
  } catch (e) {
459
493
  p = Promise.reject(e);
460
494
  }
461
495
  p.then(async (res: any) => {
496
+ if (batch || split) return live() && (k !== undefined && !F.z && C.put(k, req, res.b), end(0, res.b, 200));
462
497
  if (!live()) return;
463
498
  // R4-3: anything that throws here (a non-Response, a parser) fails the request
464
499
  try {
465
500
  const st = res.status;
466
- if (res.ok === false || st < 200 || st > 299) {
467
- let errBody: any;
468
- try { errBody = await autoParse(res); } catch (_) {}
501
+ if (bad(res)) {
502
+ const e = await httpError(res, href);
469
503
  if (!live() || ((st == 408 || st == 429 || st > 499) && again(res))) return;
470
- const e: any = new Error(`HTTP ${st}${res.statusText ? ' ' + res.statusText : ''}: ${href}`);
471
- e.status = st;
472
- e.body = errBody;
473
- return end(e, {status: st, body: errBody});
504
+ return end(e, {status: st, body: e.body});
474
505
  }
475
506
  let v: any;
476
507
  try { v = await parser(res); } catch (e) { return live() && end(e, {status: st}); }
@@ -480,7 +511,7 @@ export function makeFetchDriver(options: any = {}) {
480
511
  } catch (e) {
481
512
  live() && end(e);
482
513
  }
483
- }, (e: any) => { live() && !again() && end(e); });
514
+ }, (e: any) => { live() && (e?.status ? end(e, {status: e.status, body: e.body}) : !again() && end(e)); });
484
515
  };
485
516
  go();
486
517
  }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * PLAN-9 (D463): byId(field, id, idKey = 'id'), a { get, set } lens to the element of the array
3
+ * `state[field]` whose `idKey` is `id`: a record part's state (`state: (node) => byId('projects',
4
+ * node.params.id)`), or any child's `state={byId('todos', 3)}`. get: the element (undefined when it
5
+ * is gone); set: the array with that element replaced (the state unchanged when it is gone).
6
+ * Side-effect free.
7
+ */
8
+ export const byId = (field: string, id: any, idKey = 'id') => ({
9
+ get: (s: any) => (Array.isArray(s?.[field]) ? s[field].find((x: any) => x?.[idKey] === id) : undefined),
10
+ set: (s: any, v: any) => {
11
+ const a = s?.[field]
12
+ if (!Array.isArray(a) || !a.some((x: any) => x?.[idKey] === id)) return s
13
+ return {...s, [field]: a.map((x: any) => (x?.[idKey] === id ? v : x))}
14
+ },
15
+ })
package/src/extra/ssr.ts CHANGED
@@ -163,14 +163,14 @@ function withResources(def: any, state: any): any {
163
163
  }
164
164
 
165
165
  // PLAN-4 GS-9: uid(name?) as on the client: the root is 'u'; a child component adds its path in
166
- // the parent's view (or its `id` prop), as getComponentIdFromElement and instantiateSubComponents
166
+ // the parent's view (or its vnode `key`, else its `id` prop, D456), as getComponentIdFromElement and instantiateSubComponents
167
167
  // in the core do; a Collection item adds its key, a Switchable page its name; anything but
168
168
  // each part encoded by uidPart (G-214); in the root option anything but [A-Za-z0-9_-] becomes '_'
169
169
  // (as run() does). SSR ids = hydration ids
170
170
  const makeUid = (base: string) => (n?: string) => n ? base + '-' + n : base
171
171
  function childUid(uid: string, vnode: any, path: string): string {
172
- const id = vnode.data?.props?.id
173
- return uid + '-' + uidPart(('::' + ((id && JSON.stringify(id).replaceAll('"', '')) || path)).replace(/.*::(r\.)?/, ''))
172
+ const id = vnode.key ?? vnode.data?.props?.id
173
+ return uid + '-' + uidPart(('::' + ((id != null && JSON.stringify(id).replaceAll('"', '')) || path)).replace(/.*::(r\.)?/, ''))
174
174
  }
175
175
 
176
176
  // G-567: a view that returns a string or a number renders it as text (core/instance.ts view());
@@ -1754,6 +1754,8 @@ export function renderComponent(
1754
1754
  // 5-3: focus / online come from t.focus() / t.online() only
1755
1755
  _on: (f: any) => { signals.add(f); return () => signals.delete(f); },
1756
1756
  _tap: (req: any, res?: string, pf?: any) => { tapped = [req, res, pf]; },
1757
+ // D472: a batch / split request (a large decide()) is one fetch here, answered as one
1758
+ _whole: true,
1757
1759
  fetch: (url: string, init: any) => new Promise((resolve, reject) => {
1758
1760
  const [req, res, pf] = tapped || [{url}];
1759
1761
  tapped = undefined;
@@ -171,14 +171,14 @@ function rewrite(n: any, o: any, path: string): any {
171
171
  return r
172
172
  }
173
173
 
174
- /** widget markers among a Portal's children (the core doesn't walk them) */
174
+ /** widget markers among a Portal's children (the core doesn't walk them): `pres.widget.$k`, which the
175
+ * Portal's rewrite calls (core/markers/portal.ts; G-751: either may be registered first) */
175
176
  const inPortal = (c: any[], o: any, x: string): any[] =>
176
177
  c.map((n, i) => n?.sel == 'widget' ? rewrite(n, o, x + '.' + i) : n?.children ? {...n, children: inPortal(n.children, o, x + '.' + i)} : n)
178
+ ;(rewrite as any).$k = inPortal
177
179
 
178
180
  export function defineWidget(def: any): any {
179
181
  pres.widget ||= rewrite
180
- const p: any = pres.portal
181
- if (p && !p.$w) ((pres.portal = (n: any, o: any, x: string) => p({...n, children: inPortal(n.children || [], o, x)}, o, x)) as any).$w = 1
182
182
  const w: any = () => {}, commands: any = {}
183
183
  // D200: any name, `close` and `togglePopover` too (the core passes a widget command the options)
184
184
  for (const m in def.commands) commands[m] = (el: any, o: any) => el[W] && def.commands[m](el[W].i, o || {}, el)
package/src/index.d.ts CHANGED
@@ -2358,6 +2358,21 @@ export type FetchRequest = string | {
2358
2358
  * schema's (possibly transformed) value. A failure is an error with `issues`
2359
2359
  */
2360
2360
  validate?: StandardSchemaLike;
2361
+ /**
2362
+ * D472: send these sub-requests instead, in parallel (each inherits this request's fields:
2363
+ * `url`, `headers`, `init`, ...; its own `json` / `body` / `parse` win), and deliver ONE reply:
2364
+ * `combine(bodies, requests)`. One failure fails the request (once). The test fake
2365
+ * (renderComponent) answers the request as one, by its own `url` / `json`. `decide()` makes these
2366
+ */
2367
+ batch?: ReadonlyArray<{ url?: string; method?: string; headers?: Record<string, string>; query?: Record<string, unknown>; json?: unknown; body?: BodyInit; parse?: 'auto' | 'json' | 'text' | ((response: Response) => any) }>;
2368
+ /** D472: merges the parsed bodies of `batch` / `split` parts into the reply (default: the array) */
2369
+ combine?: (bodies: any[], requests: any[]) => any;
2370
+ /**
2371
+ * D472: a request (or a `batch` part) that fails with an HTTP error is sent again as the
2372
+ * requests this returns (≥ 2, in parallel, merged by `combine`), at most 3 times deep; null keeps
2373
+ * the error. `decide()` halves its questions on a 400 / 413 that names a size limit
2374
+ */
2375
+ split?: (request: any, error: Error & { status?: number; body?: any }) => any[] | null | undefined;
2361
2376
  /** Your own fields (an id, ...): not sent, returned on the reply's `request` */
2362
2377
  [appData: string]: any;
2363
2378
  } | {
@@ -3708,7 +3723,9 @@ declare global {
3708
3723
  namespace JSX {
3709
3724
  interface IntrinsicElements {
3710
3725
  [elemName: string]: any;
3726
+ /** @deprecated since 6.3.0, removed in the next major: `import { Collection } from 'sygnal'` and write `<Collection …>` (SYG426) */
3711
3727
  collection: CollectionProps<any>;
3728
+ /** @deprecated since 6.3.0, removed in the next major: `import { Switchable } from 'sygnal'` and write `<Switchable …>` (SYG426) */
3712
3729
  switchable: SwitchableProps<any>;
3713
3730
  slot: SlotProps;
3714
3731
  }
@@ -3721,6 +3738,15 @@ declare global {
3721
3738
  children: {};
3722
3739
  }
3723
3740
 
3741
+ /**
3742
+ * `key` on any tag (D456): on a component, its identity among its parent's children (before
3743
+ * its `id` prop, then its position): a keyed instance survives a move or a reorder, and a new
3744
+ * key makes a new instance. Never a prop of the component.
3745
+ */
3746
+ interface IntrinsicAttributes {
3747
+ key?: string | number | null;
3748
+ }
3749
+
3724
3750
  /**
3725
3751
  * A component's view receives `state: STATE` and `context`, but in JSX the parent passes
3726
3752
  * an optional `state` slice name or lens (`<Editor state="editor" />`, `<Panel />`).
@@ -3754,6 +3780,21 @@ export type { ChatState, ChatStatus, ChatActions, ChatDone, ChatOptions } from '
3754
3780
  // PLAN-6 M-2 / M-3 (3-M)
3755
3781
  export { answers, commandBar } from './ai.d'
3756
3782
  export type { AnswerPick, AnswerPicks, CommandBarState, CommandBarStatus, CommandBarActions, CommandBarOptions, CommandBarResult, CommandBarUnsure } from './ai.d'
3783
+ // PLAN-9 (A): catalogs, the compose behavior, composeWith, the layout utilities
3784
+ export { defineCatalog, compose, composeWith, fromTree, toTree, removeNode } from './ai.d'
3785
+ // PLAN-9 H: chat client tools and cards
3786
+ export { openToolCalls, askCard } from './ai.d'
3787
+ export { Composed } from './ai.d'
3788
+ export type { ChatClientTool, ChatAskOptions, ChatCard, ChatCardStatus, ChatToolPart, ComposedProps } from './ai.d'
3789
+ export type { Catalog, CatalogDecl, CatalogPartDecl, CatalogContainerDecl, CatalogParam, CatalogParamValue, CatalogParamsOf, CatalogRecord, CatalogPart, CatalogContainer, CatalogOption, CatalogTree, CatalogTreeNode, ComposeTree, ContainerView, Layout, LayoutNode, ComposeRenderOptions, ComposeRenderable, ComposeBackend, ComposeNames, ComposeState, ComposeStatus, ComposeActions, ComposeOptions, ComposeAnswer } from './ai.d'
3790
+
3791
+ /**
3792
+ * PLAN-9 (D463): a `{ get, set }` lens to the element of the array `state[field]` whose `idKey`
3793
+ * (default 'id') is `id`: a record part's state (`state: (node) => byId('projects',
3794
+ * node.params.id)`) or any child's `state={byId('todos', 3)}`. `get` is undefined once the element
3795
+ * is gone; `set` then leaves the state unchanged.
3796
+ */
3797
+ export function byId<T = any, S = any>(field: string, id: unknown, idKey?: string): { get(state: S): T | undefined; set(state: S, value: T): S }
3757
3798
  // PLAN-6 X-1 (3-X)
3758
3799
  export { makeMcpAppDriver } from './ai.d'
3759
3800
  export type { McpAppDriverOptions, McpAppSource, McpAppRequest, McpAppError, McpToolResult, McpHostContext, McpContentBlock, McpAppDisplayMode } from './ai.d'
package/src/index.ts CHANGED
@@ -68,6 +68,14 @@ export { anthropicMessages, agui, fromAISDK, strictSchemas } from './extra/ai/in
68
68
  export { makeMcpAppDriver } from './extra/ai/index'
69
69
  // A-3 (3-A3, experimental, D291)
70
70
  export { formTool } from './extra/ai/index'
71
+ // PLAN-9 (A): catalogs, the compose behavior, composeWith, the layout utilities
72
+ export { defineCatalog, compose, composeWith, fromTree, toTree, removeNode } from './extra/ai/index'
73
+ // PLAN-9 H: chat client tools and cards
74
+ export { openToolCalls, askCard } from './extra/ai/index'
75
+ // PLAN-9 M-1: <Composed catalog from /> (documented import: 'sygnal/ai')
76
+ export { Composed } from './extra/ai/index'
77
+ // PLAN-9 (D463): a lens to an array element by id
78
+ export { byId } from './extra/lens'
71
79
 
72
80
  // export dom helper functions (h, makeDOMDriver, etc.)
73
81
  export * from './cycle/dom/index'
package/src/lazy.ts CHANGED
@@ -1,5 +1,5 @@
1
- // the core's resolver for lazy components, registered on import (D157)
2
- import './core/markers/lazy';
1
+ // the core's resolver for lazy components (D157), registered by the first lazy() call (G-751)
2
+ import {enableLazy} from './core/markers/lazy';
3
3
 
4
4
  /**
5
5
  * PLAN-5 B-4 (D103): `when` defers the import: 'visible' starts it when a placeholder enters the
@@ -24,6 +24,7 @@ export interface LazyOptions {
24
24
  const WHEN = 'data-sygnal-when';
25
25
 
26
26
  export function lazy(loadFn: () => Promise<any>, options?: LazyOptions): any {
27
+ enableLazy();
27
28
  let cachedComponent: any = null;
28
29
  let loadError: any = null;
29
30
  const g: any = globalThis, when = options?.when, ph = options?.placeholderHeight;
package/src/portal.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import {h} from './cycle/dom/snabbdom';
2
- // the core's handler for this marker, registered on import (D157)
3
- import './core/markers/portal';
2
+ // the core's handler for this marker (D157), registered when a tag uses it (G-751: the pragma calls
3
+ // `$reg`; not on import, so a bundle that never uses the marker drops the handler)
4
+ import {enablePortal as reg} from './core/markers/portal';
4
5
 
5
6
  const Portal = (props: any) => {
7
+ reg();
6
8
  const {children, ...sanitizedProps} = props;
7
9
  return h('portal', {props: sanitizedProps}, children);
8
10
  };
9
11
  (Portal as any).componentName = 'portal';
10
12
  (Portal as any).preventInstantiation = true;
13
+ (Portal as any).$reg = reg;
11
14
 
12
15
  export {Portal};
13
16
  export default Portal;
@@ -322,7 +322,8 @@ export const createElementWithModules = (modules: Record<string, any>) => {
322
322
  if (typeof text === 'undefined') plain = !!(flatten(children, kids = []) & +plain)
323
323
  // G-492: a custom element (bit 8) routes list as a prop, in its own cache
324
324
  const d = data ? sanitizeData(data, modules, isComponent ? croutes : t & 8 ? xroutes : routes, isComponent || t & 8 && 2) : {}
325
- if (fn) d[fn.preventInstantiation ? 'm' : 'c'] = fn
325
+ // G-751: a marker's core handler registers on first use (`$reg`), not when its module loads
326
+ if (fn) d[fn.preventInstantiation ? 'm' : 'c'] = fn, fn.$reg?.()
326
327
  const key = k !== undefined ? k : data ? data.key : undefined
327
328
  const vnode = plain ? new (Plain as any)(sel, d, kids, text, key) : { sel, data: d, children: kids, text, elm: undefined, key }
328
329
  if (t & 2) applySvg(vnode)
package/src/suspense.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import {h} from './cycle/dom/snabbdom';
2
- // the core's handler for this marker, registered on import (D157)
3
- import './core/markers/suspense';
2
+ // the core's handler for this marker (D157), registered when a tag uses it (G-751: the pragma calls
3
+ // `$reg`; not on import, so a bundle that never uses the marker drops the handler)
4
+ import {enableSuspense as reg} from './core/markers/suspense';
4
5
 
5
6
  const Suspense = (props: any) => {
7
+ reg();
6
8
  const {children, ...sanitizedProps} = props;
7
9
  return h('suspense', {props: sanitizedProps}, children);
8
10
  };
9
11
  (Suspense as any).componentName = 'suspense';
10
12
  (Suspense as any).preventInstantiation = true;
13
+ (Suspense as any).$reg = reg;
11
14
 
12
15
  export {Suspense};
13
16
  export default Suspense;
package/src/switchable.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {h} from './cycle/dom/snabbdom';
2
- // the core's handler for this marker, registered on import (D157)
2
+ // the core's host for this marker, registered on import (D157): unlike Portal, Transition and
3
+ // Suspense (G-751), not on first use, because the lowercase tag works without an import (docs)
3
4
  import './core/hosts/switchable';
4
5
 
5
6
  /** `<Switchable of={{ a: A, b: B }} current={state.tab} />`: a marker vnode the core's Switchable host renders */
package/src/transition.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import {h} from './cycle/dom/snabbdom';
2
- // the core's handler for this marker, registered on import (D157)
3
- import './core/markers/transition';
2
+ // the core's handler for this marker (D157), registered when a tag uses it (G-751: the pragma calls
3
+ // `$reg`; not on import, so a bundle that never uses the marker drops the handler)
4
+ import {enableTransition as reg} from './core/markers/transition';
4
5
 
5
6
  const Transition = (props: any) => {
7
+ reg();
6
8
  const {children, ...sanitizedProps} = props;
7
9
  return h('transition', {props: sanitizedProps}, children);
8
10
  };
9
11
  (Transition as any).componentName = 'transition';
10
12
  (Transition as any).preventInstantiation = true;
13
+ (Transition as any).$reg = reg;
11
14
 
12
15
  export {Transition};
13
16
  export default Transition;
@@ -105,7 +105,8 @@ function componentVNode(comp: any, key: string, stateField: any, children: any[]
105
105
  children,
106
106
  text: undefined,
107
107
  elm: undefined,
108
- key: '__vike_' + key + '__',
108
+ // no vnode key: it would be the instance identity (D456); `id` is (G-207)
109
+ key: undefined,
109
110
  }
110
111
  }
111
112
 
@@ -132,7 +133,7 @@ function pageChildVNode(pageState: any, stateField: any = 'page'): any {
132
133
  children: [],
133
134
  text: undefined,
134
135
  elm: undefined,
135
- key: '__vike_page__',
136
+ key: undefined,
136
137
  }
137
138
  }
138
139