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.
- package/CHANGELOG.md +61 -0
- package/README.md +3 -1
- package/dist/ai.cjs.js +36 -0
- package/dist/ai.cjs.js.map +1 -1
- package/dist/ai.esm.js +1 -1
- package/dist/astro/server.cjs.js +3 -3
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3 -3
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +164 -3
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +164 -3
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/agent.md +7 -1
- package/dist/guide/ai-cards.md +319 -0
- package/dist/guide/ai-chat.md +4 -0
- package/dist/guide/ai-compose.md +881 -0
- package/dist/guide/ai-decisions.md +16 -0
- package/dist/guide/http.md +1 -0
- package/dist/index.cjs.js +5695 -347
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +638 -8
- package/dist/index.esm.js +5685 -347
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vike/onRenderClient.cjs.js +3 -2
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +3 -2
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/llms.txt +6 -4
- package/package.json +2 -1
- package/src/ai.d.ts +642 -7
- package/src/ai.ts +6 -0
- package/src/collection.ts +2 -1
- package/src/core/instance.ts +6 -3
- package/src/core/markers/lazy.ts +7 -3
- package/src/core/markers/portal.ts +5 -2
- package/src/core/markers/suspense.ts +3 -2
- package/src/core/markers/transition.ts +6 -3
- package/src/core/registry.ts +9 -5
- package/src/cycle/dom/IsolateModule.ts +25 -22
- package/src/extra/ai/chat/behavior.ts +354 -81
- package/src/extra/ai/chat/driver.ts +7 -4
- package/src/extra/ai/compose/agent.ts +55 -0
- package/src/extra/ai/compose/ask.ts +147 -0
- package/src/extra/ai/compose/behavior.ts +183 -0
- package/src/extra/ai/compose/catalog.ts +211 -0
- package/src/extra/ai/compose/composed.ts +61 -0
- package/src/extra/ai/compose/decisions.ts +1649 -0
- package/src/extra/ai/compose/diag.ts +33 -0
- package/src/extra/ai/compose/index.ts +23 -0
- package/src/extra/ai/compose/layout.ts +441 -0
- package/src/extra/ai/compose/llm.ts +1120 -0
- package/src/extra/ai/compose/render.ts +159 -0
- package/src/extra/ai/compose/testing.ts +71 -0
- package/src/extra/ai/compose/types.ts +159 -0
- package/src/extra/ai/decide.ts +97 -1
- package/src/extra/ai/index.ts +6 -1
- package/src/extra/diagnostics/checks/next.ts +86 -2
- package/src/extra/diagnostics/codes.ts +54 -0
- package/src/extra/fetchDriver.ts +58 -27
- package/src/extra/lens.ts +15 -0
- package/src/extra/ssr.ts +3 -3
- package/src/extra/testing.ts +2 -0
- package/src/extra/widget.ts +3 -3
- package/src/index.d.ts +41 -0
- package/src/index.ts +8 -0
- package/src/lazy.ts +3 -2
- package/src/portal.ts +5 -2
- package/src/pragma/index.ts +2 -1
- package/src/suspense.ts +5 -2
- package/src/switchable.ts +2 -1
- package/src/transition.ts +5 -2
- package/src/vike/onRenderClient.ts +3 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-9 K-2: catalog.render(slice, state, { scope?, frozen?, answer?, at? }), the composition as
|
|
3
|
+
* vnodes for the host's own view (phase1-contract.md). It renders `slice.preview ?? slice.layout`.
|
|
4
|
+
*
|
|
5
|
+
* - A part is a component vnode, so every composed part is a child instance of the host (CHILD.select,
|
|
6
|
+
* lenses, EVENTS, context and drivers work as for any child). It gets:
|
|
7
|
+
* - its identity: `key` = `scope/nodeKey` (or `nodeKey`); the core keys instances by the vnode key
|
|
8
|
+
* (D470: `key ?? props.id ?? path`), so params, including one named `id`, reach the part as props;
|
|
9
|
+
* - its state: `state: 'key'` as is, a lens as is, `(node, key) => lens` called, `local: init` a lens
|
|
10
|
+
* into `slice.local[nodeKey]` (at the slice's path `at`); each lens is memoized per node (G-752),
|
|
11
|
+
* so a part re-renders only when its own state, props or params change;
|
|
12
|
+
* - its params, defaults filled, as props (or `props(params, state)`; an object `props` is merged
|
|
13
|
+
* under them), and the props `node` (its key), and `scope` / `frozen` / `answer` when given (a
|
|
14
|
+
* result part's answer: `answer.value` when `answer.node` is its key).
|
|
15
|
+
* A part declared with `render` (a view function) is called inline with the same props and
|
|
16
|
+
* `state` read through its lens.
|
|
17
|
+
* - A container calls its view with `{ key, node, params (defaults filled), slots, children, ui, control, scope, frozen }`:
|
|
18
|
+
* `slots` maps each slot to its vnodes, `children` lists `{ key, node, part, slot, vnode }` in
|
|
19
|
+
* order, `ui` is `slice.ui[key]`, and `control(name, value)` returns data attributes to spread on an
|
|
20
|
+
* element; a click on it is the behavior's CONTROL `{ key, name, value }` (ui[key][name] = value).
|
|
21
|
+
* - A frozen render is wrapped in `<fieldset disabled class="compose-frozen">`.
|
|
22
|
+
* - A container view or `props()` that throws: only the composition is replaced (a `.compose-error`
|
|
23
|
+
* element), SYG406. Under `<Composed>` (composed.ts) the throw goes on to the host's `onError`
|
|
24
|
+
* instead, rendered in place of the composition (SYG408). An unknown part or slot is skipped (SYG250, once per catalog and name).
|
|
25
|
+
*/
|
|
26
|
+
import {createElement as h} from '../../../pragma/index'
|
|
27
|
+
import {error as logError} from '../../diagnostics/legacy'
|
|
28
|
+
import {dev} from './diag'
|
|
29
|
+
import type {Catalog, Part, RenderOptions} from './types'
|
|
30
|
+
|
|
31
|
+
const getIn = (o: any, path: any[]) => path.reduce((x, k) => x?.[k], o)
|
|
32
|
+
const setIn = (o: any, path: any[], v: any): any => {
|
|
33
|
+
const [k, ...rest] = path
|
|
34
|
+
const base = Array.isArray(o) ? o.slice() : {...o}
|
|
35
|
+
base[k] = rest.length ? setIn(o?.[k] ?? {}, rest, v) : v
|
|
36
|
+
return base
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** the memoized lenses (G-752), per catalog: (at, scope, node key, part, params) → lens */
|
|
40
|
+
const memo = /*#__PURE__*/ new WeakMap<object, Map<string, any>>()
|
|
41
|
+
const MAX = 2000
|
|
42
|
+
|
|
43
|
+
function lensOf(cat: Catalog, part: Part, node: any, key: string, at: any[] | null, scope?: string): any {
|
|
44
|
+
if (!part.hasLocal && typeof part.state != 'function') return part.state
|
|
45
|
+
let m = memo.get(cat)
|
|
46
|
+
if (!m) memo.set(cat, m = new Map())
|
|
47
|
+
const id = JSON.stringify([at, scope ?? null, key, part.id, part.hasLocal ? 0 : node.params ?? null])
|
|
48
|
+
let l = m.get(id)
|
|
49
|
+
if (l !== undefined) return l
|
|
50
|
+
if (part.hasLocal) {
|
|
51
|
+
const init = part.local, path = at ? [...at, 'local', key] : null
|
|
52
|
+
l = {
|
|
53
|
+
get: (st: any) => (path ? getIn(st, path) : undefined) ?? init,
|
|
54
|
+
set: (st: any, v: any) => (path ? setIn(st, path, v) : st),
|
|
55
|
+
}
|
|
56
|
+
} else l = (part.state as any)(node, key)
|
|
57
|
+
if (m.size > MAX) m.clear()
|
|
58
|
+
m.set(id, l)
|
|
59
|
+
return l
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** a part's params with the defaults filled in */
|
|
63
|
+
function paramsOf(part: {params: Record<string, any>}, node: any): Record<string, any> {
|
|
64
|
+
const out: any = {}
|
|
65
|
+
for (const k in part.params) {
|
|
66
|
+
const v = node.params?.[k] ?? (part.params[k] as any).default
|
|
67
|
+
if (v !== undefined) out[k] = v
|
|
68
|
+
}
|
|
69
|
+
for (const k in node.params) if (!(k in out)) out[k] = node.params[k]
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** the slice's path in the host state: `at`, else the host key whose value is the slice */
|
|
74
|
+
function pathOf(slice: any, state: any, at: RenderOptions['at']): any[] | null {
|
|
75
|
+
if (Array.isArray(at)) return at
|
|
76
|
+
if (typeof at == 'string') return [at]
|
|
77
|
+
if (state && typeof state == 'object') for (const k in state) if (state[k] === slice) return [k]
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `contain` false (the `<Composed>` marker): a throw reaches the caller, which renders the host's onError (SYG408) */
|
|
82
|
+
export function renderLayout(cat: Catalog, slice: any, state: any, opts: RenderOptions = {}, contain = true): any {
|
|
83
|
+
const layout = slice?.preview ?? slice?.layout
|
|
84
|
+
if (!layout || typeof layout != 'object' || !layout.nodes) return null
|
|
85
|
+
const {scope, frozen, answer} = opts
|
|
86
|
+
const at = pathOf(slice, state, opts.at), atStr = at ? at.join('.') : undefined
|
|
87
|
+
const nodes = layout.nodes, seen = new Set<string>()
|
|
88
|
+
const extra: any = {}
|
|
89
|
+
if (scope !== undefined) extra.scope = scope
|
|
90
|
+
if (frozen) extra.frozen = true
|
|
91
|
+
|
|
92
|
+
const nodeOf = (key: string): any => {
|
|
93
|
+
const n = nodes[key]
|
|
94
|
+
if (!n || seen.has(key)) return null
|
|
95
|
+
seen.add(key)
|
|
96
|
+
const e: any = cat.get(n.part)
|
|
97
|
+
if (!e) { dev('SYG250', 'catalog.render', `the layout names an unknown part '${n.part}' (node '${key}'); it is not rendered`, 'warn', cat, 'render:' + n.part); return null }
|
|
98
|
+
if (e.kind == 'container') {
|
|
99
|
+
const slots: Record<string, any[]> = {}, children: any[] = []
|
|
100
|
+
for (const s of e.slotNames) slots[s] = []
|
|
101
|
+
for (const s in n.slots) {
|
|
102
|
+
if (!slots[s]) { dev('SYG250', 'catalog.render', `node '${key}' (${e.id}) has no slot '${s}'; its children are not rendered`, 'warn', cat, `render:${e.id}:${s}`); continue }
|
|
103
|
+
for (const k of n.slots[s] || []) {
|
|
104
|
+
const vnode = nodeOf(k)
|
|
105
|
+
if (vnode == null) continue
|
|
106
|
+
slots[s].push(vnode)
|
|
107
|
+
children.push({key: k, node: nodes[k], part: cat.get(nodes[k].part), slot: s, vnode})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const control = (name: string, value?: any) => ({
|
|
111
|
+
'data-compose-control': name, 'data-compose-key': key, 'data-compose-value': JSON.stringify(value ?? null),
|
|
112
|
+
...(atStr !== undefined && {'data-compose-at': atStr}), ...(scope !== undefined && {'data-compose-scope': scope}),
|
|
113
|
+
})
|
|
114
|
+
return e.render({key, node: n, params: paramsOf(e, n), slots, children, ui: slice.ui?.[key] || {}, control, scope, frozen: !!frozen})
|
|
115
|
+
}
|
|
116
|
+
const part = e as Part
|
|
117
|
+
// H-2: a result part answers a card (chat's askCard gives the call id as scope); elsewhere its answer reaches no one
|
|
118
|
+
if (part.result && scope === undefined) dev('SYG160', 'catalog.render', `the result part '${part.id}' is rendered outside a card (no scope), so its answer reaches no tool call`, 'warn', cat, 'result:' + part.id)
|
|
119
|
+
const params = paramsOf(part, n)
|
|
120
|
+
const props = typeof part.props == 'function' ? part.props(params, state) : {...part.props, ...params}
|
|
121
|
+
const lens = lensOf(cat, part, n, key, at, scope)
|
|
122
|
+
const own = {...props, ...extra, node: key, ...(answer && answer.node === key && {answer: answer.value})}
|
|
123
|
+
if (part.render) return part.render({...own, state: lens === undefined ? state : typeof lens == 'string' ? state?.[lens] : lens.get(state)})
|
|
124
|
+
if (!part.component) return null
|
|
125
|
+
const id = scope !== undefined ? `${scope}/${key}` : key
|
|
126
|
+
// identity from the vnode key (D470: key ?? props.id ?? path), so a param named `id` reaches the part
|
|
127
|
+
return h(part.component, {...own, ...(lens !== undefined && {state: lens}), key: id})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let out: any
|
|
131
|
+
try { out = nodeOf(layout.root || 'page') } catch (err) {
|
|
132
|
+
if (!contain) throw err
|
|
133
|
+
logError('SYG406', 'catalog.render', 'a container view or a part\'s props() threw; the composition is replaced by an empty .compose-error element', 'Check the container\'s render and the part\'s props for missing data', err)
|
|
134
|
+
return h('div', {className: 'compose-error'})
|
|
135
|
+
}
|
|
136
|
+
return frozen ? h('fieldset', {disabled: true, className: 'compose-frozen'}, out) : out
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* H-2: an `ask_user` tool part's card as `[slice, options]` for catalog.render (askCard and
|
|
141
|
+
* `<Composed card={part} />`): the call id as scope, frozen unless the card is open, the answer
|
|
142
|
+
* marked, `at` the part's `ui` in the chat `k` (so `local` parts write to it). Null without a layout.
|
|
143
|
+
*/
|
|
144
|
+
export function cardArgs(state: any, part: any, k = 'assistant'): [any, RenderOptions] | null {
|
|
145
|
+
const u = part?.ui
|
|
146
|
+
if (!u?.layout) return null
|
|
147
|
+
const msgs = state?.[k]?.messages || []
|
|
148
|
+
let at: any[] | undefined
|
|
149
|
+
for (let i = msgs.length - 1; i >= 0 && !at; i--) {
|
|
150
|
+
const j = (msgs[i].parts || []).findIndex((x: any) => x.toolCallId === part.toolCallId && x.ui)
|
|
151
|
+
if (j >= 0) at = [k, 'messages', i, 'parts', j, 'ui']
|
|
152
|
+
}
|
|
153
|
+
return [u, {
|
|
154
|
+
scope: part.toolCallId,
|
|
155
|
+
frozen: part.state !== 'input-available' || u.status !== 'open',
|
|
156
|
+
answer: u.answer ? {node: u.answer.node, value: u.answer.value} : null,
|
|
157
|
+
...(at && {at}),
|
|
158
|
+
}]
|
|
159
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-9 T-1: composeWith(t, catalog, tree, { stream? }), a test fixture for a whole composition,
|
|
3
|
+
* the same for every backend:
|
|
4
|
+
*
|
|
5
|
+
* t.simulateAction('page.COMPOSE', 'Revenue and churn side by side')
|
|
6
|
+
* await composeWith(t, catalog, [{ part: 'row', children: [{ part: 'revenue' }, { part: 'churn' }] }])
|
|
7
|
+
*
|
|
8
|
+
* It finds the composition of `catalog` that is waiting for a reply (a `compose` use whose slice
|
|
9
|
+
* has `pending`) and answers each of its steps with the backend's `fake()` (the reply body that
|
|
10
|
+
* leads to `tree`) on the backend's sink, targeted by the use's reply action (`page.REPLY`):
|
|
11
|
+
* `t.respond`, or, with `stream: n` (true: 3) and a text body, `t.stream` in n chunks (an LLM
|
|
12
|
+
* backend's JSON). It resolves once no step is pending.
|
|
13
|
+
*
|
|
14
|
+
* H-2: a card of `chat({ ask: { catalog } })` being composed (an `ask_user` part whose `ui` has
|
|
15
|
+
* `pending`) is answered the same way, targeted by the chat's CARD action ('assistant.CARD'). A
|
|
16
|
+
* compose use waiting goes first.
|
|
17
|
+
*/
|
|
18
|
+
import {usesOf} from './behavior'
|
|
19
|
+
import {cardsOf} from './ask'
|
|
20
|
+
import type {Tree} from './types'
|
|
21
|
+
|
|
22
|
+
const err = (m: string) => new Error(`[Sygnal] composeWith: ${m}`)
|
|
23
|
+
|
|
24
|
+
/** a step waiting for a reply: its backend, the reply action, the pending step, and how to find the next one */
|
|
25
|
+
type Cur = {backend: any; target: string; pending: any; next: () => Cur | undefined} | undefined
|
|
26
|
+
|
|
27
|
+
export async function composeWith(t: any, catalog: any, tree: Tree, options: {stream?: number | boolean} = {}): Promise<void> {
|
|
28
|
+
const entries = usesOf(catalog), cards = cardsOf(catalog)
|
|
29
|
+
if (!entries.length && !cards.length) throw err('no compose behavior uses this catalog (compose(catalog, { backend }) in a component\'s uses, or chat({ ask: { catalog } }))')
|
|
30
|
+
const use = (e: (typeof entries)[number], k: string, s: any): Cur => ({backend: e.backend, target: k + '.REPLY', pending: s.pending, next: () => {
|
|
31
|
+
const n = t.state?.[k]
|
|
32
|
+
return n?.pending != null && n.pending !== s.pending ? use(e, k, n) : undefined
|
|
33
|
+
}})
|
|
34
|
+
// the card being composed in a chat's last message (cards compose one at a time)
|
|
35
|
+
const cardOf = (backend: any, k: string, id?: string): Cur => {
|
|
36
|
+
const msgs = t.state?.[k]?.messages, m = Array.isArray(msgs) ? msgs[msgs.length - 1] : null
|
|
37
|
+
const p = m?.role == 'assistant' && (m.parts || []).find((x: any) => x.ui?.status == 'composing' && x.ui.pending != null && x.state == 'input-available' && (id === undefined || x.toolCallId === id))
|
|
38
|
+
if (!p) return
|
|
39
|
+
return {backend, target: k + '.CARD', pending: p.ui.pending, next: () => { const n = cardOf(backend, k, p.toolCallId); return n && n.pending !== p.ui.pending ? n : undefined }}
|
|
40
|
+
}
|
|
41
|
+
// the use that made the pending step (several hosts may share the catalog and the key)
|
|
42
|
+
const find = (): Cur => {
|
|
43
|
+
let any: Cur
|
|
44
|
+
for (const e of entries) for (const k of e.keys) {
|
|
45
|
+
const s = t.state?.[k]
|
|
46
|
+
if (s?.pending == null) continue
|
|
47
|
+
if (typeof s.pending == 'object' && e.mine.has(s.pending)) return use(e, k, s)
|
|
48
|
+
any ||= use(e, k, s)
|
|
49
|
+
}
|
|
50
|
+
if (any) return any
|
|
51
|
+
for (const c of cards) for (const k of c.keys) {
|
|
52
|
+
const x = cardOf(c.backend, k)
|
|
53
|
+
if (x && c.mine.has(x.pending)) return x
|
|
54
|
+
any ||= x
|
|
55
|
+
}
|
|
56
|
+
return any
|
|
57
|
+
}
|
|
58
|
+
let cur: Cur = find()
|
|
59
|
+
if (!cur) { await t.settle(); cur = find() }
|
|
60
|
+
if (!cur) throw err('no composition is waiting for a reply (send COMPOSE first, or let the chat model call ask_user)')
|
|
61
|
+
for (let step = 0; cur && step < 16; step++) {
|
|
62
|
+
const {backend, target, pending} = cur
|
|
63
|
+
const sink: string = backend.sink
|
|
64
|
+
const body = backend.fake({catalog, state: t.state, pending, target: tree})
|
|
65
|
+
if (options.stream && typeof body == 'string') {
|
|
66
|
+
const n = Math.max(1, options.stream === true ? 3 : Number(options.stream) || 1), size = Math.ceil(body.length / n) || 1
|
|
67
|
+
for (let i = 0; i < n; i++) await t.stream(sink, [body.slice(i * size, (i + 1) * size)], target, {end: i == n - 1})
|
|
68
|
+
} else await t.respond(sink, body, target)
|
|
69
|
+
cur = cur.next()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-9 (phase1-contract.md): the internal shapes shared by the compose modules (catalog, layout,
|
|
3
|
+
* render, the behavior, the backends). Public, inferred types: src/ai.d.ts (`// ---- PLAN-9
|
|
4
|
+
* compose`). Type-only: this module emits nothing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* a param declaration, normalized: the canonical form every module reads (layout's checks, render,
|
|
9
|
+
* both backends). `kind` replaces the declaration's discriminator (`options` → 'options',
|
|
10
|
+
* `type: 'number' | 'text' | 'list'` → that kind (`type` itself is not kept), `from` → 'record');
|
|
11
|
+
* only the fields listed here are kept. An options param always has a `default` (its first option when none is
|
|
12
|
+
* given); `required` is a boolean on text and list params.
|
|
13
|
+
*
|
|
14
|
+
* options: { kind: 'options', options: { value: description }, default, label?, ask? }
|
|
15
|
+
* number: { kind: 'number', min?, max?, default?, bins?: { number: description }, label? }
|
|
16
|
+
* text: { kind: 'text', maxLength?, default?, required, label? }
|
|
17
|
+
* list: { kind: 'list', items?: { maxLength? }, min?, max?, required, default?, label? }
|
|
18
|
+
* record: { kind: 'record', from: (state) => [{ value, name?, aliases?, description? }], label? }
|
|
19
|
+
*/
|
|
20
|
+
export type Param =
|
|
21
|
+
| {kind: 'options'; options: Record<string, string>; default?: any; label?: string; ask?: string}
|
|
22
|
+
| {kind: 'number'; min?: number; max?: number; default?: number; bins?: Record<string, string>; label?: string}
|
|
23
|
+
| {kind: 'text'; maxLength?: number; default?: string; required?: boolean; label?: string}
|
|
24
|
+
| {kind: 'list'; items?: {maxLength?: number}; min?: number; max?: number; required?: boolean; default?: string[]; label?: string}
|
|
25
|
+
| {kind: 'record'; from: (state: any) => any[]; label?: string; default?: undefined}
|
|
26
|
+
|
|
27
|
+
/** a catalog part, normalized */
|
|
28
|
+
export interface Part {
|
|
29
|
+
id: string
|
|
30
|
+
kind: 'part'
|
|
31
|
+
component?: any
|
|
32
|
+
render?: (props: any) => any
|
|
33
|
+
props?: Record<string, any> | ((params: Record<string, any>, state: any) => Record<string, any>)
|
|
34
|
+
description: string
|
|
35
|
+
state?: string | {get: Function; set?: Function} | ((node: Node, key: string) => any)
|
|
36
|
+
/** `local: init`: state kept at slice.local[nodeKey] */
|
|
37
|
+
local?: any
|
|
38
|
+
hasLocal: boolean
|
|
39
|
+
params: Record<string, Param>
|
|
40
|
+
/** the record param's name (at most one per part) */
|
|
41
|
+
record?: string
|
|
42
|
+
/** the places it may go: 'container:slot' entries (the root is 'page:default') */
|
|
43
|
+
in: string[]
|
|
44
|
+
group?: string
|
|
45
|
+
maxUses: number
|
|
46
|
+
required: boolean
|
|
47
|
+
pin?: 'start' | 'end'
|
|
48
|
+
companions: string[]
|
|
49
|
+
category?: string
|
|
50
|
+
result: boolean
|
|
51
|
+
/** everything the declaration had, for backends that read extra fields */
|
|
52
|
+
decl: Record<string, any>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** a catalog container (a view function with named slots), normalized */
|
|
56
|
+
export interface Container {
|
|
57
|
+
id: string
|
|
58
|
+
kind: 'container'
|
|
59
|
+
slots: Record<string, string | null>
|
|
60
|
+
slotNames: string[]
|
|
61
|
+
minSlots?: number
|
|
62
|
+
/** children per slot: fewer than `min` unwraps it, more than `max` overflow (a row splits) */
|
|
63
|
+
min?: number
|
|
64
|
+
max?: number
|
|
65
|
+
/** 'leaves': only parts in its slots (nested containers go after it) */
|
|
66
|
+
accepts?: 'leaves'
|
|
67
|
+
params: Record<string, Param>
|
|
68
|
+
arranged: boolean
|
|
69
|
+
direction?: 'row' | 'column'
|
|
70
|
+
description?: string
|
|
71
|
+
render: (a: any) => any
|
|
72
|
+
/** where the container itself may go ('container:slot' entries) */
|
|
73
|
+
in: string[]
|
|
74
|
+
decl: Record<string, any>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type Entry = Part | Container
|
|
78
|
+
|
|
79
|
+
/** one node of a layout */
|
|
80
|
+
export interface Node {
|
|
81
|
+
part: string
|
|
82
|
+
params?: Record<string, any>
|
|
83
|
+
slots?: Record<string, string[]>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** the layout: a node map with identity keys */
|
|
87
|
+
export interface Layout {
|
|
88
|
+
root: 'page'
|
|
89
|
+
nodes: Record<string, Node>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** the nested form a model writes and tests use */
|
|
93
|
+
export type TreeNode = {part: string; params?: Record<string, any>; children?: Tree | Record<string, Tree>}
|
|
94
|
+
export type Tree = TreeNode[]
|
|
95
|
+
|
|
96
|
+
export interface Option {option: string; part: string; params?: Record<string, any>; description: string}
|
|
97
|
+
|
|
98
|
+
export interface Catalog {
|
|
99
|
+
parts: Record<string, Part>
|
|
100
|
+
containers: Record<string, Container>
|
|
101
|
+
get(id: string): Entry | undefined
|
|
102
|
+
options(state: any): Option[]
|
|
103
|
+
/** a record part's records (validated `from`: SYG252) */
|
|
104
|
+
records(part: Part | string, state: any): Array<{value: any; name?: string; aliases?: string[]; description?: string}>
|
|
105
|
+
finish(layout: Layout, state?: any): Layout
|
|
106
|
+
/** the page's label (B: `containers.page.description ?? catalog.description`) */
|
|
107
|
+
description?: string
|
|
108
|
+
categories: Record<string, string>
|
|
109
|
+
/** false: the catalog opted out of the built-in row */
|
|
110
|
+
rows: boolean
|
|
111
|
+
guidance?: string
|
|
112
|
+
/**
|
|
113
|
+
* maxParts: leaves on the page (finish drops the last non-required ones). maxDepth: how deep a
|
|
114
|
+
* CONTAINER may sit, counting the page as 0 and its children as 1: a container at depth d > maxDepth
|
|
115
|
+
* moves to the page (finish; structure/composer.mjs's rule). Both backends follow it: decisions'
|
|
116
|
+
* reading, and llm's schema and repair (a container is offered at depth d only when d <= maxDepth).
|
|
117
|
+
* Leaves may sit one level below the deepest container. (The research llm-composer.mjs counted
|
|
118
|
+
* leaves instead: its maxDepth m is this one's m - 1.)
|
|
119
|
+
*/
|
|
120
|
+
limits: {maxParts: number; maxDepth: number}
|
|
121
|
+
examples: any[]
|
|
122
|
+
render(slice: any, state: any, opts?: RenderOptions): any
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface RenderOptions {
|
|
126
|
+
scope?: string
|
|
127
|
+
frozen?: boolean
|
|
128
|
+
answer?: {node: string; value: any} | null
|
|
129
|
+
/** the slice's path in the host state (default: found by identity among the host's keys) */
|
|
130
|
+
at?: string | Array<string | number>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** the reply action names a backend's requests carry */
|
|
134
|
+
export interface Names {ok: string; error: string; delta: string; key: string}
|
|
135
|
+
|
|
136
|
+
/** a composition backend (phase1-contract.md: A defines it, B and C implement it) */
|
|
137
|
+
export interface Backend {
|
|
138
|
+
sink: string
|
|
139
|
+
start(a: {catalog: Catalog; state: any; prompt: string; previous: Layout | null; names: Names}): {request: any; pending: any}
|
|
140
|
+
reply(a: {catalog: Catalog; state: any; pending: any; reply: any; names: Names}):
|
|
141
|
+
{layout?: Layout | null; unavailable?: true; preview?: Layout; request?: any; pending?: any; decisions?: unknown; error?: string; keyed?: boolean}
|
|
142
|
+
delta?(a: {catalog: Catalog; state: any; pending: any; delta: any}): {preview?: Layout; keyed?: boolean}
|
|
143
|
+
fake(a: {catalog: Catalog; state: any; pending: any; target: Tree}): unknown
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type Status = 'idle' | 'composing' | 'preview' | 'done' | 'unavailable' | 'error'
|
|
147
|
+
|
|
148
|
+
/** the compose behavior's slice */
|
|
149
|
+
export interface ComposeSlice {
|
|
150
|
+
prompt: string
|
|
151
|
+
status: Status
|
|
152
|
+
layout: Layout | null
|
|
153
|
+
preview: Layout | null
|
|
154
|
+
pending: any
|
|
155
|
+
decisions: unknown
|
|
156
|
+
error: string | null
|
|
157
|
+
ui: Record<string, Record<string, any>>
|
|
158
|
+
local: Record<string, any>
|
|
159
|
+
}
|
package/src/extra/ai/decide.ts
CHANGED
|
@@ -44,6 +44,102 @@ function dictionary({url = '/api/decide', model, state, questions, images, ...re
|
|
|
44
44
|
return {url, method: 'POST', json, ...rest};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// ---------- D472 / G-757: large question sets ----------
|
|
48
|
+
/**
|
|
49
|
+
* A conservative estimate of the prompt tokens of a request's question (Ollama /v1/systemone: each
|
|
50
|
+
* question's prompt holds the state and EVERY question of the request, instructions and options).
|
|
51
|
+
* Measured on nimble over compose.decisions()'s requests: 1.13–1.30 × the count Ollama reports
|
|
52
|
+
* (ASCII ~2.6 characters a token; any other character counted as one).
|
|
53
|
+
*/
|
|
54
|
+
const tokensOf = (x: any) => {
|
|
55
|
+
const t = text(x ?? '');
|
|
56
|
+
let other = 0;
|
|
57
|
+
for (let i = 0; i < t.length; i++) if (t.charCodeAt(i) > 127) other++;
|
|
58
|
+
return (t.length - other) / 2.5 + other;
|
|
59
|
+
};
|
|
60
|
+
const questionTokens = (name: string, q: any) => {
|
|
61
|
+
const c = q?.criteria, opts: any[] = Array.isArray(c) ? c.map(x => [x, null]) : Object.entries(c && typeof c == 'object' ? c : {});
|
|
62
|
+
return 12 + 4 * opts.length + tokensOf(name) + tokensOf(q?.instructions) + opts.reduce((n, [k, v]) => n + tokensOf(k) + (v == null ? 0 : tokensOf(v)), 0);
|
|
63
|
+
};
|
|
64
|
+
/** the estimated prompt tokens of one question of a request with this state and these questions */
|
|
65
|
+
export const promptTokens = (state: any, questions: any) => Math.ceil(60 + tokensOf(state ?? '') +
|
|
66
|
+
Object.keys(questions || {}).reduce((n, k) => n + questionTokens(k, questions[k]), 0));
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The questions in parts within `maxQuestions` and `maxPromptTokens` (the estimate): one part when
|
|
70
|
+
* they fit; else the fewest parts of about even size (by the estimate), in question order. A
|
|
71
|
+
* question too large on its own goes alone.
|
|
72
|
+
*/
|
|
73
|
+
export function splitQuestions(state: any, questions: any, maxQuestions = Infinity, maxPromptTokens = Infinity): string[][] {
|
|
74
|
+
const all = Object.keys(questions || {});
|
|
75
|
+
if (all.length <= maxQuestions && (maxPromptTokens === Infinity || promptTokens(state, questions) <= maxPromptTokens)) return [all];
|
|
76
|
+
const room = maxPromptTokens - promptTokens(state, {});
|
|
77
|
+
const cost = all.map(n => questionTokens(n, questions[n]));
|
|
78
|
+
const total = cost.reduce((s, x) => s + x, 0);
|
|
79
|
+
const fits = (a: number, b: number) => b - a == 1 || (b - a <= maxQuestions && cost.slice(a, b).reduce((s, x) => s + x, 0) <= room);
|
|
80
|
+
for (let k = Math.max(2, Math.ceil(all.length / maxQuestions), Math.ceil(total / Math.max(1, room))); k < all.length; k++) {
|
|
81
|
+
// k parts: a cut where the running estimate passes each j/k of the total, or every ceil(n/k) questions
|
|
82
|
+
const cuts = [0];
|
|
83
|
+
for (let i = 0, acc = 0; i < all.length - 1; i++) {
|
|
84
|
+
acc += cost[i];
|
|
85
|
+
if (cuts.length < k && (acc >= total * cuts.length / k || i + 1 - cuts[cuts.length - 1] >= Math.ceil(all.length / k))) cuts.push(i + 1);
|
|
86
|
+
}
|
|
87
|
+
cuts.push(all.length);
|
|
88
|
+
if (cuts.every((c, j) => !j || fits(cuts[j - 1], c))) return cuts.slice(1).map((c, j) => all.slice(cuts[j], c));
|
|
89
|
+
}
|
|
90
|
+
return all.map(n => [n]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pick = (questions: any, names: string[]) => Object.fromEntries(names.map(n => [n, questions[n]]));
|
|
94
|
+
const sum = (a: any, b: any): any => {
|
|
95
|
+
if (typeof a == 'number' && typeof b == 'number') return a + b;
|
|
96
|
+
if (a && b && typeof a == 'object' && typeof b == 'object' && !Array.isArray(a)) {
|
|
97
|
+
const out: any = {...a};
|
|
98
|
+
for (const k of Object.keys(b)) out[k] = k in a ? sum(a[k], b[k]) : b[k];
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
return a ?? b;
|
|
102
|
+
};
|
|
103
|
+
/** the replies of a split decision as one: `answers` merged by name, `usage` summed, the rest (`model`) of the first */
|
|
104
|
+
export const combineDecisions = (bodies: any[]) => bodies.length == 1 ? bodies[0] : bodies.reduce((a, b) => ({
|
|
105
|
+
...b, ...a,
|
|
106
|
+
answers: {...a?.answers, ...b?.answers},
|
|
107
|
+
...((a?.usage || b?.usage) && {usage: sum(a?.usage, b?.usage)}),
|
|
108
|
+
}));
|
|
109
|
+
/**
|
|
110
|
+
* The reactive split: a request a server rejects as too large (HTTP 400 or 413 whose body mentions
|
|
111
|
+
* tokens, fields, context, a limit, or "too long / many / large") is sent again as two halves of its
|
|
112
|
+
* questions (the fetch driver: at most 3 times deep). Anything else: null (the error stands).
|
|
113
|
+
*/
|
|
114
|
+
const TOO_LARGE = /token|field|context|limit|exceed|too (long|many|large)/i;
|
|
115
|
+
export const splitDecision = (req: any, error: any) => {
|
|
116
|
+
if (error?.status != 400 && error?.status != 413) return null;
|
|
117
|
+
if (!TOO_LARGE.test(text(error.body ?? ''))) return null;
|
|
118
|
+
const qs = req?.json?.questions;
|
|
119
|
+
const n = Array.isArray(qs) ? qs.length : Object.keys(qs || {}).length;
|
|
120
|
+
if (n < 2) return null;
|
|
121
|
+
const half = (a: number, b: number) => ({...req, json: {...req.json, questions: Array.isArray(qs) ? qs.slice(a, b) : pick(qs, Object.keys(qs).slice(a, b))}});
|
|
122
|
+
return [half(0, n >> 1), half(n >> 1, n)];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* A decision request over the limits becomes a fetch-driver batch (sub-requests in parallel, one
|
|
126
|
+
* merged reply); every request can be halved when a server rejects it as too large. `split: false`:
|
|
127
|
+
* neither. `json` stays the whole request (what tests and `t.requests` see).
|
|
128
|
+
*/
|
|
129
|
+
const DEFAULT_MAX_QUESTIONS = 64;
|
|
130
|
+
function splitting(build: (o: any) => any) {
|
|
131
|
+
return ({maxQuestions = DEFAULT_MAX_QUESTIONS, maxPromptTokens = Infinity, split = true, ...o}: any = {}) => {
|
|
132
|
+
const req = build(o);
|
|
133
|
+
if (split === false) return req;
|
|
134
|
+
const parts = splitQuestions(o.state, o.questions, Math.max(1, maxQuestions), maxPromptTokens);
|
|
135
|
+
const sub = (names: string[]) => {
|
|
136
|
+
const {url, method, json, parse} = build({...o, questions: pick(o.questions, names)});
|
|
137
|
+
return parse ? {url, method, json, parse} : {url, method, json};
|
|
138
|
+
};
|
|
139
|
+
return {...req, ...(parts.length > 1 && {batch: parts.map(sub)}), combine: combineDecisions, split: splitDecision};
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
47
143
|
const text = (x: any) => typeof x == 'string' ? x : JSON.stringify(x);
|
|
48
144
|
// the dictionary form takes base64 files; OpenAI takes data URLs (the type from the magic bytes)
|
|
49
145
|
const dataUrl = (b64: string) => b64.startsWith('data:') ? b64
|
|
@@ -101,4 +197,4 @@ function openai({url = '/api/decide', model, state, questions, images, ...rest}:
|
|
|
101
197
|
}
|
|
102
198
|
|
|
103
199
|
/** the dictionary-form request (TypeSafe, Ollama, OpenRouter, AI Gateway); `decide.openai()`: the same as an OpenAI Decisions request */
|
|
104
|
-
export const decide = /*#__PURE__*/ Object.assign(dictionary, {openai})
|
|
200
|
+
export const decide = /*#__PURE__*/ Object.assign(splitting(dictionary), {openai: splitting(openai)})
|
package/src/extra/ai/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ export {outputJsonSchema} from './chat/output'
|
|
|
20
20
|
// A-2 (2-W): WebMCP (experimental, D241)
|
|
21
21
|
export {experimentalExposeWebMcp} from './webmcp'
|
|
22
22
|
// L-3 (2-C): the chat behavior
|
|
23
|
-
export {chat} from './chat/behavior'
|
|
23
|
+
export {chat, openToolCalls} from './chat/behavior'
|
|
24
24
|
// L-2 (2-T): transports, wave 1 (D248), each its own module (tree-shaken), and the Open Responses
|
|
25
25
|
// event encoder (D273)
|
|
26
26
|
export {openResponses} from './transports/openResponses'
|
|
@@ -40,3 +40,8 @@ export {fromAISDK} from './transports/fromAISDK'
|
|
|
40
40
|
export {strictSchemas} from './schema/strict'
|
|
41
41
|
// X-1 (3-X): MCP Apps, the view side
|
|
42
42
|
export {makeMcpAppDriver} from './mcpApp'
|
|
43
|
+
// PLAN-9 (A): catalogs (K-1), render (K-2), layout utilities (K-3), the compose behavior (C-1),
|
|
44
|
+
// composeWith (T-1)
|
|
45
|
+
export {defineCatalog, compose, composeWith, fromTree, toTree, removeNode, askCard} from './compose/index'
|
|
46
|
+
// PLAN-9 M-1: the <Composed> marker
|
|
47
|
+
export {Composed} from './compose/index'
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* a Collection's viewTransitionName that isn't an identifier, the same name twice on the
|
|
18
18
|
* page after a patch, G-460: on rendered elements), SYG612 (D173: a form
|
|
19
19
|
* 6.0 removed, met at runtime; once per form and component, with a link to the migration
|
|
20
|
-
* guide)
|
|
20
|
+
* guide), SYG453 (PLAN-9, D470: two children with one identity share one instance), SYG426
|
|
21
|
+
* (PLAN-9, D471: a lowercase <collection> / <switchable> tag, deprecated in 6.3.0).
|
|
21
22
|
*
|
|
22
23
|
* Only while diagnostics are on (the checks' own rule); production builds never load this.
|
|
23
24
|
*/
|
|
@@ -94,6 +95,85 @@ function removedTags(f: any, v: any, depth = 0): void {
|
|
|
94
95
|
if (Array.isArray(kids)) for (const k of kids) removedTags(f, k, depth + 1)
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* PLAN-9 SYG453 (D470): two children of one view with the same identity (the same component and
|
|
100
|
+
* `key`, else `id`; `sel::key` in the core) are one instance, rendered in both places. The core
|
|
101
|
+
* doesn't look (0 production bytes): in the owner's injected vnode that instance's output, a
|
|
102
|
+
* scoped root (`data.isolate` in the real DOM driver, a `___<scope>` class in the mock DOM), is
|
|
103
|
+
* the same object twice. Its name and key come from the child whose DOM source has that scope.
|
|
104
|
+
*/
|
|
105
|
+
const scopeOfVnode = (n: any): string | undefined => {
|
|
106
|
+
const iso = n.data?.isolate
|
|
107
|
+
if (Array.isArray(iso)) return iso.length ? iso[iso.length - 1]?.scope : undefined
|
|
108
|
+
const m = typeof n.sel == 'string' && /\.___([^.]+)/.exec(n.sel)
|
|
109
|
+
return m ? m[1] : undefined
|
|
110
|
+
}
|
|
111
|
+
const scopeOfSource = (dom: any): string | undefined => {
|
|
112
|
+
const ns = dom?.namespace
|
|
113
|
+
if (Array.isArray(ns)) return ns[ns.length - 1]?.scope
|
|
114
|
+
const p = dom?._path
|
|
115
|
+
const m = Array.isArray(p) && /^\.___(.+)$/.exec(String(p[p.length - 1]))
|
|
116
|
+
return m ? m[1] : undefined
|
|
117
|
+
}
|
|
118
|
+
function sharedIdentity(iv: any, v: any): void {
|
|
119
|
+
const seen = new Set<any>(), dup = new Set<string>()
|
|
120
|
+
const walk = (n: any, d: number): void => {
|
|
121
|
+
if (!n || typeof n != 'object' || d > 300) return
|
|
122
|
+
const sc = scopeOfVnode(n)
|
|
123
|
+
if (sc !== undefined) {
|
|
124
|
+
if (seen.has(n)) { dup.add(sc); return }
|
|
125
|
+
seen.add(n)
|
|
126
|
+
}
|
|
127
|
+
const kids = n.children
|
|
128
|
+
if (Array.isArray(kids)) for (const k of kids) walk(k, d + 1)
|
|
129
|
+
}
|
|
130
|
+
walk(v, 0)
|
|
131
|
+
if (!dup.size) return
|
|
132
|
+
const owner = iv.name
|
|
133
|
+
for (const sc of dup) {
|
|
134
|
+
let child: any
|
|
135
|
+
try { child = iv.children().find((c: any) => scopeOfSource(c.sources?.DOM) === sc) } catch (_) { /* ignore */ }
|
|
136
|
+
const name = child ? child.name : 'a child component'
|
|
137
|
+
// the identity's own part of the child's uid (the owner's uid + '-' + its key, id or path)
|
|
138
|
+
const key = child && typeof child.uid == 'string' && child.uid.startsWith(iv.uid + '-') ? child.uid.slice(iv.uid.length + 1) : undefined
|
|
139
|
+
if (!once(`SYG453:${owner}:${name}:${key ?? sc}`)) continue
|
|
140
|
+
devReport('SYG453', {
|
|
141
|
+
component: owner,
|
|
142
|
+
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`,
|
|
143
|
+
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`,
|
|
144
|
+
data: {child: child ? name : undefined, key},
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* PLAN-9 SYG426 (D471): the lowercase host tags, `<collection>` / `<switchable>` (JSX string tags,
|
|
151
|
+
* or `h('collection')`), deprecated in 6.3.0; the next major removes them (and registers the hosts
|
|
152
|
+
* on first use, as Portal). The core doesn't look (0 production bytes): the instance's template
|
|
153
|
+
* keeps a host marker after reconcile (only `pres` markers are rewritten), and the imported
|
|
154
|
+
* components' markers carry their function in `data.m`, a string tag's don't. The template is
|
|
155
|
+
* the raw instance's (`iv.i`: the InstanceView's own field, a dev entry of the same build reads it;
|
|
156
|
+
* the vnode onRender gets has the hosts' output injected in their place). Once per tag.
|
|
157
|
+
*/
|
|
158
|
+
const HOST_TAGS: Record<string, string> = {collection: 'Collection', switchable: 'Switchable'}
|
|
159
|
+
function lowercaseTags(iv: any): void {
|
|
160
|
+
const raw = iv.i
|
|
161
|
+
if (!raw?.kids?.size || !raw.tmpl) return
|
|
162
|
+
const walk = (n: any, d: number): void => {
|
|
163
|
+
if (!n || typeof n != 'object' || n.$p || d > 300) return
|
|
164
|
+
const name = typeof n.sel == 'string' && !n.data?.m && HOST_TAGS[n.sel]
|
|
165
|
+
if (name && once(`SYG426:${n.sel}`)) devReport('SYG426', {
|
|
166
|
+
component: iv.name,
|
|
167
|
+
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)`,
|
|
168
|
+
fix: `import { ${name} } from 'sygnal' and write <${name} …> (sygnal-check --fix does it)`,
|
|
169
|
+
data: {tag: n.sel, component: name},
|
|
170
|
+
})
|
|
171
|
+
const kids = n.children
|
|
172
|
+
if (Array.isArray(kids)) for (const k of kids) walk(k, d + 1)
|
|
173
|
+
}
|
|
174
|
+
walk(raw.tmpl, 0)
|
|
175
|
+
}
|
|
176
|
+
|
|
97
177
|
/** the instance fields the checks read from today's Component instances */
|
|
98
178
|
class Facade {
|
|
99
179
|
_so: any
|
|
@@ -311,7 +391,11 @@ export function nextHooks(_api: any): any {
|
|
|
311
391
|
onRender(iv: any, vnode: any) {
|
|
312
392
|
const f = fac(iv)
|
|
313
393
|
H.onRender(f, vnode)
|
|
314
|
-
if (on())
|
|
394
|
+
if (!on()) return
|
|
395
|
+
removedTags(f, vnode)
|
|
396
|
+
try { lowercaseTags(iv) } catch (_) { /* a check never breaks the app */ }
|
|
397
|
+
// SYG453: only a view with children can render one twice
|
|
398
|
+
if (iv.children().length) try { sharedIdentity(iv, vnode) } catch (_) { /* a check never breaks the app */ }
|
|
315
399
|
},
|
|
316
400
|
onReducer(iv: any, type: string, prev: any, next: any) {
|
|
317
401
|
H.onReducer(fac(iv), type, prev, next, 'STATE')
|