@urbicon-ui/blocks 6.38.0 → 6.39.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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Render-tree assembly for the A2UI renderer — the bridge between the pure-TS
3
+ * processor (`a2ui-validate.ts`, plain Maps) and the recursive Svelte dispatcher
4
+ * (`A2UINode.svelte`). Pure TS, no Svelte, so `A2UINode` and `A2UIView` share
5
+ * one typed contract (the house pattern of `md-context.ts`, deliberately not a
6
+ * Svelte context — the tree is private and explicit prop flow keeps it
7
+ * inspectable).
8
+ *
9
+ * `buildRenderTree` walks the component graph from `root` into a fully-expanded,
10
+ * BOUNDED node tree: child/children references resolved, list templates expanded
11
+ * over the data model, dangling references marked, and the depth/node/cycle
12
+ * limits enforced in ONE place so the Svelte layer can never recurse without
13
+ * bound regardless of payload. Graph-level ISSUES (the reportable faults) are
14
+ * still produced by `collectGraphIssues` in `a2ui-validate.ts`; this walk only
15
+ * shapes what renders.
16
+ */
17
+ import { getAtPointer } from './a2ui-data.js';
18
+ /** Default traversal bounds — mirror `collectGraphIssues` so the tree and the issue list agree. */
19
+ export const A2UI_MAX_DEPTH = 32;
20
+ export const A2UI_MAX_NODES = 512;
21
+ /** Clamp a finite number into `[min, max]`; NaN/∞ collapse to `min`. */
22
+ function clampWeight(value, min, max) {
23
+ if (!Number.isFinite(value))
24
+ return min;
25
+ return Math.min(max, Math.max(min, value));
26
+ }
27
+ /**
28
+ * Resolve a `{ path }` data binding to the ABSOLUTE JSON Pointer it addresses,
29
+ * honouring the template scope for relative paths. Returns `undefined` for
30
+ * literals and function-call bindings (nothing to write back into).
31
+ */
32
+ export function bindingPointer(value, scopePrefix) {
33
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
34
+ return undefined;
35
+ const object = value;
36
+ if (typeof object.call === 'string')
37
+ return undefined;
38
+ if (typeof object.path !== 'string')
39
+ return undefined;
40
+ const path = object.path;
41
+ return path.startsWith('/') ? path : `${scopePrefix ?? ''}/${path}`;
42
+ }
43
+ /** Coerce an unknown model value to a string array of stable option values (for ChoicePicker). */
44
+ export function toStringArray(value) {
45
+ if (!Array.isArray(value))
46
+ return [];
47
+ return value.filter((item) => typeof item === 'string');
48
+ }
49
+ /** Coerce a scalar model value to the string an `<input>` displays. */
50
+ export function toInputString(value) {
51
+ if (value === null || value === undefined)
52
+ return '';
53
+ if (typeof value === 'string')
54
+ return value;
55
+ if (typeof value === 'number' && Number.isFinite(value))
56
+ return String(value);
57
+ if (typeof value === 'boolean')
58
+ return String(value);
59
+ return '';
60
+ }
61
+ function childRefs(instance, scopePrefix, state) {
62
+ const out = [];
63
+ const single = instance.props.get('child');
64
+ if (typeof single === 'string')
65
+ out.push({ id: single, scope: scopePrefix });
66
+ const children = instance.props.get('children');
67
+ if (Array.isArray(children)) {
68
+ for (const childId of children) {
69
+ if (typeof childId === 'string')
70
+ out.push({ id: childId, scope: scopePrefix });
71
+ }
72
+ }
73
+ else if (children !== null && typeof children === 'object') {
74
+ const template = children;
75
+ if (typeof template.componentId === 'string' && typeof template.path === 'string') {
76
+ const absPath = template.path.startsWith('/')
77
+ ? template.path
78
+ : `${scopePrefix ?? ''}/${template.path}`;
79
+ const list = getAtPointer(state.dataModel, absPath);
80
+ if (Array.isArray(list)) {
81
+ for (let i = 0; i < list.length; i++) {
82
+ out.push({ id: template.componentId, scope: `${absPath}/${i}` });
83
+ if (out.length > state.maxNodes)
84
+ break; // bound the expansion itself
85
+ }
86
+ }
87
+ // non-array / undefined → render nothing (issue reported by collectGraphIssues)
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+ function buildNode(id, scopePrefix, key, parentIsFlex, ancestors, depth, state) {
93
+ if (state.count >= state.maxNodes || depth > state.maxDepth)
94
+ return null;
95
+ state.count++;
96
+ const instance = state.components.get(id);
97
+ if (!instance) {
98
+ return { key, id, instance: null, scopePrefix, children: [] };
99
+ }
100
+ const node = { key, id, instance, scopePrefix, children: [] };
101
+ if (parentIsFlex) {
102
+ const weight = instance.props.get('weight');
103
+ if (typeof weight === 'number' && Number.isFinite(weight)) {
104
+ node.weight = clampWeight(weight, 0, 100);
105
+ }
106
+ }
107
+ const isFlex = instance.component === 'Row' || instance.component === 'Column';
108
+ const nextAncestors = new Set(ancestors);
109
+ nextAncestors.add(id);
110
+ const refs = childRefs(instance, scopePrefix, state);
111
+ for (let i = 0; i < refs.length; i++) {
112
+ if (state.count >= state.maxNodes)
113
+ break;
114
+ const ref = refs[i];
115
+ if (nextAncestors.has(ref.id))
116
+ continue; // cycle: truncate (issue reported elsewhere)
117
+ const childKey = `${ref.id}@${ref.scope ?? ''}#${i}`;
118
+ const child = buildNode(ref.id, ref.scope, childKey, isFlex, nextAncestors, depth + 1, state);
119
+ if (child)
120
+ node.children.push(child);
121
+ }
122
+ return node;
123
+ }
124
+ /**
125
+ * Assemble the bounded render tree for a surface, starting at `root`. Returns
126
+ * `null` when no `root` component exists yet (the caller renders a placeholder
127
+ * while streaming, or a fault chip once settled).
128
+ */
129
+ export function buildRenderTree(surface, options) {
130
+ if (!surface.components.has('root'))
131
+ return null;
132
+ const state = {
133
+ components: surface.components,
134
+ dataModel: surface.dataModel,
135
+ maxDepth: options?.maxDepth ?? A2UI_MAX_DEPTH,
136
+ maxNodes: options?.maxNodes ?? A2UI_MAX_NODES,
137
+ count: 0
138
+ };
139
+ return buildNode('root', undefined, 'root', false, new Set(), 0, state);
140
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The A2UI processor: incremental, whitelist-only, fail-loud validation that
3
+ * turns a stream of Server→Client envelopes into per-surface component maps and
4
+ * data models. Pure TS, no Svelte — deliberately NOT reactive (the house
5
+ * pattern of the streaming-markdown engine: plain Maps mutated in place; the
6
+ * Svelte layer bumps a version counter to re-derive the render tree).
7
+ *
8
+ * Security posture (untrusted-payload path):
9
+ * - Whitelist-only: only registry-declared props reach the render layer, so
10
+ * handler injection (`onclick`, …) and prop smuggling are structurally
11
+ * impossible — there is no payload spread.
12
+ * - `__proto__`/`constructor`/`prototype` are rejected as component ids, prop
13
+ * keys, pointer segments and action-context keys.
14
+ * - Never merges or spreads payload objects; everything flows through `Map`s.
15
+ * - `collectGraphIssues` bounds traversal (depth 32, nodes 512) to cap DoS.
16
+ *
17
+ * Issue routing: envelope-level faults (bad version, unknown envelope type,
18
+ * op-before-createSurface) land in `globalIssues`; everything scoped to a known
19
+ * surface lands in that surface's `issues`. Graph-level faults (cycle, depth,
20
+ * node count, dangling refs) are computed on demand by `collectGraphIssues`,
21
+ * because "dangling" is a warning mid-stream and an error once settled.
22
+ */
23
+ import { type A2uiValidationIssue } from './a2ui.types.js';
24
+ export interface A2uiComponentInstance {
25
+ id: string;
26
+ component: string;
27
+ /** Only registry-declared props; the raw payload values (dynamics resolved at render). */
28
+ props: ReadonlyMap<string, unknown>;
29
+ /** Index of the envelope that last defined this component. */
30
+ sourceIndex: number;
31
+ }
32
+ export interface A2uiSurfaceState {
33
+ surfaceId: string;
34
+ components: Map<string, A2uiComponentInstance>;
35
+ /** Root of the surface data model (mutated in place by two-way edits). */
36
+ dataModel: unknown;
37
+ issues: A2uiValidationIssue[];
38
+ }
39
+ export interface A2uiProcessor {
40
+ surfaces: Map<string, A2uiSurfaceState>;
41
+ globalIssues: A2uiValidationIssue[];
42
+ /** Validate + apply one envelope. `index` positions it as `/messages/<index>` in issue paths. */
43
+ apply(envelope: unknown, index: number): void;
44
+ }
45
+ export declare function createA2uiProcessor(): A2uiProcessor;
46
+ /**
47
+ * Normalize a render payload into an envelope list. Accepts an envelope array,
48
+ * a single envelope, or the golden-file `{ messages: [...] }` wrapper. Returns
49
+ * an `issue` (and no envelopes) only for wholly unusable input.
50
+ */
51
+ export declare function normalizeA2uiPayload(payload: unknown): {
52
+ envelopes: unknown[];
53
+ issue?: A2uiValidationIssue;
54
+ };
55
+ export interface GraphIssueOptions {
56
+ /** When true, dangling refs are warnings (still streaming); when false, errors. @default false */
57
+ streaming?: boolean;
58
+ /** @default 32 */
59
+ maxDepth?: number;
60
+ /** @default 512 */
61
+ maxNodes?: number;
62
+ }
63
+ /**
64
+ * Walk the component graph from `root` and collect structural faults that only
65
+ * exist once components are assembled: cycles, excessive depth/nodes, dangling
66
+ * child references, non-array template paths, and mis-placed `weight`.
67
+ *
68
+ * NOTE — contract extension. The design's published `a2ui-validate.ts` surface
69
+ * is `createA2uiProcessor` + `normalizeA2uiPayload`. This function is an
70
+ * additive export (no existing signature changed): the renderer (WP-B) needs
71
+ * render-time graph faults whose severity depends on the `streaming` flag, and
72
+ * the test suite exercises them here. Traversal is bounded by `maxNodes` so a
73
+ * template over a huge array cannot DoS the walk itself.
74
+ */
75
+ export declare function collectGraphIssues(surface: A2uiSurfaceState, options?: GraphIssueOptions): A2uiValidationIssue[];