@antglobal/copilot-cards-core 1.0.5 → 1.0.7

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/dist/index.js CHANGED
@@ -201,87 +201,106 @@ function evaluateExpression(expr, context) {
201
201
  return t;
202
202
  }
203
203
  // Precedence (low → high): ternary → || → && → equality → relational → unary → atom
204
- function parseTernary() {
205
- const cond = parseOr();
204
+ function parseTernary(mode) {
205
+ const condition = parseOr(mode);
206
206
  if (peek()?.type === 'question') {
207
207
  consume(); // ?
208
- const truthy = parseTernary();
208
+ const truthyMode = mode === 'evaluate' && Boolean(condition)
209
+ ? 'evaluate'
210
+ : 'consume';
211
+ const truthy = parseTernary(truthyMode);
209
212
  expect('colon');
210
- const falsy = parseTernary();
211
- return cond ? truthy : falsy;
213
+ const falsyMode = mode === 'evaluate' && !condition
214
+ ? 'evaluate'
215
+ : 'consume';
216
+ const falsy = parseTernary(falsyMode);
217
+ if (mode === 'consume')
218
+ return undefined;
219
+ return condition ? truthy : falsy;
212
220
  }
213
- return cond;
221
+ return condition;
214
222
  }
215
- function parseOr() {
216
- let left = parseAnd();
223
+ function parseOr(mode) {
224
+ let left = parseAnd(mode);
217
225
  while (peek()?.value === '||') {
218
226
  consume();
219
- left = left || parseAnd();
227
+ const rightMode = mode === 'evaluate' && !left ? 'evaluate' : 'consume';
228
+ const right = parseAnd(rightMode);
229
+ if (rightMode === 'evaluate')
230
+ left = right;
220
231
  }
221
232
  return left;
222
233
  }
223
- function parseAnd() {
224
- let left = parseEquality();
234
+ function parseAnd(mode) {
235
+ let left = parseEquality(mode);
225
236
  while (peek()?.value === '&&') {
226
237
  consume();
227
- left = left && parseEquality();
238
+ const rightMode = mode === 'evaluate' && Boolean(left) ? 'evaluate' : 'consume';
239
+ const right = parseEquality(rightMode);
240
+ if (rightMode === 'evaluate')
241
+ left = right;
228
242
  }
229
243
  return left;
230
244
  }
231
- function parseEquality() {
232
- let left = parseRelational();
245
+ function parseEquality(mode) {
246
+ let left = parseRelational(mode);
233
247
  while (peek()?.value === '===' || peek()?.value === '!==' || peek()?.value === '==' || peek()?.value === '!=') {
234
248
  const op = consume().value;
235
- const right = parseRelational();
236
- if (op === '===' || op === '==')
237
- left = left === right;
238
- else
239
- left = left !== right;
249
+ const right = parseRelational(mode);
250
+ if (mode === 'evaluate') {
251
+ if (op === '===' || op === '==')
252
+ left = left === right;
253
+ else
254
+ left = left !== right;
255
+ }
240
256
  }
241
257
  return left;
242
258
  }
243
- function parseRelational() {
244
- let left = parseUnary();
259
+ function parseRelational(mode) {
260
+ let left = parseUnary(mode);
245
261
  while (peek()?.value === '>' || peek()?.value === '<' || peek()?.value === '>=' || peek()?.value === '<=') {
246
262
  const op = consume().value;
247
- const right = parseUnary();
248
- if (op === '>')
249
- left = left > right;
250
- else if (op === '<')
251
- left = left < right;
252
- else if (op === '>=')
253
- left = left >= right;
254
- else
255
- left = left <= right;
263
+ const right = parseUnary(mode);
264
+ if (mode === 'evaluate') {
265
+ if (op === '>')
266
+ left = left > right;
267
+ else if (op === '<')
268
+ left = left < right;
269
+ else if (op === '>=')
270
+ left = left >= right;
271
+ else
272
+ left = left <= right;
273
+ }
256
274
  }
257
275
  return left;
258
276
  }
259
- function parseUnary() {
277
+ function parseUnary(mode) {
260
278
  if (peek()?.value === '!') {
261
279
  consume();
262
- return !parseUnary();
280
+ const value = parseUnary(mode);
281
+ return mode === 'evaluate' ? !value : undefined;
263
282
  }
264
- return parseAtom();
283
+ return parseAtom(mode);
265
284
  }
266
- function parseAtom() {
285
+ function parseAtom(mode) {
267
286
  const t = peek();
268
287
  if (!t)
269
288
  return undefined;
270
289
  if (t.type === 'string') {
271
290
  consume();
272
- return t.value;
291
+ return mode === 'evaluate' ? t.value : undefined;
273
292
  }
274
293
  if (t.type === 'number') {
275
294
  consume();
276
- return Number(t.value);
295
+ return mode === 'evaluate' ? Number(t.value) : undefined;
277
296
  }
278
297
  if (t.type === 'boolean') {
279
298
  consume();
280
- return t.value === 'true';
299
+ return mode === 'evaluate' ? t.value === 'true' : undefined;
281
300
  }
282
301
  if (t.type === 'null') {
283
302
  consume();
284
- return null;
303
+ return mode === 'evaluate' ? null : undefined;
285
304
  }
286
305
  if (t.type === 'undefined') {
287
306
  consume();
@@ -289,19 +308,19 @@ function evaluateExpression(expr, context) {
289
308
  }
290
309
  if (t.type === 'ident') {
291
310
  consume();
292
- return getByPath$1(context, t.value);
311
+ return mode === 'evaluate' ? getByPath$1(context, t.value) : undefined;
293
312
  }
294
313
  if (t.type === 'paren' && t.value === '(') {
295
314
  consume();
296
- const val = parseTernary();
315
+ const value = parseTernary(mode);
297
316
  expect('paren', ')');
298
- return val;
317
+ return value;
299
318
  }
300
319
  // Fallback
301
320
  consume();
302
321
  return undefined;
303
322
  }
304
- const result = parseTernary();
323
+ const result = parseTernary('evaluate');
305
324
  return result;
306
325
  }
307
326
  /**
@@ -386,7 +405,7 @@ async function parseResponseBody(response) {
386
405
  * @param silent - If true, only write to ctx.variables without calling
387
406
  * setVariable (avoids triggering re-render during polling iterations).
388
407
  * When variableWriter is present it owns the write and must synchronously
389
- * update the bound variables draft while honoring this flag.
408
+ * update the renderer-owned variable view while honoring this flag.
390
409
  */
391
410
  function writeResponseVariable(ctx, responseKey, data, silent = false) {
392
411
  if (ctx.variableWriter) {
@@ -1844,97 +1863,215 @@ function parseSchema(input) {
1844
1863
  }
1845
1864
  return buildNode(rootID);
1846
1865
  }
1847
- // ─── Validation ──────────────────────────────────────────────────
1866
+ function encodePointerSegment(segment) {
1867
+ return String(segment).replace(/~/g, '~0').replace(/\//g, '~1');
1868
+ }
1869
+ function schemaPath(...segments) {
1870
+ return segments.length > 0
1871
+ ? `/${segments.map(encodePointerSegment).join('/')}`
1872
+ : '';
1873
+ }
1874
+ function elementPath(id, ...segments) {
1875
+ return schemaPath('elements', id, ...segments);
1876
+ }
1877
+ function slotPath(id, slotName, ...segments) {
1878
+ return elementPath(id, 'props', 'slots', slotName, ...segments);
1879
+ }
1880
+ function addIssue(issues, code, path, message, params, anchorPath) {
1881
+ issues.push({
1882
+ code,
1883
+ path,
1884
+ ...(anchorPath === undefined ? {} : { anchorPath }),
1885
+ message,
1886
+ ...(params === undefined ? {} : { params }),
1887
+ });
1888
+ }
1889
+ function displayValue(value) {
1890
+ try {
1891
+ return String(value);
1892
+ }
1893
+ catch {
1894
+ return '<unprintable>';
1895
+ }
1896
+ }
1897
+ function displayError(error) {
1898
+ const message = readOwnData(error, 'message');
1899
+ return displayValue(message.kind === 'value' ? message.value : error);
1900
+ }
1848
1901
  /**
1849
- * Validate a CardSchema and return any error messages.
1902
+ * Validate an unknown schema input and return structured diagnostics.
1903
+ *
1904
+ * Component names and component-specific props are deliberately not checked;
1905
+ * this validator only owns the shared card graph and binding protocol.
1850
1906
  */
1851
- function validateSchema(input) {
1852
- const schema = normalizeSchema(input);
1853
- const errors = [];
1854
- if (!schema.version) {
1855
- errors.push('Missing "version" field');
1907
+ function validateSchemaDetailed(input) {
1908
+ const issues = [];
1909
+ if (!isPlainObject(input)) {
1910
+ addIssue(issues, 'SCHEMA_TYPE_MISMATCH', '', 'Schema must be a non-null plain object');
1911
+ return issues;
1912
+ }
1913
+ let schema;
1914
+ try {
1915
+ schema = normalizeSchema(input);
1856
1916
  }
1857
- if (typeof schema.rootID !== 'string') {
1858
- errors.push('"rootID" field must be a string');
1917
+ catch (error) {
1918
+ addIssue(issues, 'LEGACY_CONVERSION_FAILED', '', `Legacy schema conversion failed: ${displayError(error)}`);
1919
+ return issues;
1859
1920
  }
1860
- else if (!schema.rootID) {
1861
- errors.push('Missing "rootID" field');
1921
+ const version = readOwnData(schema, 'version');
1922
+ if (version.kind !== 'value' || !version.value) {
1923
+ addIssue(issues, 'SCHEMA_REQUIRED_FIELD', schemaPath('version'), 'Missing "version" field', undefined, '');
1862
1924
  }
1863
- else if (!schema.elements[schema.rootID]) {
1864
- errors.push(`Root element "${schema.rootID}" not found in elements`);
1925
+ const rootID = readOwnData(schema, 'rootID');
1926
+ if (rootID.kind !== 'value' || typeof rootID.value !== 'string') {
1927
+ addIssue(issues, 'ROOT_TYPE_MISMATCH', schemaPath('rootID'), '"rootID" field must be a string', undefined, rootID.kind === 'missing' ? '' : undefined);
1865
1928
  }
1866
- const allIds = new Set(Object.keys(schema.elements));
1867
- for (const [id, element] of Object.entries(schema.elements)) {
1868
- if (!element.type) {
1869
- errors.push(`Element "${id}" is missing a "type" field`);
1929
+ else if (!rootID.value) {
1930
+ addIssue(issues, 'SCHEMA_REQUIRED_FIELD', schemaPath('rootID'), 'Missing "rootID" field');
1931
+ }
1932
+ const elementsRead = readOwnData(schema, 'elements');
1933
+ if (elementsRead.kind === 'missing') {
1934
+ addIssue(issues, 'SCHEMA_REQUIRED_FIELD', schemaPath('elements'), 'Missing "elements" field', undefined, '');
1935
+ return issues;
1936
+ }
1937
+ const rawElements = elementsRead.kind === 'value'
1938
+ ? elementsRead.value
1939
+ : undefined;
1940
+ if (!isPlainObject(rawElements)) {
1941
+ addIssue(issues, 'SCHEMA_TYPE_MISMATCH', schemaPath('elements'), '"elements" field must be an object');
1942
+ return issues;
1943
+ }
1944
+ const elements = rawElements;
1945
+ const elementEntries = ownStringDataEntries(elements);
1946
+ for (const id of elementEntries.accessors) {
1947
+ addIssue(issues, 'ELEMENT_ACCESSOR_NOT_ALLOWED', elementPath(id), `Element "${id}" must be an own data property`, { elementId: id });
1948
+ }
1949
+ if (elementEntries.opaque) {
1950
+ addIssue(issues, 'ELEMENTS_UNINSPECTABLE', schemaPath('elements'), 'The "elements" field could not be inspected safely');
1951
+ return issues;
1952
+ }
1953
+ const allIds = new Set([
1954
+ ...elementEntries.entries.map(([id]) => id),
1955
+ ...elementEntries.accessors,
1956
+ ]);
1957
+ const validationElements = Object.create(null);
1958
+ for (const [id, element] of elementEntries.entries) {
1959
+ Object.defineProperty(validationElements, id, {
1960
+ configurable: true,
1961
+ enumerable: true,
1962
+ value: element,
1963
+ writable: true,
1964
+ });
1965
+ }
1966
+ const validationSchema = {
1967
+ rootID: rootID.kind === 'value' && typeof rootID.value === 'string'
1968
+ ? rootID.value
1969
+ : '',
1970
+ elements: validationElements};
1971
+ if (rootID.kind === 'value'
1972
+ && typeof rootID.value === 'string'
1973
+ && rootID.value
1974
+ && !allIds.has(rootID.value)) {
1975
+ addIssue(issues, 'ROOT_REFERENCE_NOT_FOUND', schemaPath('rootID'), `Root element "${rootID.value}" not found in elements`, { rootID: rootID.value });
1976
+ }
1977
+ for (const [id, rawElement] of elementEntries.entries) {
1978
+ if (!isPlainObject(rawElement)) {
1979
+ addIssue(issues, 'ELEMENT_TYPE_MISMATCH', elementPath(id), `Element "${id}" must be an object`, { elementId: id });
1980
+ continue;
1981
+ }
1982
+ const element = rawElement;
1983
+ const type = readOwnData(element, 'type');
1984
+ if (type.kind !== 'value' || !type.value) {
1985
+ addIssue(issues, 'ELEMENT_TYPE_MISSING', elementPath(id, 'type'), `Element "${id}" is missing a "type" field`, { elementId: id }, type.kind === 'missing' ? elementPath(id) : undefined);
1986
+ }
1987
+ const props = readOwnData(element, 'props');
1988
+ if (props.kind !== 'value' || !isPlainObject(props.value)) {
1989
+ addIssue(issues, 'ELEMENT_PROPS_TYPE_MISMATCH', elementPath(id, 'props'), `Element "${id}" "props" field must be an object`, { elementId: id }, props.kind === 'missing' ? elementPath(id) : undefined);
1990
+ continue;
1870
1991
  }
1871
1992
  // Validate slot children and groups references without invoking accessors.
1872
- const slots = elementSlotEntries(element);
1993
+ const rawSlots = readOwnData(props.value, 'slots');
1994
+ if (rawSlots.kind === 'accessor' || rawSlots.kind === 'opaque') {
1995
+ addIssue(issues, 'SLOTS_UNINSPECTABLE', elementPath(id, 'props', 'slots'), `Element "${id}" slots could not be inspected safely`, { elementId: id });
1996
+ continue;
1997
+ }
1998
+ if (rawSlots.kind === 'value' && !isPlainObject(rawSlots.value)) {
1999
+ addIssue(issues, 'SLOTS_TYPE_MISMATCH', elementPath(id, 'props', 'slots'), `Element "${id}" "slots" field must be an object`, { elementId: id });
2000
+ continue;
2001
+ }
2002
+ const slots = rawSlots.kind === 'value'
2003
+ ? ownStringDataEntries(rawSlots.value)
2004
+ : ownStringDataEntries(undefined);
1873
2005
  for (const slotName of slots.accessors) {
1874
- errors.push(`Element "${id}" slot "${slotName}" must be an own data property`);
2006
+ addIssue(issues, 'SLOT_ACCESSOR_NOT_ALLOWED', slotPath(id, slotName), `Element "${id}" slot "${slotName}" must be an own data property`, { elementId: id, slotName });
1875
2007
  }
1876
2008
  if (slots.opaque) {
1877
- errors.push(`Element "${id}" slots could not be inspected safely`);
2009
+ addIssue(issues, 'SLOTS_UNINSPECTABLE', elementPath(id, 'props', 'slots'), `Element "${id}" slots could not be inspected safely`, { elementId: id });
1878
2010
  }
1879
2011
  for (const [slotName, slot] of slots.entries) {
1880
- if (!slot || typeof slot !== 'object') {
1881
- errors.push(`Element "${id}" slot "${slotName}" must be an object`);
2012
+ if (!isPlainObject(slot)) {
2013
+ addIssue(issues, 'SLOT_TYPE_MISMATCH', slotPath(id, slotName), `Element "${id}" slot "${slotName}" must be an object`, { elementId: id, slotName });
1882
2014
  continue;
1883
2015
  }
1884
2016
  const children = readOwnData(slot, 'children');
1885
2017
  if (children.kind === 'value') {
1886
2018
  const values = ownArrayDataValues(children.value);
1887
2019
  if (!values) {
1888
- errors.push(`Element "${id}" slot "${slotName}" children must be an array`);
2020
+ addIssue(issues, 'SLOT_CHILDREN_TYPE_MISMATCH', slotPath(id, slotName, 'children'), `Element "${id}" slot "${slotName}" children must be an array`, { elementId: id, slotName });
1889
2021
  }
1890
2022
  else {
1891
- for (const childId of values) {
2023
+ for (const [index, childId] of values.entries()) {
1892
2024
  if (typeof childId !== 'string' || childId.length === 0) {
1893
- errors.push(`Element "${id}" slot "${slotName}" child IDs must be non-empty strings; received "${String(childId)}"`);
2025
+ addIssue(issues, 'SLOT_CHILD_ID_INVALID', slotPath(id, slotName, 'children', index), `Element "${id}" slot "${slotName}" child IDs must be non-empty strings; received "${displayValue(childId)}"`, { elementId: id, slotName, value: childId });
1894
2026
  }
1895
2027
  else if (!allIds.has(childId)) {
1896
- errors.push(`Element "${id}" slot "${slotName}" references unknown child "${String(childId)}"`);
2028
+ addIssue(issues, 'ELEMENT_REFERENCE_NOT_FOUND', slotPath(id, slotName, 'children', index), `Element "${id}" slot "${slotName}" references unknown child "${displayValue(childId)}"`, { elementId: id, slotName, reference: childId });
1897
2029
  }
1898
2030
  }
1899
2031
  }
1900
2032
  }
1901
2033
  else if (children.kind === 'accessor' || children.kind === 'opaque') {
1902
- errors.push(`Element "${id}" slot "${slotName}" children must be an own data property`);
2034
+ addIssue(issues, 'SLOT_ACCESSOR_NOT_ALLOWED', slotPath(id, slotName, 'children'), `Element "${id}" slot "${slotName}" children must be an own data property`, { elementId: id, slotName, field: 'children' });
1903
2035
  }
1904
2036
  const groups = readOwnData(slot, 'groups');
1905
2037
  if (groups.kind === 'value') {
1906
2038
  const groupValues = ownArrayDataValues(groups.value);
1907
2039
  if (!groupValues) {
1908
- errors.push(`Element "${id}" slot "${slotName}" groups must be an array`);
2040
+ addIssue(issues, 'SLOT_GROUPS_TYPE_MISMATCH', slotPath(id, slotName, 'groups'), `Element "${id}" slot "${slotName}" groups must be an array`, { elementId: id, slotName });
1909
2041
  }
1910
2042
  else {
1911
- for (const group of groupValues) {
2043
+ for (const [groupIndex, group] of groupValues.entries()) {
1912
2044
  const childrenInGroup = ownArrayDataValues(group);
1913
2045
  if (!childrenInGroup) {
1914
- errors.push(`Element "${id}" slot "${slotName}" groups must contain arrays`);
2046
+ addIssue(issues, 'SLOT_GROUP_TYPE_MISMATCH', slotPath(id, slotName, 'groups', groupIndex), `Element "${id}" slot "${slotName}" groups must contain arrays`, { elementId: id, slotName });
1915
2047
  continue;
1916
2048
  }
1917
- for (const childId of childrenInGroup) {
2049
+ for (const [childIndex, childId] of childrenInGroup.entries()) {
1918
2050
  if (typeof childId !== 'string' || childId.length === 0) {
1919
- errors.push(`Element "${id}" slot "${slotName}" group child IDs must be non-empty strings; received "${String(childId)}"`);
2051
+ addIssue(issues, 'SLOT_GROUP_CHILD_ID_INVALID', slotPath(id, slotName, 'groups', groupIndex, childIndex), `Element "${id}" slot "${slotName}" group child IDs must be non-empty strings; received "${displayValue(childId)}"`, { elementId: id, slotName, value: childId });
1920
2052
  }
1921
2053
  else if (!allIds.has(childId)) {
1922
- errors.push(`Element "${id}" slot "${slotName}" group references unknown child "${String(childId)}"`);
2054
+ addIssue(issues, 'ELEMENT_REFERENCE_NOT_FOUND', slotPath(id, slotName, 'groups', groupIndex, childIndex), `Element "${id}" slot "${slotName}" group references unknown child "${displayValue(childId)}"`, { elementId: id, slotName, reference: childId });
1923
2055
  }
1924
2056
  }
1925
2057
  }
1926
2058
  }
1927
2059
  }
1928
2060
  else if (groups.kind === 'accessor' || groups.kind === 'opaque') {
1929
- errors.push(`Element "${id}" slot "${slotName}" groups must be an own data property`);
2061
+ addIssue(issues, 'SLOT_ACCESSOR_NOT_ALLOWED', slotPath(id, slotName, 'groups'), `Element "${id}" slot "${slotName}" groups must be an own data property`, { elementId: id, slotName, field: 'groups' });
1930
2062
  }
1931
2063
  }
1932
2064
  }
1933
- validateRepeatBindings(schema, errors);
1934
- return errors;
2065
+ validateRepeatBindings(validationSchema, issues);
2066
+ return issues;
2067
+ }
2068
+ /** Validate a CardSchema and return backwards-compatible error messages. */
2069
+ function validateSchema(input) {
2070
+ return validateSchemaDetailed(input).map(issue => issue.message);
1935
2071
  }
1936
2072
  const REPEAT_SLOT_LAYOUTS = new Set([
1937
2073
  'default',
2074
+ 'flex',
1938
2075
  'list',
1939
2076
  'grid',
1940
2077
  'horizontalScroll',
@@ -2032,7 +2169,10 @@ function ownArrayDataValues(value) {
2032
2169
  }
2033
2170
  }
2034
2171
  function elementSlotEntries(element) {
2035
- const slots = readOwnData(element.props, 'slots');
2172
+ const props = readOwnData(element, 'props');
2173
+ const slots = props.kind === 'value'
2174
+ ? readOwnData(props.value, 'slots')
2175
+ : props;
2036
2176
  return slots.kind === 'value'
2037
2177
  ? ownStringDataEntries(slots.value)
2038
2178
  : {
@@ -2048,11 +2188,15 @@ function readRepeatBindingFields(value) {
2048
2188
  const template = readOwnData(value, 'template');
2049
2189
  const item = readOwnData(value, 'item');
2050
2190
  const index = readOwnData(value, 'index');
2191
+ const emptyTemplate = readOwnData(value, 'emptyTemplate');
2051
2192
  return {
2052
2193
  source: source.kind === 'value' ? source.value : undefined,
2053
2194
  template: template.kind === 'value' ? template.value : undefined,
2054
- item: item.kind === 'value' ? item.value : undefined,
2195
+ ...(item.kind === 'value' ? { item: item.value } : {}),
2055
2196
  ...(index.kind === 'value' ? { index: index.value } : {}),
2197
+ ...(emptyTemplate.kind === 'value'
2198
+ ? { emptyTemplate: emptyTemplate.value }
2199
+ : {}),
2056
2200
  };
2057
2201
  }
2058
2202
  function slotStaticReferences(slot) {
@@ -2177,6 +2321,10 @@ function elementReferences(element) {
2177
2321
  if (typeof binding?.template === 'string' && binding.template) {
2178
2322
  references.push(binding.template);
2179
2323
  }
2324
+ if (typeof binding?.emptyTemplate === 'string'
2325
+ && binding.emptyTemplate) {
2326
+ references.push(binding.emptyTemplate);
2327
+ }
2180
2328
  const a2ui = getA2UIChildBinding(slot);
2181
2329
  if (typeof a2ui?.templateId === 'string' && a2ui.templateId) {
2182
2330
  references.push(a2ui.templateId);
@@ -2184,14 +2332,14 @@ function elementReferences(element) {
2184
2332
  }
2185
2333
  return references;
2186
2334
  }
2187
- function validateRepeatBindings(schema, errors) {
2335
+ function validateRepeatBindings(schema, issues) {
2188
2336
  const dynamicBindings = [];
2189
2337
  for (const [id, element] of Object.entries(schema.elements)) {
2190
2338
  const elementRepeats = elementSlotEntries(element).entries
2191
2339
  .filter((entry) => hasOwn(entry[1], 'repeat')
2192
2340
  || getA2UIChildBinding(entry[1]) !== undefined);
2193
2341
  if (elementRepeats.length > 1) {
2194
- errors.push(`Element "${id}" has more than one dynamic slot`);
2342
+ addIssue(issues, 'DYNAMIC_SLOT_MULTIPLE', elementPath(id, 'props', 'slots'), `Element "${id}" has more than one dynamic slot`, { elementId: id });
2195
2343
  }
2196
2344
  for (const [slotName, slot] of elementRepeats) {
2197
2345
  const a2ui = getA2UIChildBinding(slot);
@@ -2205,10 +2353,10 @@ function validateRepeatBindings(schema, errors) {
2205
2353
  });
2206
2354
  if (typeof a2ui.templateId !== 'string'
2207
2355
  || a2ui.templateId.length === 0) {
2208
- errors.push(`Element "${id}" slot "${slotName}" dynamic children template must be a non-empty string`);
2356
+ addIssue(issues, 'DYNAMIC_TEMPLATE_INVALID', slotPath(id, slotName), `Element "${id}" slot "${slotName}" dynamic children template must be a non-empty string`, { elementId: id, slotName });
2209
2357
  }
2210
2358
  else if (!getOwnElement(schema, a2ui.templateId)) {
2211
- errors.push(`Element "${id}" slot "${slotName}" dynamic children reference unknown template "${a2ui.templateId}"`);
2359
+ addIssue(issues, 'DYNAMIC_TEMPLATE_NOT_FOUND', slotPath(id, slotName), `Element "${id}" slot "${slotName}" dynamic children reference unknown template "${a2ui.templateId}"`, { elementId: id, slotName, reference: a2ui.templateId });
2212
2360
  }
2213
2361
  continue;
2214
2362
  }
@@ -2216,7 +2364,7 @@ function validateRepeatBindings(schema, errors) {
2216
2364
  ? repeat.value
2217
2365
  : undefined;
2218
2366
  if (!isPlainObject(rawBinding)) {
2219
- errors.push(`Element "${id}" slot "${slotName}" repeat must be a non-null plain object`);
2367
+ addIssue(issues, 'REPEAT_TYPE_MISMATCH', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" repeat must be a non-null plain object`, { elementId: id, slotName });
2220
2368
  continue;
2221
2369
  }
2222
2370
  const binding = readRepeatBindingFields(rawBinding);
@@ -2228,7 +2376,7 @@ function validateRepeatBindings(schema, errors) {
2228
2376
  binding,
2229
2377
  });
2230
2378
  if (!REPEAT_SLOT_LAYOUTS.has(slotName)) {
2231
- errors.push(`Element "${id}" slot "${slotName}" does not support repeat`);
2379
+ addIssue(issues, 'REPEAT_SLOT_UNSUPPORTED', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" does not support repeat`, { elementId: id, slotName });
2232
2380
  }
2233
2381
  const children = readOwnData(slot, 'children');
2234
2382
  const groups = readOwnData(slot, 'groups');
@@ -2238,49 +2386,78 @@ function validateRepeatBindings(schema, errors) {
2238
2386
  || (groups.kind === 'value' && !!groups.value)
2239
2387
  || groups.kind === 'accessor'
2240
2388
  || groups.kind === 'opaque') {
2241
- errors.push(`Element "${id}" slot "${slotName}" cannot combine repeat with children or groups`);
2389
+ addIssue(issues, 'REPEAT_CONTENT_CONFLICT', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" cannot combine repeat with children or groups`, { elementId: id, slotName });
2242
2390
  }
2243
2391
  if (typeof binding.source !== 'string') {
2244
- errors.push(`Element "${id}" slot "${slotName}" repeat source must be a string`);
2392
+ addIssue(issues, 'REPEAT_SOURCE_TYPE_MISMATCH', slotPath(id, slotName, 'repeat', 'source'), `Element "${id}" slot "${slotName}" repeat source must be a string`, { elementId: id, slotName }, hasOwn(rawBinding, 'source')
2393
+ ? undefined
2394
+ : slotPath(id, slotName, 'repeat'));
2245
2395
  }
2246
2396
  else if (!isCompleteExpression(binding.source)) {
2247
- errors.push(`Element "${id}" slot "${slotName}" repeat source must be one complete non-empty \${...} expression`);
2397
+ addIssue(issues, 'REPEAT_SOURCE_INVALID', slotPath(id, slotName, 'repeat', 'source'), `Element "${id}" slot "${slotName}" repeat source must be one complete non-empty \${...} expression`, { elementId: id, slotName });
2248
2398
  }
2249
2399
  if (typeof binding.template !== 'string') {
2250
- errors.push(`Element "${id}" slot "${slotName}" repeat template must be a string`);
2400
+ addIssue(issues, 'REPEAT_TEMPLATE_TYPE_MISMATCH', slotPath(id, slotName, 'repeat', 'template'), `Element "${id}" slot "${slotName}" repeat template must be a string`, { elementId: id, slotName }, hasOwn(rawBinding, 'template')
2401
+ ? undefined
2402
+ : slotPath(id, slotName, 'repeat'));
2251
2403
  }
2252
2404
  else if (!binding.template) {
2253
- errors.push(`Element "${id}" slot "${slotName}" repeat template must be a non-empty string`);
2405
+ addIssue(issues, 'REPEAT_TEMPLATE_INVALID', slotPath(id, slotName, 'repeat', 'template'), `Element "${id}" slot "${slotName}" repeat template must be a non-empty string`, { elementId: id, slotName });
2254
2406
  }
2255
2407
  else if (!getOwnElement(schema, binding.template)) {
2256
- errors.push(`Element "${id}" slot "${slotName}" repeat references unknown template "${binding.template}"`);
2408
+ addIssue(issues, 'REPEAT_TEMPLATE_NOT_FOUND', slotPath(id, slotName, 'repeat', 'template'), `Element "${id}" slot "${slotName}" repeat references unknown template "${binding.template}"`, { elementId: id, slotName, reference: binding.template });
2409
+ }
2410
+ if (binding.item !== undefined) {
2411
+ validateAlias(id, slotName, 'item', binding.item, issues);
2257
2412
  }
2258
- validateAlias(id, slotName, 'item', binding.item, errors);
2259
2413
  if (binding.index !== undefined) {
2260
- validateAlias(id, slotName, 'index', binding.index, errors);
2414
+ validateAlias(id, slotName, 'index', binding.index, issues);
2261
2415
  }
2262
- if (binding.index !== undefined && binding.item === binding.index) {
2263
- errors.push(`Element "${id}" slot "${slotName}" repeat item and index aliases must be different`);
2416
+ if (binding.index !== undefined
2417
+ && (binding.item ?? '$item') === binding.index) {
2418
+ addIssue(issues, 'REPEAT_ALIAS_COLLISION', slotPath(id, slotName, 'repeat', 'index'), `Element "${id}" slot "${slotName}" repeat item and index aliases must be different`, { elementId: id, slotName, alias: binding.index });
2419
+ }
2420
+ if (binding.emptyTemplate !== undefined) {
2421
+ if (typeof binding.emptyTemplate !== 'string') {
2422
+ addIssue(issues, 'REPEAT_EMPTY_TEMPLATE_TYPE_MISMATCH', slotPath(id, slotName, 'repeat', 'emptyTemplate'), `Element "${id}" slot "${slotName}" repeat emptyTemplate must be a string`, { elementId: id, slotName });
2423
+ }
2424
+ else if (!binding.emptyTemplate) {
2425
+ addIssue(issues, 'REPEAT_EMPTY_TEMPLATE_INVALID', slotPath(id, slotName, 'repeat', 'emptyTemplate'), `Element "${id}" slot "${slotName}" repeat emptyTemplate must be a non-empty string`, { elementId: id, slotName });
2426
+ }
2427
+ else if (!getOwnElement(schema, binding.emptyTemplate)) {
2428
+ addIssue(issues, 'REPEAT_EMPTY_TEMPLATE_NOT_FOUND', slotPath(id, slotName, 'repeat', 'emptyTemplate'), `Element "${id}" slot "${slotName}" repeat references unknown empty template "${binding.emptyTemplate}"`, { elementId: id, slotName, reference: binding.emptyTemplate });
2429
+ }
2264
2430
  }
2265
2431
  }
2266
2432
  }
2267
- validateRepeatNesting(schema, dynamicBindings, errors);
2268
- validateDynamicClosures(schema, dynamicBindings, errors);
2433
+ validateRepeatNesting(schema, dynamicBindings, issues);
2434
+ validateDynamicClosures(schema, dynamicBindings, issues);
2269
2435
  }
2270
- function validateAlias(owner, slotName, kind, alias, errors) {
2436
+ function validateAlias(owner, slotName, kind, alias, issues) {
2271
2437
  if (typeof alias !== 'string') {
2272
- errors.push(`Element "${owner}" slot "${slotName}" repeat ${kind} must be a string`);
2438
+ addIssue(issues, 'REPEAT_ALIAS_TYPE_MISMATCH', slotPath(owner, slotName, 'repeat', kind), `Element "${owner}" slot "${slotName}" repeat ${kind} must be a string`, { elementId: owner, slotName, field: kind });
2273
2439
  }
2274
2440
  else if (!ALIAS_PATTERN.test(alias)) {
2275
- errors.push(`Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is invalid`);
2441
+ addIssue(issues, 'REPEAT_ALIAS_INVALID', slotPath(owner, slotName, 'repeat', kind), `Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is invalid`, { elementId: owner, slotName, field: kind, alias });
2276
2442
  }
2277
2443
  else if (RESERVED_ALIASES.has(alias)) {
2278
- errors.push(`Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is reserved`);
2444
+ addIssue(issues, 'REPEAT_ALIAS_RESERVED', slotPath(owner, slotName, 'repeat', kind), `Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is reserved`, { elementId: owner, slotName, field: kind, alias });
2279
2445
  }
2280
2446
  }
2281
- function validateRepeatNesting(schema, bindings, errors) {
2447
+ function validateRepeatNesting(schema, bindings, issues) {
2282
2448
  const reportedCycles = new Set();
2283
2449
  const reportedCollisions = new Set();
2450
+ function effectiveAliases(binding) {
2451
+ if (!binding)
2452
+ return [];
2453
+ if (binding.item !== undefined) {
2454
+ return [binding.item, binding.index].filter((alias) => typeof alias === 'string');
2455
+ }
2456
+ return [
2457
+ '$item',
2458
+ binding.index === undefined ? '$index' : binding.index,
2459
+ ].filter((alias) => typeof alias === 'string');
2460
+ }
2284
2461
  function visitElement(id, activeAliases, repeatPath, visitedStatic) {
2285
2462
  const element = getOwnElement(schema, id);
2286
2463
  if (!element || visitedStatic.has(id))
@@ -2295,26 +2472,31 @@ function validateRepeatNesting(schema, bindings, errors) {
2295
2472
  ? readRepeatBindingFields(repeat.value)
2296
2473
  : undefined;
2297
2474
  const a2uiBinding = getA2UIChildBinding(slot);
2298
- const template = nativeBinding?.template ?? a2uiBinding?.templateId;
2299
- if (typeof template === 'string' && template) {
2475
+ const itemTemplate = nativeBinding?.template ?? a2uiBinding?.templateId;
2476
+ const visitDynamicTemplate = (template, aliases) => {
2477
+ if (!template)
2478
+ return;
2300
2479
  const cycleAt = repeatPath.indexOf(template);
2301
2480
  if (cycleAt >= 0) {
2302
2481
  const cycle = [...repeatPath.slice(cycleAt), template].join(' -> ');
2303
2482
  if (!reportedCycles.has(cycle)) {
2304
2483
  reportedCycles.add(cycle);
2305
- errors.push(nativeBinding
2484
+ addIssue(issues, nativeBinding
2485
+ ? 'REPEAT_TEMPLATE_CYCLE'
2486
+ : 'DYNAMIC_TEMPLATE_CYCLE', nativeBinding
2487
+ ? slotPath(id, slotName, 'repeat', 'template')
2488
+ : slotPath(id, slotName), nativeBinding
2306
2489
  ? `Element "${id}" slot "${slotName}" has repeat template cycle: ${cycle}`
2307
- : `Element "${id}" slot "${slotName}" has dynamic template cycle: ${cycle}`);
2490
+ : `Element "${id}" slot "${slotName}" has dynamic template cycle: ${cycle}`, { elementId: id, slotName, cycle });
2308
2491
  }
2309
- continue;
2492
+ return;
2310
2493
  }
2311
- const aliases = [nativeBinding?.item, nativeBinding?.index].filter((alias) => typeof alias === 'string');
2312
2494
  for (const alias of aliases) {
2313
2495
  if (activeAliases.has(alias)) {
2314
2496
  const key = `${id}:${slotName}:${alias}`;
2315
2497
  if (!reportedCollisions.has(key)) {
2316
2498
  reportedCollisions.add(key);
2317
- errors.push(`Element "${id}" slot "${slotName}" repeat alias "${alias}" conflicts with an active repeat alias`);
2499
+ addIssue(issues, 'REPEAT_ALIAS_COLLISION', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" repeat alias "${alias}" conflicts with an active repeat alias`, { elementId: id, slotName, alias });
2318
2500
  }
2319
2501
  }
2320
2502
  }
@@ -2322,7 +2504,9 @@ function validateRepeatNesting(schema, bindings, errors) {
2322
2504
  for (const alias of aliases)
2323
2505
  nestedAliases.add(alias);
2324
2506
  visitElement(template, nestedAliases, [...repeatPath, template], new Set());
2325
- }
2507
+ };
2508
+ visitDynamicTemplate(itemTemplate, effectiveAliases(nativeBinding));
2509
+ visitDynamicTemplate(nativeBinding?.emptyTemplate, []);
2326
2510
  for (const child of slotStaticReferences(slot)) {
2327
2511
  visitElement(child, activeAliases, repeatPath, nextVisitedStatic);
2328
2512
  }
@@ -2330,13 +2514,17 @@ function validateRepeatNesting(schema, bindings, errors) {
2330
2514
  }
2331
2515
  for (const dynamic of bindings) {
2332
2516
  const { owner, binding, template } = dynamic;
2333
- const aliases = new Set([binding?.item, binding?.index].filter((alias) => typeof alias === 'string'));
2517
+ const aliases = new Set(effectiveAliases(binding));
2334
2518
  if (typeof template === 'string' && template) {
2335
2519
  visitElement(template, aliases, [owner, template], new Set());
2336
2520
  }
2521
+ if (typeof binding?.emptyTemplate === 'string'
2522
+ && binding.emptyTemplate) {
2523
+ visitElement(binding.emptyTemplate, new Set(), [owner, binding.emptyTemplate], new Set());
2524
+ }
2337
2525
  }
2338
2526
  }
2339
- function validateDynamicClosures(schema, repeats, errors) {
2527
+ function validateDynamicClosures(schema, repeats, issues) {
2340
2528
  for (const { owner } of repeats) {
2341
2529
  const visited = new Set();
2342
2530
  function visit(id) {
@@ -2350,10 +2538,11 @@ function validateDynamicClosures(schema, repeats, errors) {
2350
2538
  if ((lifecycle.kind === 'value' && !!lifecycle.value)
2351
2539
  || lifecycle.kind === 'accessor'
2352
2540
  || lifecycle.kind === 'opaque') {
2353
- errors.push(`Element "${id}" in dynamic patch closure owned by "${owner}" cannot use lifecycle`);
2541
+ addIssue(issues, 'DYNAMIC_LIFECYCLE_UNSUPPORTED', elementPath(id, 'lifecycle'), `Element "${id}" in dynamic patch closure owned by "${owner}" cannot use lifecycle`, { elementId: id, owner });
2354
2542
  }
2355
- if (hasOwn(element.props, 'variableKey')) {
2356
- errors.push(`Element "${id}" in dynamic patch closure owned by "${owner}" cannot use props.variableKey`);
2543
+ const props = readOwnData(element, 'props');
2544
+ if (props.kind === 'value' && hasOwn(props.value, 'variableKey')) {
2545
+ addIssue(issues, 'DYNAMIC_VARIABLE_KEY_UNSUPPORTED', elementPath(id, 'props', 'variableKey'), `Element "${id}" in dynamic patch closure owned by "${owner}" cannot use props.variableKey`, { elementId: id, owner });
2357
2546
  }
2358
2547
  for (const reference of elementReferences(element))
2359
2548
  visit(reference);
@@ -2880,6 +3069,7 @@ function materializeCard(input, variables, options = {}) {
2880
3069
  template: native.template,
2881
3070
  item: native.item,
2882
3071
  index: native.index,
3072
+ emptyTemplate: native.emptyTemplate,
2883
3073
  };
2884
3074
  }
2885
3075
  else {
@@ -2907,6 +3097,9 @@ function materializeCard(input, variables, options = {}) {
2907
3097
  sourceOwnerId: sourceId,
2908
3098
  slot: slotName,
2909
3099
  templateId: binding.template,
3100
+ ...(binding.emptyTemplate
3101
+ ? { emptyTemplateId: binding.emptyTemplate }
3102
+ : {}),
2910
3103
  ...(sourcePath === undefined ? {} : { sourcePath }),
2911
3104
  dataPath: activeScope.dataPath,
2912
3105
  instancePath,
@@ -2918,6 +3111,9 @@ function materializeCard(input, variables, options = {}) {
2918
3111
  repeatOwners.set(ownerKey, owner);
2919
3112
  slotOwners.set(slotName, { owner, binding, sourcePath, depth });
2920
3113
  addDependency('component', binding.template, ownerKey);
3114
+ if (binding.emptyTemplate) {
3115
+ addDependency('component', binding.emptyTemplate, ownerKey);
3116
+ }
2921
3117
  const runtimeKeys = runtimeOwnerKeys.get(id) ?? [];
2922
3118
  if (runtimeKeys.length > 0) {
2923
3119
  for (const key of runtimeKeys)
@@ -2998,15 +3194,30 @@ function materializeCard(input, variables, options = {}) {
2998
3194
  Reflect.deleteProperty(slot, 'repeat');
2999
3195
  Reflect.deleteProperty(slot, A2UI_CHILD_BINDING);
3000
3196
  slot.children = [];
3197
+ if (items.length === 0
3198
+ && binding.dialect === 'native'
3199
+ && binding.emptyTemplate) {
3200
+ const emptyPath = appendOccurrence(structuralPath, 'item', sourceId, slotName, `${binding.emptyTemplate}:empty`);
3201
+ const empty = buildNode(binding.emptyTemplate, activeScope, emptyPath, emptyPath, true, id, depth, owner.key, nextActiveDefinitions);
3202
+ slot.children.push(empty.id);
3203
+ node.children.push(empty);
3204
+ }
3001
3205
  items.forEach((item, index) => {
3002
3206
  const dataPath = sourcePath === undefined
3003
3207
  ? activeScope.dataPath
3004
3208
  : joinPointer(sourcePath, [String(index)]);
3005
- const locals = binding.dialect === 'native' && binding.item
3006
- ? {
3007
- [binding.item]: item,
3008
- ...(binding.index ? { [binding.index]: index } : {}),
3009
- }
3209
+ const locals = binding.dialect === 'native'
3210
+ ? binding.item !== undefined
3211
+ ? {
3212
+ [binding.item]: item,
3213
+ ...(binding.index === undefined
3214
+ ? {}
3215
+ : { [binding.index]: index }),
3216
+ }
3217
+ : {
3218
+ $item: item,
3219
+ [binding.index ?? '$index']: index,
3220
+ }
3010
3221
  : {};
3011
3222
  const childScope = {
3012
3223
  root: rootVariables,
@@ -3016,14 +3227,23 @@ function materializeCard(input, variables, options = {}) {
3016
3227
  bindingDialect: binding.dialect,
3017
3228
  };
3018
3229
  const aliases = new Map();
3019
- if (binding.dialect === 'native' && binding.item) {
3020
- aliases.set(binding.item, sourcePath === undefined ? undefined : dataPath);
3021
- if (binding.index)
3022
- aliases.set(binding.index, sourcePath);
3230
+ if (binding.dialect === 'native') {
3231
+ const itemAlias = binding.item ?? '$item';
3232
+ const indexAlias = binding.item === undefined
3233
+ ? binding.index ?? '$index'
3234
+ : binding.index;
3235
+ aliases.set(itemAlias, sourcePath === undefined ? undefined : dataPath);
3236
+ if (indexAlias !== undefined) {
3237
+ aliases.set(indexAlias, sourcePath);
3238
+ }
3023
3239
  }
3024
3240
  scopeAliasPaths.set(childScope, aliases);
3025
- scopeIndexAliases.set(childScope, new Set(binding.dialect === 'native' && binding.index
3026
- ? [binding.index]
3241
+ scopeIndexAliases.set(childScope, new Set(binding.dialect === 'native'
3242
+ ? binding.item === undefined
3243
+ ? [binding.index ?? '$index']
3244
+ : binding.index === undefined
3245
+ ? []
3246
+ : [binding.index]
3027
3247
  : []));
3028
3248
  const itemPath = appendOccurrence(structuralPath, 'item', sourceId, slotName, `${binding.template}:${index}`);
3029
3249
  const child = buildNode(binding.template, childScope, itemPath, itemPath, true, id, depth, owner.key, nextActiveDefinitions);
@@ -4749,4 +4969,4 @@ function getBuiltinIcon(name) {
4749
4969
  return BUILTIN_ICONS[name];
4750
4970
  }
4751
4971
 
4752
- export { ActionRegistry, BUILTIN_ICONS, BUILTIN_ICON_NAMES, JsonPointerPathError, LifecycleManager, StreamingEngine, StreamingParser, a2uiComponentToElement, a2uiToCommand, bindingTopologyFingerprint, cloneJsonData, convertLegacySchema, createA2UIParameterResolver, createExpressionContext, createLifecycleManager, decodeJsonPointerSegment, extractPartialSchema, findAffectedRepeatOwners, findTemplateRepeatOwners, getBuiltinIcon, getByJsonPointer, getByPath$1 as getByPath, hasDynamicChildren, hasExpression, interpolate, isA2UIChildList, isA2UIDynamicChildList, isA2UIEnvelope, isA2UIPathBinding, isBoundRenderTreeNode, isLegacySchema, materializeCard, normalizeSchema, parseJsonPointer, parseSchema, registerActionHandler, registry, replaceRootContents, requiresBindingMaterialization, resetIdCounter, resolveA2UIDeep, resolveA2UIPath, resolveActionRef, resolveDeep, resolveExpression, resolveExpressionValue, runActionStep, runActionSteps, setByJsonPointer, setByPath, validateSchema };
4972
+ export { ActionRegistry, BUILTIN_ICONS, BUILTIN_ICON_NAMES, JsonPointerPathError, LifecycleManager, StreamingEngine, StreamingParser, a2uiComponentToElement, a2uiToCommand, bindingTopologyFingerprint, cloneJsonData, convertLegacySchema, createA2UIParameterResolver, createExpressionContext, createLifecycleManager, decodeJsonPointerSegment, extractPartialSchema, findAffectedRepeatOwners, findTemplateRepeatOwners, getBuiltinIcon, getByJsonPointer, getByPath$1 as getByPath, hasDynamicChildren, hasExpression, interpolate, isA2UIChildList, isA2UIDynamicChildList, isA2UIEnvelope, isA2UIPathBinding, isBoundRenderTreeNode, isLegacySchema, materializeCard, normalizeSchema, parseJsonPointer, parseSchema, registerActionHandler, registry, replaceRootContents, requiresBindingMaterialization, resetIdCounter, resolveA2UIDeep, resolveA2UIPath, resolveActionRef, resolveDeep, resolveExpression, resolveExpressionValue, runActionStep, runActionSteps, setByJsonPointer, setByPath, validateSchema, validateSchemaDetailed };