sygnal 6.2.0 → 6.4.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 +112 -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/index.cjs.js +69 -3
  7. package/dist/astro/index.cjs.js.map +1 -1
  8. package/dist/astro/index.mjs +69 -3
  9. package/dist/astro/index.mjs.map +1 -1
  10. package/dist/astro/server.cjs.js +3 -3
  11. package/dist/astro/server.cjs.js.map +1 -1
  12. package/dist/astro/server.mjs +3 -3
  13. package/dist/astro/server.mjs.map +1 -1
  14. package/dist/diagnostics.cjs.js +211 -11
  15. package/dist/diagnostics.cjs.js.map +1 -1
  16. package/dist/diagnostics.esm.js +211 -11
  17. package/dist/diagnostics.esm.js.map +1 -1
  18. package/dist/guide/agent.md +7 -1
  19. package/dist/guide/ai-cards.md +319 -0
  20. package/dist/guide/ai-chat.md +4 -0
  21. package/dist/guide/ai-compose.md +881 -0
  22. package/dist/guide/ai-decisions.md +16 -0
  23. package/dist/guide/http.md +1 -0
  24. package/dist/guide/mcp-apps.md +59 -4
  25. package/dist/index.cjs.js +5704 -350
  26. package/dist/index.cjs.js.map +1 -1
  27. package/dist/index.d.ts +647 -10
  28. package/dist/index.esm.js +5694 -350
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/sygnal.min.js +1 -1
  31. package/dist/sygnal.min.js.map +1 -1
  32. package/dist/vike/config/package.json +1 -1
  33. package/dist/vike/onRenderClient.cjs.js +3 -2
  34. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  35. package/dist/vike/onRenderClient.mjs +3 -2
  36. package/dist/vike/onRenderClient.mjs.map +1 -1
  37. package/dist/vite/plugin.cjs.js +69 -3
  38. package/dist/vite/plugin.cjs.js.map +1 -1
  39. package/dist/vite/plugin.mjs +69 -3
  40. package/dist/vite/plugin.mjs.map +1 -1
  41. package/llms.txt +7 -5
  42. package/package.json +2 -1
  43. package/src/ai.d.ts +642 -7
  44. package/src/ai.ts +6 -0
  45. package/src/collection.ts +2 -1
  46. package/src/core/instance.ts +6 -3
  47. package/src/core/markers/lazy.ts +7 -3
  48. package/src/core/markers/portal.ts +5 -2
  49. package/src/core/markers/suspense.ts +3 -2
  50. package/src/core/markers/transition.ts +6 -3
  51. package/src/core/registry.ts +9 -5
  52. package/src/cycle/dom/IsolateModule.ts +25 -22
  53. package/src/extra/ai/chat/behavior.ts +354 -81
  54. package/src/extra/ai/chat/driver.ts +7 -4
  55. package/src/extra/ai/compose/agent.ts +55 -0
  56. package/src/extra/ai/compose/ask.ts +147 -0
  57. package/src/extra/ai/compose/behavior.ts +183 -0
  58. package/src/extra/ai/compose/catalog.ts +211 -0
  59. package/src/extra/ai/compose/composed.ts +61 -0
  60. package/src/extra/ai/compose/decisions.ts +1649 -0
  61. package/src/extra/ai/compose/diag.ts +33 -0
  62. package/src/extra/ai/compose/index.ts +23 -0
  63. package/src/extra/ai/compose/layout.ts +441 -0
  64. package/src/extra/ai/compose/llm.ts +1120 -0
  65. package/src/extra/ai/compose/render.ts +159 -0
  66. package/src/extra/ai/compose/testing.ts +71 -0
  67. package/src/extra/ai/compose/types.ts +159 -0
  68. package/src/extra/ai/decide.ts +97 -1
  69. package/src/extra/ai/index.ts +6 -1
  70. package/src/extra/diagnostics/checks/dom.ts +10 -2
  71. package/src/extra/diagnostics/checks/next.ts +103 -3
  72. package/src/extra/diagnostics/checks/persist.ts +4 -2
  73. package/src/extra/diagnostics/checks/wiring.ts +14 -6
  74. package/src/extra/diagnostics/codes.ts +59 -0
  75. package/src/extra/fetchDriver.ts +58 -27
  76. package/src/extra/lens.ts +15 -0
  77. package/src/extra/pwa.ts +3 -1
  78. package/src/extra/reducers.ts +10 -4
  79. package/src/extra/ssr.ts +3 -3
  80. package/src/extra/testing.ts +2 -0
  81. package/src/extra/widget.ts +3 -3
  82. package/src/index.d.ts +50 -2
  83. package/src/index.ts +8 -0
  84. package/src/lazy.ts +3 -2
  85. package/src/portal.ts +5 -2
  86. package/src/pragma/index.ts +2 -1
  87. package/src/suspense.ts +5 -2
  88. package/src/switchable.ts +2 -1
  89. package/src/transition.ts +5 -2
  90. package/src/vike/onRenderClient.ts +3 -2
  91. package/src/vite/plugin.ts +63 -2
@@ -267,6 +267,13 @@ const CODE_TITLES = {
267
267
  SYG423: 'Context change skipped a view whose output it changes',
268
268
  SYG424: 'Duplicate Collection item id',
269
269
  SYG425: 'isolatedState slice lacks initialState keys',
270
+ // PLAN-9 (D471): checks/next.ts; dev entry and sygnal-check
271
+ SYG426: 'Deprecated lowercase marker tag',
272
+ // checks/next.ts and sygnal-check: a component reference that is not a function (an import of
273
+ // a name the module doesn't export is undefined under Vitest)
274
+ SYG427: 'CHILD.select() of something that is not a component',
275
+ // PLAN-9 (D470): checks/next.ts; dev entry
276
+ SYG453: 'Two children with the same identity share one instance',
270
277
  SYG601: "Retired in 6.0: Missing or invalid sources",
271
278
  SYG602: 'Intent is not a function',
272
279
  SYG603: 'Intent returned an invalid value',
@@ -330,6 +337,29 @@ const CODE_TITLES = {
330
337
  // PLAN-6 A-2: experimentalExposeWebMcp (src/extra/ai/webmcp.ts; dev)
331
338
  SYG674: 'WebMCP not available',
332
339
  SYG676: 'WebMCP registerTool rejected',
340
+ // ---- PLAN-9 compose (A: src/extra/ai/compose/; dev, reported with the severity passed) ----
341
+ SYG161: 'Catalog container render is a component with a model',
342
+ SYG162: 'Catalog part with neither component nor render',
343
+ SYG250: 'Layout names an unknown part or slot',
344
+ SYG251: 'Invalid param value in a layout',
345
+ SYG252: 'Record param from() does not return an array of { value }',
346
+ SYG450: 'Catalog in names an unknown container or slot',
347
+ SYG451: 'Composed has no catalog or from is not a composition',
348
+ SYG452: 'Layout lists a node key twice',
349
+ SYG681: 'Compose backend option problem',
350
+ // PLAN-9 B-1/B-2: compose.decisions() (src/extra/ai/compose/decisions.ts; explicit severities)
351
+ SYG680: 'Part is LLM-only under a decision backend',
352
+ SYG682: 'Decision model answered outside the criteria',
353
+ // PLAN-9 B-3: compose.llm() (src/extra/ai/compose/llm.ts; the compose status becomes 'error')
354
+ SYG683: 'LLM layout invalid after repair',
355
+ // PLAN-9 X-2: static only (sygnal-check rules/syg253-catalog.js)
356
+ SYG253: 'Misspelled catalog declaration key',
357
+ SYG254: 'Catalog descriptions or options overlap',
358
+ // PLAN-9 H: chat client tools and ask (src/extra/ai/chat/behavior.ts, compose/ask.ts, compose/render.ts)
359
+ SYG160: 'Result part rendered outside a card',
360
+ SYG684: 'Client tool shadows an agent tool',
361
+ SYG685: 'Answer for a tool call that is not open',
362
+ SYG686: 'Ask catalog without a result part',
333
363
  // PLAN-4 GS-3: a11y lane (static only, sygnal-check)
334
364
  SYG701: 'Click listener on a non-interactive element',
335
365
  SYG702: 'Form field without an accessible label',
@@ -446,6 +476,12 @@ const DEV_CODE_SEVERITY = {
446
476
  SYG423: 'warn',
447
477
  SYG424: 'warn',
448
478
  SYG425: 'warn',
479
+ // PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
480
+ SYG426: 'warn',
481
+ // checks/next.ts: CHILD.select(undefined), usually a missing export
482
+ SYG427: 'error',
483
+ // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
484
+ SYG453: 'warn',
449
485
  SYG612: 'error',
450
486
  // PLAN-4 D131 (element.ts, through the core bridge; a console warning in dev with diagnostics off)
451
487
  SYG644: 'warn',
@@ -486,6 +522,16 @@ const DEV_CODE_SEVERITY = {
486
522
  // PLAN-6 A-2 (experimentalExposeWebMcp reports them with an explicit severity)
487
523
  SYG674: 'info',
488
524
  SYG676: 'warn',
525
+ // PLAN-9 B-1/B-2 (compose.decisions() reports them with an explicit severity)
526
+ SYG680: 'info',
527
+ SYG682: 'warn',
528
+ // PLAN-9 B-3 (compose.llm)
529
+ SYG683: 'error',
530
+ // PLAN-9 H (chat client tools and ask; reported with the severity passed)
531
+ SYG160: 'warn',
532
+ SYG684: 'warn',
533
+ SYG685: 'warn',
534
+ SYG686: 'warn',
489
535
  // PLAN-5 V-1: VirtualCollection (checks/virtual.ts, through the core bridge)
490
536
  SYG430: 'warn',
491
537
  SYG431: 'warn',
@@ -517,6 +563,19 @@ const DEV_CODE_SEVERITY = {
517
563
  SYG153: 'warn',
518
564
  SYG730: 'warn',
519
565
  SYG731: 'warn',
566
+ // ---- PLAN-9 compose (A: src/extra/ai/compose/, reported with the severity passed, dev) ----
567
+ SYG161: 'error',
568
+ SYG162: 'error',
569
+ SYG250: 'warn',
570
+ SYG251: 'warn',
571
+ SYG252: 'warn',
572
+ SYG450: 'warn',
573
+ SYG451: 'warn',
574
+ SYG452: 'warn',
575
+ SYG681: 'error',
576
+ // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
577
+ SYG253: 'warn',
578
+ SYG254: 'info',
520
579
  };
521
580
  function getCodeInfo(code) {
522
581
  const severity = CODE_SEVERITY[code];
@@ -737,11 +796,20 @@ const underTest = (c) => {
737
796
  const dom = c?.sources?.[c.DOMSourceName || 'DOM'];
738
797
  return !!dom && typeof dom == 'object' && '_hub' in dom;
739
798
  };
740
- // G-620: an LLM / chat sink gets the chat driver's fix, not makeFetchDriver's
741
- const driverFix = (sink) => `Pass a driver named ${sink} to run(): ` + (/^(LLM|CHAT|AI|ASSISTANT)$/.test(sink)
742
- ? `run(App, { ${sink}: makeChatDriver({ transport: openResponses({ baseURL: 'http://localhost:11434/v1', model: 'llama3.2' }) }) }) (from 'sygnal/ai'; uiMessageStream('/api/chat') for an AI SDK server)`
743
- : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
744
- `. Check the spelling against the drivers you pass`;
799
+ // G-620: the usual driver for a known sink name (an LLM / chat sink gets the chat driver's fix,
800
+ // SW the service worker's), the generic text for any other name
801
+ const DRIVER_FOR = [
802
+ [/^(LLM|CHAT|AI|ASSISTANT)$/, (s) => `run(App, { ${s}: makeChatDriver({ transport: openResponses({ baseURL: 'http://localhost:11434/v1', model: 'llama3.2' }) }) }) (from 'sygnal/ai'; uiMessageStream('/api/chat') for an AI SDK server)`],
803
+ [/^(SW|SERVICE_WORKER)$/, (s) => `run(App, { ${s}: makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD }) }) (enabled: false keeps the driver, with no service worker, on a dev server)`],
804
+ [/^(HTTP|API|FETCH)$/, (s) => `run(App, { ${s}: makeFetchDriver() })`],
805
+ [/^(WS|SOCKET)$/, (s) => `run(App, { ${s}: makeSocketDriver() })`],
806
+ ];
807
+ const driverFix = (sink) => {
808
+ const known = DRIVER_FOR.find(([re]) => re.test(sink));
809
+ return `Pass a driver named ${sink} to run(): ` +
810
+ (known ? known[1](sink) : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
811
+ `. Check the spelling against the drivers you pass`;
812
+ };
745
813
  const wiringCheck = {
746
814
  id: 'wiring',
747
815
  onIntent(component, actionNames) {
@@ -1290,6 +1358,10 @@ onReset(() => {
1290
1358
  });
1291
1359
  const unref = (t) => { if (t && typeof t.unref === 'function')
1292
1360
  t.unref(); return t; };
1361
+ /**
1362
+ * The view's source, plus the words sygnal/vite registered for the component (its module's and
1363
+ * its relative imports' string literals: markup a view helper draws), when it did
1364
+ */
1293
1365
  function viewSource(component) {
1294
1366
  const view = component && component.view;
1295
1367
  if (typeof view !== 'function')
@@ -1302,6 +1374,9 @@ function viewSource(component) {
1302
1374
  catch (_) {
1303
1375
  src = '';
1304
1376
  }
1377
+ const registered = globalThis.__SYGNAL_VIEW_TOKENS__?.get?.(view);
1378
+ if (typeof registered == 'string')
1379
+ src += '\n' + registered;
1305
1380
  viewTokens.set(view, src);
1306
1381
  }
1307
1382
  return src;
@@ -2946,7 +3021,8 @@ const persistCheck = {
2946
3021
  const o = p.options, init = view.initialState;
2947
3022
  if (!o || !init || typeof init != 'object')
2948
3023
  return;
2949
- const keys = Object.keys(init);
3024
+ // a behavior's slice (`uses = { chat: chat() }`) is a state key too
3025
+ const keys = [...Object.keys(init), ...Object.keys(view.uses || {})];
2950
3026
  for (const opt of ['pick', 'omit']) {
2951
3027
  for (const k of Array.isArray(o[opt]) ? o[opt] : []) {
2952
3028
  if (keys.includes(k) || !once(`SYG223:${name}:${opt}:${k}`))
@@ -3625,7 +3701,8 @@ function installInspect() {
3625
3701
  * a Collection's viewTransitionName that isn't an identifier, the same name twice on the
3626
3702
  * page after a patch, G-460: on rendered elements), SYG612 (D173: a form
3627
3703
  * 6.0 removed, met at runtime; once per form and component, with a link to the migration
3628
- * guide).
3704
+ * guide), SYG453 (PLAN-9, D470: two children with one identity share one instance), SYG426
3705
+ * (PLAN-9, D471: a lowercase <collection> / <switchable> tag, deprecated in 6.3.0).
3629
3706
  *
3630
3707
  * Only while diagnostics are on (the checks' own rule); production builds never load this.
3631
3708
  */
@@ -3659,6 +3736,18 @@ function removed(component, name, anchor, detail) {
3659
3736
  data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3660
3737
  });
3661
3738
  }
3739
+ /** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
3740
+ function notComponent(f, x) {
3741
+ if (!once(`SYG427:${f.name}`))
3742
+ return;
3743
+ const got = x === null ? 'null' : typeof x == 'object' ? 'an object' : String(x);
3744
+ devReport('SYG427', {
3745
+ component: f.name,
3746
+ message: `${f.name}'s intent calls CHILD.select(${got}): that is not a component, so it never emits`,
3747
+ fix: "Pass the child component itself, and check its import: a name its module doesn't export is undefined (Vitest runs on; the browser fails to link the module). Export it: export function Child() { … }",
3748
+ data: { got },
3749
+ });
3750
+ }
3662
3751
  /** the removed forms a definition shows (statics, view arity, model keys), once per function */
3663
3752
  let seenDefs = new WeakSet();
3664
3753
  // G-332: resetChecks() forgets them (as the once() dedupe)
@@ -3698,6 +3787,101 @@ function removedTags(f, v, depth = 0) {
3698
3787
  for (const k of kids)
3699
3788
  removedTags(f, k, depth + 1);
3700
3789
  }
3790
+ /**
3791
+ * PLAN-9 SYG453 (D470): two children of one view with the same identity (the same component and
3792
+ * `key`, else `id`; `sel::key` in the core) are one instance, rendered in both places. The core
3793
+ * doesn't look (0 production bytes): in the owner's injected vnode that instance's output, a
3794
+ * scoped root (`data.isolate` in the real DOM driver, a `___<scope>` class in the mock DOM), is
3795
+ * the same object twice. Its name and key come from the child whose DOM source has that scope.
3796
+ */
3797
+ const scopeOfVnode = (n) => {
3798
+ const iso = n.data?.isolate;
3799
+ if (Array.isArray(iso))
3800
+ return iso.length ? iso[iso.length - 1]?.scope : undefined;
3801
+ const m = typeof n.sel == 'string' && /\.___([^.]+)/.exec(n.sel);
3802
+ return m ? m[1] : undefined;
3803
+ };
3804
+ const scopeOfSource = (dom) => {
3805
+ const ns = dom?.namespace;
3806
+ if (Array.isArray(ns))
3807
+ return ns[ns.length - 1]?.scope;
3808
+ const p = dom?._path;
3809
+ const m = Array.isArray(p) && /^\.___(.+)$/.exec(String(p[p.length - 1]));
3810
+ return m ? m[1] : undefined;
3811
+ };
3812
+ function sharedIdentity(iv, v) {
3813
+ const seen = new Set(), dup = new Set();
3814
+ const walk = (n, d) => {
3815
+ if (!n || typeof n != 'object' || d > 300)
3816
+ return;
3817
+ const sc = scopeOfVnode(n);
3818
+ if (sc !== undefined) {
3819
+ if (seen.has(n)) {
3820
+ dup.add(sc);
3821
+ return;
3822
+ }
3823
+ seen.add(n);
3824
+ }
3825
+ const kids = n.children;
3826
+ if (Array.isArray(kids))
3827
+ for (const k of kids)
3828
+ walk(k, d + 1);
3829
+ };
3830
+ walk(v, 0);
3831
+ if (!dup.size)
3832
+ return;
3833
+ const owner = iv.name;
3834
+ for (const sc of dup) {
3835
+ let child;
3836
+ try {
3837
+ child = iv.children().find((c) => scopeOfSource(c.sources?.DOM) === sc);
3838
+ }
3839
+ catch (_) { /* ignore */ }
3840
+ const name = child ? child.name : 'a child component';
3841
+ // the identity's own part of the child's uid (the owner's uid + '-' + its key, id or path)
3842
+ const key = child && typeof child.uid == 'string' && child.uid.startsWith(iv.uid + '-') ? child.uid.slice(iv.uid.length + 1) : undefined;
3843
+ if (!once(`SYG453:${owner}:${name}:${key ?? sc}`))
3844
+ continue;
3845
+ devReport('SYG453', {
3846
+ component: owner,
3847
+ message: `${owner} renders ${child ? `<${name}>` : name} in two places with the same identity${key !== undefined ? ` (key or id ${JSON.stringify(key)})` : ''}, so both places show one instance: it has one state, and the second tag's props replace the first's`,
3848
+ fix: `Give each <${child ? name : 'Child'}> a different key (or id) among its parent's children, e.g. key={item.id}; a key on a component is its identity`,
3849
+ data: { child: child ? name : undefined, key },
3850
+ });
3851
+ }
3852
+ }
3853
+ /**
3854
+ * PLAN-9 SYG426 (D471): the lowercase host tags, `<collection>` / `<switchable>` (JSX string tags,
3855
+ * or `h('collection')`), deprecated in 6.3.0; the next major removes them (and registers the hosts
3856
+ * on first use, as Portal). The core doesn't look (0 production bytes): the instance's template
3857
+ * keeps a host marker after reconcile (only `pres` markers are rewritten), and the imported
3858
+ * components' markers carry their function in `data.m`, a string tag's don't. The template is
3859
+ * the raw instance's (`iv.i`: the InstanceView's own field, a dev entry of the same build reads it;
3860
+ * the vnode onRender gets has the hosts' output injected in their place). Once per tag.
3861
+ */
3862
+ const HOST_TAGS = { collection: 'Collection', switchable: 'Switchable' };
3863
+ function lowercaseTags(iv) {
3864
+ const raw = iv.i;
3865
+ if (!raw?.kids?.size || !raw.tmpl)
3866
+ return;
3867
+ const walk = (n, d) => {
3868
+ if (!n || typeof n != 'object' || n.$p || d > 300)
3869
+ return;
3870
+ const name = typeof n.sel == 'string' && !n.data?.m && HOST_TAGS[n.sel];
3871
+ if (name && once(`SYG426:${n.sel}`))
3872
+ devReport('SYG426', {
3873
+ component: iv.name,
3874
+ message: `${iv.name} renders the lowercase <${n.sel}> tag, which is deprecated since Sygnal 6.3.0 and will be removed in the next major (it will then render an unknown <${n.sel}> element)`,
3875
+ fix: `import { ${name} } from 'sygnal' and write <${name} …> (sygnal-check --fix does it)`,
3876
+ data: { tag: n.sel, component: name },
3877
+ });
3878
+ const kids = n.children;
3879
+ if (Array.isArray(kids))
3880
+ for (const k of kids)
3881
+ walk(k, d + 1);
3882
+ };
3883
+ walk(raw.tmpl, 0);
3884
+ }
3701
3885
  /** the instance fields the checks read from today's Component instances */
3702
3886
  class Facade {
3703
3887
  constructor(__next) {
@@ -3947,8 +4131,13 @@ function nextHooks(_api) {
3947
4131
  const v = t[k];
3948
4132
  if (k !== 'CHILD' || !v || typeof v.select != 'function')
3949
4133
  return v;
3950
- return { ...v, select: (x, ...r) => { if (typeof x == 'string')
3951
- removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`); return v.select(x, ...r); } };
4134
+ return { ...v, select: (x, ...r) => {
4135
+ if (typeof x == 'string')
4136
+ removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`);
4137
+ else if (typeof x != 'function')
4138
+ notComponent(f, x);
4139
+ return v.select(x, ...r);
4140
+ } };
3952
4141
  },
3953
4142
  });
3954
4143
  },
@@ -3958,8 +4147,19 @@ function nextHooks(_api) {
3958
4147
  onRender(iv, vnode) {
3959
4148
  const f = fac(iv);
3960
4149
  H.onRender(f, vnode);
3961
- if (on())
3962
- removedTags(f, vnode);
4150
+ if (!on())
4151
+ return;
4152
+ removedTags(f, vnode);
4153
+ try {
4154
+ lowercaseTags(iv);
4155
+ }
4156
+ catch (_) { /* a check never breaks the app */ }
4157
+ // SYG453: only a view with children can render one twice
4158
+ if (iv.children().length)
4159
+ try {
4160
+ sharedIdentity(iv, vnode);
4161
+ }
4162
+ catch (_) { /* a check never breaks the app */ }
3963
4163
  },
3964
4164
  onReducer(iv, type, prev, next) {
3965
4165
  H.onReducer(fac(iv), type, prev, next, 'STATE');