@antglobal/copilot-cards-core 1.0.5 → 1.0.6

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