@objectstack/lint 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -250,10 +250,10 @@ var OUTCOME_CLAUSE = {
250
250
  "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
251
251
  "fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
252
252
  };
253
- function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
253
+ function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
254
254
  const owner = objectName ? `'${objectName}'` : "this object";
255
- const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
256
- return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
255
+ const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
256
+ return `${subject} applies \`${finding2.operator}\` to \`${finding2.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding2.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
257
257
  }
258
258
 
259
259
  // src/validate-expressions.ts
@@ -436,10 +436,10 @@ function validateStackExpressions(stack) {
436
436
  if (!nullableFields || nullableFields.size === 0) return;
437
437
  const source = celSourceOf(raw);
438
438
  if (!source) return;
439
- for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
439
+ for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
440
440
  issues.push({
441
441
  where,
442
- message: nullGuardMessage(subject, objectName, finding, outcome),
442
+ message: nullGuardMessage(subject, objectName, finding2, outcome),
443
443
  source,
444
444
  severity: "error"
445
445
  });
@@ -463,7 +463,7 @@ function validateStackExpressions(stack) {
463
463
  const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
464
464
  const FIELD_RULE_SLOT_CONSEQUENCE = {
465
465
  visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
466
- readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
466
+ readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). The server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
467
467
  requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
468
468
  // Listed rather than left to the `??` below, so the map covers every slot
469
469
  // the field walk passes and the default stays unreachable. `FieldSchema`
@@ -522,14 +522,15 @@ function validateStackExpressions(stack) {
522
522
  if (retired.length > 0) {
523
523
  issues.push({
524
524
  where: `${at} \xB7 node '${node.id}' (script) callable`,
525
- message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
526
- // behaviour, never the retired key's fate — "rewrite it" reads two ways
527
- // over a branch that DELETES the key (template/recipients/variables/script),
528
- // "rewrite existing sources" only one. Plain-quoted (not a template literal)
525
+ message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled), reworded under #9529: the house
526
+ // sentence names the TOOL's behaviour, never the retired key's fate —
527
+ // "rewrite it" read two ways over a branch that DELETES the key
528
+ // (template/recipients/variables/script), and the tool never rewrote a
529
+ // source file at all. Plain-quoted (not a template literal)
529
530
  // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
530
531
  // widened scan (#7030) on the same textual shape as the spec corpus — no
531
532
  // interpolation lives in this clause, so nothing is lost switching quote style.
532
- "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
533
+ "Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
533
534
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
534
535
  });
535
536
  } else if (!fn) {
@@ -600,10 +601,10 @@ function validateStackExpressions(stack) {
600
601
  f.expression,
601
602
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
602
603
  );
603
- const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
604
- for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
605
- for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
606
- warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
604
+ const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
605
+ for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
606
+ for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
607
+ warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
607
608
  }
608
609
  }
609
610
  }
@@ -1259,7 +1260,6 @@ function asArray4(v) {
1259
1260
  function strName(v) {
1260
1261
  return typeof v === "string" && v.length > 0 ? v : void 0;
1261
1262
  }
1262
- var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
1263
1263
  var URL_COLLECTION_TO_STACK_KEY = {
1264
1264
  object: "objects",
1265
1265
  objects: "objects",
@@ -1304,13 +1304,10 @@ function collectKnownTargets(stack) {
1304
1304
  return { actions, objects, reports, dashboards, pages, views };
1305
1305
  }
1306
1306
  function resolveActionTarget(actionType, target, known) {
1307
- if (known.actions.has(target)) return true;
1308
1307
  if (actionType === "modal") {
1309
- if (known.objects.has(target)) return true;
1310
- const m = MODAL_VERB_RE.exec(target);
1311
- if (m && known.objects.has(m[1])) return true;
1308
+ return known.pages.has(target);
1312
1309
  }
1313
- return false;
1310
+ return known.actions.has(target);
1314
1311
  }
1315
1312
  function resolveUrlRoute(target, known) {
1316
1313
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
@@ -1340,14 +1337,13 @@ function validateDashboardActionRefs(stack) {
1340
1337
  const actionType = strName(action.actionType) ?? "url";
1341
1338
  if (actionType === "script" || actionType === "modal") {
1342
1339
  if (resolveActionTarget(actionType, target, known)) return;
1343
- const kindWord = actionType === "script" ? "script" : "modal";
1344
1340
  findings.push({
1345
1341
  severity: "error",
1346
1342
  rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
1347
1343
  where,
1348
1344
  path,
1349
- message: `${kindWord} action target "${target}" resolves to no defined action` + (actionType === "modal" ? " or object" : "") + `. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
1350
- hint: actionType === "modal" ? `Define an action named "${target}" (stack.actions or the object's actions), use the "<verb>_<object>" convention against a real object (e.g. "create_<object>"), point actionUrl at an existing object, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
1345
+ message: actionType === "modal" ? `modal action target "${target}" names no declared page \u2014 a modal target names a PAGE, only (objectstack#6739). The button renders but the runtime refuses the dispatch when clicked \u2014 a dangling reference (ADR-0049: a declared reference must resolve).` : `script action target "${target}" resolves to no defined action. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
1346
+ hint: actionType === "modal" ? `Point actionUrl at a declared page (stack.pages), or use actionType: 'form' with an "<object>.<view>" form-view target to open an object's form, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
1351
1347
  });
1352
1348
  return;
1353
1349
  }
@@ -1389,7 +1385,7 @@ function validateDashboardActionRefs(stack) {
1389
1385
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
1390
1386
 
1391
1387
  // src/filter-walk.ts
1392
- var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1388
+ var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
1393
1389
  function asArray5(v) {
1394
1390
  if (Array.isArray(v)) return v;
1395
1391
  if (v && typeof v === "object") {
@@ -1493,6 +1489,149 @@ function validateFilterTokens(stack) {
1493
1489
  return out;
1494
1490
  }
1495
1491
 
1492
+ // src/validate-preset-comparands.ts
1493
+ import {
1494
+ VALID_AST_OPERATORS,
1495
+ bareDateRangePresetComparandMessage,
1496
+ canonicalAstOperator,
1497
+ isDateRangePresetName
1498
+ } from "@objectstack/spec/data";
1499
+ import { normalizeFilterOperator } from "@objectstack/spec/ui";
1500
+ var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
1501
+ var PRESET_COMPARAND_SURFACES = [
1502
+ { key: "dashboards", kind: "dashboard" },
1503
+ { key: "objects", kind: "object" },
1504
+ { key: "views", kind: "view" },
1505
+ { key: "reports", kind: "report" },
1506
+ { key: "datasets", kind: "dataset" },
1507
+ { key: "pages", kind: "page" },
1508
+ { key: "apps", kind: "app" },
1509
+ { key: "flows", kind: "flow" }
1510
+ ];
1511
+ var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
1512
+ var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
1513
+ var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
1514
+ "greater_than",
1515
+ "greater_than_or_equal",
1516
+ "less_than",
1517
+ "less_than_or_equal",
1518
+ "before",
1519
+ "after"
1520
+ ]);
1521
+ var MAX_DEPTH = 32;
1522
+ function isPlainObject(v) {
1523
+ return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
1524
+ }
1525
+ function finding(where, path, preset, operator) {
1526
+ return {
1527
+ severity: "error",
1528
+ rule: FILTER_PRESET_COMPARAND,
1529
+ where,
1530
+ path,
1531
+ message: bareDateRangePresetComparandMessage(preset, operator),
1532
+ hint: "Presets belong to the dashboard date-filter bar (dateRange.defaultRange, a date global filter's defaultValue). In a filter comparand, write the {date-macro} window the message names, or an ISO date."
1533
+ };
1534
+ }
1535
+ function judgeConditionNode(node, path, where, out, depth) {
1536
+ if (depth > MAX_DEPTH) return;
1537
+ for (const [key, value] of Object.entries(node)) {
1538
+ const here = `${path}.${key}`;
1539
+ if (key === "$and" || key === "$or") {
1540
+ if (Array.isArray(value)) {
1541
+ value.forEach((arm, i) => {
1542
+ if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
1543
+ });
1544
+ }
1545
+ continue;
1546
+ }
1547
+ if (key === "$not") {
1548
+ if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
1549
+ continue;
1550
+ }
1551
+ if (key.startsWith("$")) continue;
1552
+ if (!isPlainObject(value)) continue;
1553
+ const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
1554
+ if (!hasOps) {
1555
+ judgeConditionNode(value, here, where, out, depth + 1);
1556
+ continue;
1557
+ }
1558
+ for (const [op, comparand] of Object.entries(value)) {
1559
+ if (ORDERING_DOLLAR_OPS.has(op) && isDateRangePresetName(comparand)) {
1560
+ out.push(finding(where, `${here}.${op}`, comparand, op));
1561
+ continue;
1562
+ }
1563
+ if (op === "$between" && Array.isArray(comparand)) {
1564
+ comparand.forEach((endpoint, i) => {
1565
+ if (isDateRangePresetName(endpoint)) {
1566
+ out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
1567
+ }
1568
+ });
1569
+ }
1570
+ }
1571
+ }
1572
+ }
1573
+ function judgeFilterRule(rule, path, where, out) {
1574
+ const operator = normalizeFilterOperator(rule.operator);
1575
+ if (typeof operator !== "string") return;
1576
+ const value = rule.value;
1577
+ if (ORDERING_RULE_OPS.has(operator) && isDateRangePresetName(value)) {
1578
+ out.push(finding(where, `${path}.value`, value, operator));
1579
+ return;
1580
+ }
1581
+ if (operator === "between" && Array.isArray(value)) {
1582
+ value.forEach((endpoint, i) => {
1583
+ if (isDateRangePresetName(endpoint)) {
1584
+ out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
1585
+ }
1586
+ });
1587
+ }
1588
+ }
1589
+ function judgeTriple(triple, path, where, out) {
1590
+ const op = triple[1];
1591
+ if (typeof op !== "string") return;
1592
+ const canonical = canonicalAstOperator(op);
1593
+ const value = triple[2];
1594
+ if (ORDERING_INFIX_OPS.has(canonical) && isDateRangePresetName(value)) {
1595
+ out.push(finding(where, `${path}[2]`, value, op));
1596
+ return;
1597
+ }
1598
+ if (canonical === "between" && Array.isArray(value)) {
1599
+ value.forEach((endpoint, i) => {
1600
+ if (isDateRangePresetName(endpoint)) {
1601
+ out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
1602
+ }
1603
+ });
1604
+ }
1605
+ }
1606
+ function judgeFilterValue(node, path, where, out, depth) {
1607
+ if (depth > MAX_DEPTH) return;
1608
+ if (Array.isArray(node)) {
1609
+ if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
1610
+ judgeTriple(node, path, where, out);
1611
+ return;
1612
+ }
1613
+ node.forEach((member, i) => {
1614
+ if (typeof member === "string") return;
1615
+ judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
1616
+ });
1617
+ return;
1618
+ }
1619
+ if (!isPlainObject(node)) return;
1620
+ if (typeof node.field === "string" && typeof node.operator === "string") {
1621
+ judgeFilterRule(node, path, where, out);
1622
+ return;
1623
+ }
1624
+ judgeConditionNode(node, path, where, out, depth);
1625
+ }
1626
+ function validatePresetComparands(stack) {
1627
+ if (!stack || typeof stack !== "object") return [];
1628
+ const out = [];
1629
+ walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
1630
+ judgeFilterValue(value, path, where, out, 0);
1631
+ });
1632
+ return out;
1633
+ }
1634
+
1496
1635
  // src/validate-empty-combinators.ts
1497
1636
  import { reduceFilterVerdict } from "@objectstack/spec/data";
1498
1637
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
@@ -1816,6 +1955,7 @@ import {
1816
1955
  } from "@objectstack/spec/data";
1817
1956
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1818
1957
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1958
+ var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
1819
1959
  function asArray7(v) {
1820
1960
  if (Array.isArray(v)) return v;
1821
1961
  if (v && typeof v === "object") {
@@ -1906,7 +2046,7 @@ function indexObjectSearchTargets(stack) {
1906
2046
  }
1907
2047
  return fieldsByObject;
1908
2048
  }
1909
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
2049
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
1910
2050
  const findings = [];
1911
2051
  if (!Array.isArray(declared) || declared.length === 0) return findings;
1912
2052
  if (!objectName) return findings;
@@ -1914,6 +2054,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1914
2054
  const target = fieldsByObject.get(objectName);
1915
2055
  if (!target) return findings;
1916
2056
  const known = target.names;
2057
+ const anchors = unprovisionedAnchors?.get(objectName);
1917
2058
  const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
1918
2059
  for (let i = 0; i < declared.length; i++) {
1919
2060
  const entry = declared[i];
@@ -1931,6 +2072,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1931
2072
  });
1932
2073
  continue;
1933
2074
  }
2075
+ if (anchors?.has(name)) {
2076
+ findings.push({
2077
+ severity: "warning",
2078
+ rule: SEARCHABLE_FIELD_UNPROVISIONED,
2079
+ where,
2080
+ path: `${path}[${i}]`,
2081
+ message: `${subject} entry "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)}` + (role === "narrowing" ? ` \u2014 clients echo this declaration verbatim as the '$searchFields' override, so every toolbar search on this list scans a column that is empty on every record: it reads as search coverage and matches nothing.` : ` \u2014 'search' scans it on every record and it can never match, so the object's searchable set is narrower than it declares. Should it be the ONLY entry that resolves, the set scans nothing at all.`),
2082
+ hint: unprovisionedAnchorHint(objectName, name)
2083
+ });
2084
+ }
1934
2085
  if (isVirtualSearchField(target.fields[name])) {
1935
2086
  const vtype = target.fields[name]?.type;
1936
2087
  findings.push({
@@ -1984,9 +2135,19 @@ function validateSearchableFields(stack) {
1984
2135
  if (!isRec3(stack)) return findings;
1985
2136
  const objects = asArray7(stack.objects);
1986
2137
  const fieldsByObject = indexObjectSearchTargets(stack);
2138
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
1987
2139
  const check = (declared, objectName, where, path, subject, role) => {
1988
2140
  findings.push(
1989
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
2141
+ ...checkSearchableFieldList(
2142
+ declared,
2143
+ objectName,
2144
+ fieldsByObject,
2145
+ where,
2146
+ path,
2147
+ subject,
2148
+ role,
2149
+ unprovisionedAnchors
2150
+ )
1990
2151
  );
1991
2152
  };
1992
2153
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2024,6 +2185,26 @@ function validateSearchableFields(stack) {
2024
2185
  if (!isRec3(view)) continue;
2025
2186
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
2026
2187
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
2188
+ if (view.viewKind === "list" && !isRec3(view.config)) {
2189
+ check(
2190
+ view.searchableFields,
2191
+ listViewObject(view) ?? viewObject,
2192
+ `view "${viewLabel2}" (flattened list overlay)`,
2193
+ `views[${vi}].searchableFields`,
2194
+ "list-view searchableFields",
2195
+ "narrowing"
2196
+ );
2197
+ }
2198
+ if (view.viewKind === "list" && isRec3(view.config)) {
2199
+ check(
2200
+ view.config.searchableFields,
2201
+ listViewObject(view.config) ?? viewObject,
2202
+ `view "${viewLabel2}" (ViewItem record)`,
2203
+ `views[${vi}].config.searchableFields`,
2204
+ "list-view searchableFields",
2205
+ "narrowing"
2206
+ );
2207
+ }
2027
2208
  if (isRec3(view.list)) {
2028
2209
  check(
2029
2210
  view.list.searchableFields,
@@ -2055,33 +2236,244 @@ function listViewObject(listView) {
2055
2236
  return isRec3(data) ? strName3(data.object) : void 0;
2056
2237
  }
2057
2238
 
2058
- // src/page-walk.ts
2239
+ // src/validate-sortable-fields.ts
2240
+ import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
2241
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
2242
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
2243
+ var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
2059
2244
  function isRec4(v) {
2060
2245
  return !!v && typeof v === "object" && !Array.isArray(v);
2061
2246
  }
2062
2247
  function strName4(v) {
2063
2248
  return typeof v === "string" && v.length > 0 ? v : void 0;
2064
2249
  }
2250
+ function readSortKeys(declared) {
2251
+ const fromShorthand = (raw, at) => {
2252
+ const trimmed = raw.trim();
2253
+ if (!trimmed) return void 0;
2254
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
2255
+ return bare ? { field: bare, at } : void 0;
2256
+ };
2257
+ if (typeof declared === "string") {
2258
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
2259
+ }
2260
+ if (Array.isArray(declared)) {
2261
+ const keys = [];
2262
+ for (let i = 0; i < declared.length; i++) {
2263
+ const el = declared[i];
2264
+ if (typeof el === "string") {
2265
+ const k = fromShorthand(el, `[${i}]`);
2266
+ if (k) keys.push(k);
2267
+ continue;
2268
+ }
2269
+ if (!isRec4(el)) continue;
2270
+ const field = strName4(el.field);
2271
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
2272
+ }
2273
+ return keys;
2274
+ }
2275
+ return [];
2276
+ }
2277
+ function suggest4(target, known) {
2278
+ let best;
2279
+ let bestScore = Infinity;
2280
+ for (const candidate of known) {
2281
+ const d = distance3(target, candidate);
2282
+ if (d < bestScore) {
2283
+ bestScore = d;
2284
+ best = candidate;
2285
+ }
2286
+ }
2287
+ const limit = Math.max(2, Math.floor(target.length / 3));
2288
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
2289
+ }
2290
+ function distance3(a, b) {
2291
+ const m = a.length;
2292
+ const n = b.length;
2293
+ if (m === 0) return n;
2294
+ if (n === 0) return m;
2295
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
2296
+ for (let i = 1; i <= m; i++) {
2297
+ const curr = [i, ...new Array(n).fill(0)];
2298
+ for (let j = 1; j <= n; j++) {
2299
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2300
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2301
+ }
2302
+ prev = curr;
2303
+ }
2304
+ return prev[n];
2305
+ }
2306
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
2307
+ const findings = [];
2308
+ if (declared === void 0 || declared === null) return findings;
2309
+ if (!objectName) return findings;
2310
+ if (!fieldsByObject.has(objectName)) return findings;
2311
+ const target = fieldsByObject.get(objectName);
2312
+ if (!target) return findings;
2313
+ const known = target.names;
2314
+ const anchors = unprovisionedAnchors?.get(objectName);
2315
+ for (const key of readSortKeys(declared)) {
2316
+ const name = key.field;
2317
+ const head = name.split(".")[0];
2318
+ if (SYSTEM_FIELDS.has(head)) {
2319
+ if (!name.includes(".") && anchors?.has(head)) {
2320
+ findings.push({
2321
+ severity: "warning",
2322
+ rule: SORT_FIELD_UNPROVISIONED,
2323
+ where,
2324
+ path: `${path}${key.at}`,
2325
+ message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
2326
+ hint: unprovisionedAnchorHint(objectName, name)
2327
+ });
2328
+ }
2329
+ continue;
2330
+ }
2331
+ if (!known.has(head)) {
2332
+ const dotted = name.includes(".");
2333
+ findings.push({
2334
+ severity: "error",
2335
+ rule: SORT_FIELD_UNKNOWN,
2336
+ where,
2337
+ path: `${path}${key.at}`,
2338
+ message: `${subject} orders by "${name}", which is not a field on object "${objectName}". The runtime refuses the sort rather than dropping it: every load of this view answers 400 INVALID_SORT (#6994), because a sort is the view's FIRST fetch and not an optional interaction.` + (dotted ? "" : suggest4(head, known)),
2339
+ hint: (dotted ? `'sort' reaches only whole columns of "${objectName}" itself, never a related record's column \u2014 denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + (known.size > 0 ? `Object fields: ${[...known].sort().join(", ")}.` : "")
2340
+ });
2341
+ continue;
2342
+ }
2343
+ const meta = target.fields[name];
2344
+ if (isVirtualSearchField2(meta)) {
2345
+ const vtype = meta?.type;
2346
+ findings.push({
2347
+ severity: "error",
2348
+ rule: SORT_FIELD_UNSORTABLE,
2349
+ where,
2350
+ path: `${path}${key.at}`,
2351
+ message: `${subject} orders by "${name}" on object "${objectName}", a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materialises a column to ORDER BY. Measured, an unrefused sort on one returns 'asc' and 'desc' in byte-identical order \u2014 the rows carry the values they were asked to be ordered by, unordered, under a success.`,
2352
+ hint: `Denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that, or drop "${name}" from this sort. At runtime both doors now refuse it with 400 INVALID_SORT \u2014 the REST ingress (#6994) and the engine itself (#7095) \u2014 so the declaration breaks the view's first fetch, and every fetch after it.`
2353
+ });
2354
+ }
2355
+ }
2356
+ return findings;
2357
+ }
2358
+ function validateSortableFields(stack) {
2359
+ const findings = [];
2360
+ if (!isRec4(stack)) return findings;
2361
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
2362
+ const fieldsByObject = indexObjectSearchTargets(stack);
2363
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2364
+ const check = (declared, objectName, where, path, subject) => {
2365
+ findings.push(
2366
+ ...checkSortDeclaration(
2367
+ declared,
2368
+ objectName,
2369
+ fieldsByObject,
2370
+ where,
2371
+ path,
2372
+ subject,
2373
+ unprovisionedAnchors
2374
+ )
2375
+ );
2376
+ };
2377
+ for (let oi = 0; oi < objects.length; oi++) {
2378
+ const obj = objects[oi];
2379
+ if (!isRec4(obj)) continue;
2380
+ const objName = strName4(obj.name);
2381
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
2382
+ if (isRec4(obj.listViews)) {
2383
+ for (const [key, lv] of Object.entries(obj.listViews)) {
2384
+ if (!isRec4(lv)) continue;
2385
+ check(
2386
+ lv.sort,
2387
+ // A built-in list view belongs to its object; an inline `data.object`
2388
+ // may still retarget it (ADR-0047 allows the explicit binding).
2389
+ listViewObject2(lv) ?? objName,
2390
+ `${label2} \u203A listViews.${key}`,
2391
+ `objects[${oi}].listViews.${key}.sort`,
2392
+ "list-view sort"
2393
+ );
2394
+ }
2395
+ }
2396
+ }
2397
+ const views = Array.isArray(stack.views) ? stack.views : [];
2398
+ for (let vi = 0; vi < views.length; vi++) {
2399
+ const view = views[vi];
2400
+ if (!isRec4(view)) continue;
2401
+ const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
2402
+ const viewObject = strName4(view.objectName) ?? strName4(view.object);
2403
+ if (view.viewKind === "list" && !isRec4(view.config)) {
2404
+ check(
2405
+ view.sort,
2406
+ listViewObject2(view) ?? viewObject,
2407
+ `view "${viewLabel2}" (flattened list overlay)`,
2408
+ `views[${vi}].sort`,
2409
+ "list-view sort"
2410
+ );
2411
+ }
2412
+ if (view.viewKind === "list" && isRec4(view.config)) {
2413
+ check(
2414
+ view.config.sort,
2415
+ listViewObject2(view.config) ?? viewObject,
2416
+ `view "${viewLabel2}" (ViewItem record)`,
2417
+ `views[${vi}].config.sort`,
2418
+ "list-view sort"
2419
+ );
2420
+ }
2421
+ if (isRec4(view.list)) {
2422
+ check(
2423
+ view.list.sort,
2424
+ listViewObject2(view.list) ?? viewObject,
2425
+ `view "${viewLabel2}" \u203A list`,
2426
+ `views[${vi}].list.sort`,
2427
+ "list-view sort"
2428
+ );
2429
+ }
2430
+ if (isRec4(view.listViews)) {
2431
+ for (const [key, lv] of Object.entries(view.listViews)) {
2432
+ if (!isRec4(lv)) continue;
2433
+ check(
2434
+ lv.sort,
2435
+ listViewObject2(lv) ?? viewObject,
2436
+ `view "${viewLabel2}" \u203A listViews.${key}`,
2437
+ `views[${vi}].listViews.${key}.sort`,
2438
+ "list-view sort"
2439
+ );
2440
+ }
2441
+ }
2442
+ }
2443
+ return findings;
2444
+ }
2445
+ function listViewObject2(listView) {
2446
+ const data = listView.data;
2447
+ return isRec4(data) ? strName4(data.object) : void 0;
2448
+ }
2449
+
2450
+ // src/page-walk.ts
2451
+ function isRec5(v) {
2452
+ return !!v && typeof v === "object" && !Array.isArray(v);
2453
+ }
2454
+ function strName5(v) {
2455
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2456
+ }
2065
2457
  var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
2066
2458
  function isSourceAuthoredPage(page) {
2067
- const kind = strName4(page.kind);
2459
+ const kind = strName5(page.kind);
2068
2460
  return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
2069
2461
  }
2070
2462
  function walkPageComponents(page, pagePath) {
2071
2463
  const out = [];
2072
- if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
2073
- const pageObject = strName4(page.object);
2464
+ if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
2465
+ const pageObject = strName5(page.object);
2074
2466
  const visit = (node, path, inheritedObject) => {
2075
- if (!isRec4(node)) return;
2076
- const props = isRec4(node.properties) ? node.properties : void 0;
2077
- const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
2078
- const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
2467
+ if (!isRec5(node)) return;
2468
+ const props = isRec5(node.properties) ? node.properties : void 0;
2469
+ const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
2470
+ const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
2079
2471
  out.push({ component: node, path, objectName });
2080
2472
  if (!props) return;
2081
2473
  if (Array.isArray(props.items)) {
2082
2474
  for (let i = 0; i < props.items.length; i++) {
2083
2475
  const item = props.items[i];
2084
- if (!isRec4(item) || !Array.isArray(item.children)) continue;
2476
+ if (!isRec5(item) || !Array.isArray(item.children)) continue;
2085
2477
  for (let c = 0; c < item.children.length; c++) {
2086
2478
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
2087
2479
  }
@@ -2103,12 +2495,12 @@ function walkPageComponents(page, pagePath) {
2103
2495
  const regions = Array.isArray(page.regions) ? page.regions : [];
2104
2496
  for (let r = 0; r < regions.length; r++) {
2105
2497
  const region = regions[r];
2106
- if (!isRec4(region) || !Array.isArray(region.components)) continue;
2498
+ if (!isRec5(region) || !Array.isArray(region.components)) continue;
2107
2499
  for (let c = 0; c < region.components.length; c++) {
2108
2500
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
2109
2501
  }
2110
2502
  }
2111
- const slots = isRec4(page.slots) ? page.slots : void 0;
2503
+ const slots = isRec5(page.slots) ? page.slots : void 0;
2112
2504
  if (slots) {
2113
2505
  for (const [slot, value] of Object.entries(slots)) {
2114
2506
  const list3 = Array.isArray(value) ? value : [value];
@@ -2130,13 +2522,13 @@ function asArray8(v) {
2130
2522
  }
2131
2523
  return [];
2132
2524
  }
2133
- function strName5(v) {
2525
+ function strName6(v) {
2134
2526
  return typeof v === "string" && v.length > 0 ? v : void 0;
2135
2527
  }
2136
2528
  function strList(v) {
2137
2529
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2138
2530
  }
2139
- function distance3(a, b) {
2531
+ function distance4(a, b) {
2140
2532
  const m = a.length;
2141
2533
  const n = b.length;
2142
2534
  if (m === 0) return n;
@@ -2152,11 +2544,11 @@ function distance3(a, b) {
2152
2544
  }
2153
2545
  return prev[n];
2154
2546
  }
2155
- function suggest4(target, known) {
2547
+ function suggest5(target, known) {
2156
2548
  let best;
2157
2549
  let bestScore = Infinity;
2158
2550
  for (const candidate of known) {
2159
- const d = distance3(target, candidate);
2551
+ const d = distance4(target, candidate);
2160
2552
  if (d < bestScore) {
2161
2553
  bestScore = d;
2162
2554
  best = candidate;
@@ -2168,13 +2560,13 @@ function suggest4(target, known) {
2168
2560
  function collectActionNames(stack) {
2169
2561
  const names = /* @__PURE__ */ new Set();
2170
2562
  for (const action of asArray8(stack.actions)) {
2171
- const n = strName5(action?.name);
2563
+ const n = strName6(action?.name);
2172
2564
  if (n) names.add(n);
2173
2565
  }
2174
2566
  for (const obj of asArray8(stack.objects)) {
2175
2567
  if (!obj || typeof obj !== "object") continue;
2176
2568
  for (const action of asArray8(obj.actions)) {
2177
- const n = strName5(action?.name);
2569
+ const n = strName6(action?.name);
2178
2570
  if (n) names.add(n);
2179
2571
  }
2180
2572
  }
@@ -2191,7 +2583,7 @@ function validateActionNameRefs(stack) {
2191
2583
  rule: ACTION_NAME_UNDEFINED,
2192
2584
  where,
2193
2585
  path,
2194
- message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
2586
+ message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest5(name, known),
2195
2587
  hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
2196
2588
  });
2197
2589
  };
@@ -2217,7 +2609,7 @@ function validateActionNameRefs(stack) {
2217
2609
  if (!def || typeof def !== "object") continue;
2218
2610
  if (def.execution !== "aggregate") continue;
2219
2611
  if (def.actionDef !== void 0) continue;
2220
- const name = strName5(def.name);
2612
+ const name = strName6(def.name);
2221
2613
  if (!name) continue;
2222
2614
  check(
2223
2615
  name,
@@ -2232,7 +2624,7 @@ function validateActionNameRefs(stack) {
2232
2624
  for (let vi = 0; vi < views.length; vi++) {
2233
2625
  const view = views[vi];
2234
2626
  if (!view || typeof view !== "object") continue;
2235
- const viewName = strName5(view.name) ?? strName5(view.object) ?? `#${vi}`;
2627
+ const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
2236
2628
  const owner = `view "${viewName}"`;
2237
2629
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
2238
2630
  const listViews = view.listViews;
@@ -2248,7 +2640,7 @@ function validateActionNameRefs(stack) {
2248
2640
  if (!obj || typeof obj !== "object") continue;
2249
2641
  const objListViews = obj.listViews;
2250
2642
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
2251
- const owner = `object "${strName5(obj.name) ?? `#${oi}`}"`;
2643
+ const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
2252
2644
  for (const [key, lv] of Object.entries(objListViews)) {
2253
2645
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
2254
2646
  }
@@ -2257,7 +2649,7 @@ function validateActionNameRefs(stack) {
2257
2649
  for (let pi = 0; pi < pages.length; pi++) {
2258
2650
  const page = pages[pi];
2259
2651
  if (!page || typeof page !== "object") continue;
2260
- const pageName = strName5(page.name) ?? `#${pi}`;
2652
+ const pageName = strName6(page.name) ?? `#${pi}`;
2261
2653
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2262
2654
  const props = component.properties;
2263
2655
  if (!props || typeof props !== "object") continue;
@@ -2265,7 +2657,7 @@ function validateActionNameRefs(stack) {
2265
2657
  for (let ai = 0; ai < names.length; ai++) {
2266
2658
  check(
2267
2659
  names[ai],
2268
- `page "${pageName}" \xB7 component "${strName5(component.type) ?? "?"}"`,
2660
+ `page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
2269
2661
  `${path}.properties.actionNames[${ai}]`,
2270
2662
  "Quick-actions bar"
2271
2663
  );
@@ -2276,7 +2668,7 @@ function validateActionNameRefs(stack) {
2276
2668
  for (let ai = 0; ai < apps.length; ai++) {
2277
2669
  const app = apps[ai];
2278
2670
  if (!app || typeof app !== "object") continue;
2279
- const appName = strName5(app.name) ?? `#${ai}`;
2671
+ const appName = strName6(app.name) ?? `#${ai}`;
2280
2672
  const walkNav = (items, basePath) => {
2281
2673
  const navItems = asArray8(items);
2282
2674
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -2284,20 +2676,20 @@ function validateActionNameRefs(stack) {
2284
2676
  if (!nav || typeof nav !== "object") continue;
2285
2677
  const navPath = `${basePath}[${ni}]`;
2286
2678
  const actionDef = nav.actionDef;
2287
- const actionName = strName5(actionDef?.actionName);
2679
+ const actionName = strName6(actionDef?.actionName);
2288
2680
  if (nav.type === "action" && actionName) {
2289
2681
  check(
2290
2682
  actionName,
2291
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2683
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2292
2684
  `${navPath}.actionDef.actionName`,
2293
2685
  "Navigation action item"
2294
2686
  );
2295
2687
  }
2296
- const runAction = strName5(nav.runAction);
2688
+ const runAction = strName6(nav.runAction);
2297
2689
  if (nav.type === "object" && runAction) {
2298
2690
  check(
2299
2691
  runAction,
2300
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2692
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2301
2693
  `${navPath}.runAction`,
2302
2694
  "Navigation deep-link auto-run"
2303
2695
  );
@@ -2324,23 +2716,23 @@ function asArray9(v) {
2324
2716
  }
2325
2717
  return [];
2326
2718
  }
2327
- function strName6(v) {
2719
+ function strName7(v) {
2328
2720
  return typeof v === "string" && v.length > 0 ? v : void 0;
2329
2721
  }
2330
- function isRec5(v) {
2722
+ function isRec6(v) {
2331
2723
  return !!v && typeof v === "object" && !Array.isArray(v);
2332
2724
  }
2333
2725
  function fieldRefsFrom(value, basePath) {
2334
2726
  const out = [];
2335
2727
  const one = (v, path) => {
2336
- const bare = strName6(v);
2728
+ const bare = strName7(v);
2337
2729
  if (bare) {
2338
2730
  out.push({ name: bare, path });
2339
2731
  return;
2340
2732
  }
2341
- if (!isRec5(v)) return;
2342
- const named = strName6(v.field) ?? strName6(v.name);
2343
- if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
2733
+ if (!isRec6(v)) return;
2734
+ const named = strName7(v.field) ?? strName7(v.name);
2735
+ if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
2344
2736
  };
2345
2737
  if (Array.isArray(value)) {
2346
2738
  for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
@@ -2366,7 +2758,12 @@ var COMPONENT_FIELD_SPECS = {
2366
2758
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2367
2759
  "record:path": { props: ["statusField"] },
2368
2760
  "element:number": { props: ["field"] },
2369
- "element:filter": { props: ["fields"] },
2761
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
2762
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
2763
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
2764
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
2765
+ // reports an authored one by name with the element-retirement prescription —
2766
+ // the same #5775/#6629 residue class as the record-picker entries below.
2370
2767
  "element:form": { props: ["fields"] },
2371
2768
  // `labelField` is the one field-bearing prop this element declares. Its former
2372
2769
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -2396,18 +2793,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2396
2793
  const sections = Array.isArray(props[key]) ? props[key] : [];
2397
2794
  for (let si = 0; si < sections.length; si++) {
2398
2795
  const section = sections[si];
2399
- if (!isRec5(section)) continue;
2796
+ if (!isRec6(section)) continue;
2400
2797
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2401
2798
  }
2402
2799
  }
2403
2800
  return refs;
2404
2801
  }
2405
2802
  function relatedListFieldRefs(props, basePath, sep = ".") {
2406
- const add = isRec5(props.add) ? props.add : void 0;
2407
- const picker = add && isRec5(add.picker) ? add.picker : void 0;
2803
+ const add = isRec6(props.add) ? props.add : void 0;
2804
+ const picker = add && isRec6(add.picker) ? add.picker : void 0;
2408
2805
  const at = (key) => `${basePath}${sep}${key}`;
2409
2806
  return {
2410
- relatedObject: strName6(props.objectName),
2807
+ relatedObject: strName7(props.objectName),
2411
2808
  related: [
2412
2809
  ...fieldRefsFrom(props.columns, at("columns")),
2413
2810
  ...sortFieldRefs(props.sort, at("sort")),
@@ -2416,7 +2813,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2416
2813
  ...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
2417
2814
  ],
2418
2815
  parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
2419
- pickerObject: picker ? strName6(picker.object) : void 0,
2816
+ pickerObject: picker ? strName7(picker.object) : void 0,
2420
2817
  picker: picker ? [
2421
2818
  ...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
2422
2819
  ...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
@@ -2425,13 +2822,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2425
2822
  }
2426
2823
  function indexObjectFields(stack) {
2427
2824
  const objectFields = /* @__PURE__ */ new Map();
2428
- if (!isRec5(stack)) return objectFields;
2825
+ if (!isRec6(stack)) return objectFields;
2429
2826
  for (const obj of asArray9(stack.objects)) {
2430
- const name = strName6(obj.name);
2827
+ const name = strName7(obj.name);
2431
2828
  if (!name) continue;
2432
2829
  const names = /* @__PURE__ */ new Set();
2433
2830
  for (const f of asArray9(obj.fields)) {
2434
- const fn = strName6(f.name);
2831
+ const fn = strName7(f.name);
2435
2832
  if (fn) names.add(fn);
2436
2833
  }
2437
2834
  objectFields.set(name, names);
@@ -2479,15 +2876,15 @@ function validatePageFieldBindings(stack) {
2479
2876
  for (let pi = 0; pi < pages.length; pi++) {
2480
2877
  const page = pages[pi];
2481
2878
  if (!page || typeof page !== "object") continue;
2482
- const pageName = strName6(page.name) ?? `#${pi}`;
2879
+ const pageName = strName7(page.name) ?? `#${pi}`;
2483
2880
  const checkRefs = (refs, objectName, where) => {
2484
2881
  findings.push(
2485
2882
  ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2486
2883
  );
2487
2884
  };
2488
2885
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2489
- const type = strName6(component.type);
2490
- const props = isRec5(component.properties) ? component.properties : void 0;
2886
+ const type = strName7(component.type);
2887
+ const props = isRec6(component.properties) ? component.properties : void 0;
2491
2888
  if (!type || !props) continue;
2492
2889
  const where = `page "${pageName}" \xB7 ${type}`;
2493
2890
  const base = `${path}.properties`;
@@ -2502,16 +2899,16 @@ function validatePageFieldBindings(stack) {
2502
2899
  if (!refs) continue;
2503
2900
  checkRefs(refs, objectName, where);
2504
2901
  }
2505
- const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2902
+ const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
2506
2903
  if (cfg) {
2507
- const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2904
+ const cfgObject = strName7(cfg.source) ?? strName7(page.object);
2508
2905
  const base = `pages[${pi}].interfaceConfig`;
2509
2906
  const refs = [
2510
2907
  ...fieldRefsFrom(cfg.columns, `${base}.columns`),
2511
2908
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2512
2909
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2513
2910
  ];
2514
- const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2911
+ const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
2515
2912
  if (userFilters) {
2516
2913
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2517
2914
  }
@@ -2533,16 +2930,16 @@ function asArray10(v) {
2533
2930
  }
2534
2931
  return [];
2535
2932
  }
2536
- function strName7(v) {
2933
+ function strName8(v) {
2537
2934
  return typeof v === "string" && v.length > 0 ? v : void 0;
2538
2935
  }
2539
2936
  function strList2(v) {
2540
2937
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2541
2938
  }
2542
- function isRec6(v) {
2939
+ function isRec7(v) {
2543
2940
  return !!v && typeof v === "object" && !Array.isArray(v);
2544
2941
  }
2545
- function distance4(a, b) {
2942
+ function distance5(a, b) {
2546
2943
  const m = a.length;
2547
2944
  const n = b.length;
2548
2945
  if (m === 0) return n;
@@ -2558,11 +2955,11 @@ function distance4(a, b) {
2558
2955
  }
2559
2956
  return prev[n];
2560
2957
  }
2561
- function suggest5(target, known) {
2958
+ function suggest6(target, known) {
2562
2959
  let best;
2563
2960
  let bestScore = Infinity;
2564
2961
  for (const c of known) {
2565
- const d = distance4(target, c);
2962
+ const d = distance5(target, c);
2566
2963
  if (d < bestScore) {
2567
2964
  bestScore = d;
2568
2965
  best = c;
@@ -2578,16 +2975,16 @@ function list2(names) {
2578
2975
  function indexDatasets(stack) {
2579
2976
  const out = /* @__PURE__ */ new Map();
2580
2977
  for (const ds of asArray10(stack.datasets)) {
2581
- const name = strName7(ds.name);
2978
+ const name = strName8(ds.name);
2582
2979
  if (!name) continue;
2583
2980
  const dimensions = /* @__PURE__ */ new Set();
2584
2981
  for (const d of asArray10(ds.dimensions)) {
2585
- const n = strName7(d.name);
2982
+ const n = strName8(d.name);
2586
2983
  if (n) dimensions.add(n);
2587
2984
  }
2588
2985
  const measures = /* @__PURE__ */ new Set();
2589
2986
  for (const m of asArray10(ds.measures)) {
2590
- const n = strName7(m.name);
2987
+ const n = strName8(m.name);
2591
2988
  if (n) measures.add(n);
2592
2989
  }
2593
2990
  out.set(name, { dimensions, measures });
@@ -2610,7 +3007,7 @@ function validateChartBindings(stack) {
2610
3007
  where: binding.where,
2611
3008
  path: `${binding.path}.dataset`,
2612
3009
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
2613
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
3010
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
2614
3011
  });
2615
3012
  return;
2616
3013
  }
@@ -2622,7 +3019,7 @@ function validateChartBindings(stack) {
2622
3019
  where: binding.where,
2623
3020
  path,
2624
3021
  message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
2625
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
3022
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
2626
3023
  });
2627
3024
  };
2628
3025
  const measureRef = (name, path, selected2) => {
@@ -2633,7 +3030,7 @@ function validateChartBindings(stack) {
2633
3030
  where: binding.where,
2634
3031
  path,
2635
3032
  message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
2636
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
3033
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
2637
3034
  });
2638
3035
  return;
2639
3036
  }
@@ -2668,26 +3065,26 @@ function validateChartBindings(stack) {
2668
3065
  const reports = asArray10(stack.reports);
2669
3066
  for (let ri = 0; ri < reports.length; ri++) {
2670
3067
  const report = reports[ri];
2671
- if (!isRec6(report)) continue;
2672
- const reportName = strName7(report.name) ?? `#${ri}`;
3068
+ if (!isRec7(report)) continue;
3069
+ const reportName = strName8(report.name) ?? `#${ri}`;
2673
3070
  const checkReportChart = (chart, dataset, values, where, path) => {
2674
- if (!isRec6(chart)) return;
3071
+ if (!isRec7(chart)) return;
2675
3072
  check({
2676
3073
  dataset,
2677
3074
  // `values` is the report's measure SELECTION, not a chart ref; feeding
2678
3075
  // it in lets the yAxis "declared but not selected" check work without
2679
3076
  // reporting the selection itself twice.
2680
3077
  values: { names: values, path: `${path}.values` },
2681
- xAxis: strName7(chart.xAxis) ? { name: strName7(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
2682
- yAxis: strName7(chart.yAxis) ? { name: strName7(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
2683
- series: asArray10(chart.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
3078
+ xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
3079
+ yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
3080
+ series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
2684
3081
  where,
2685
3082
  path: `${path}.chart`
2686
3083
  });
2687
3084
  };
2688
3085
  checkReportChart(
2689
3086
  report.chart,
2690
- strName7(report.dataset),
3087
+ strName8(report.dataset),
2691
3088
  strList2(report.values),
2692
3089
  `report "${reportName}" \xB7 chart`,
2693
3090
  `reports[${ri}]`
@@ -2695,22 +3092,22 @@ function validateChartBindings(stack) {
2695
3092
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2696
3093
  for (let bi = 0; bi < blocks.length; bi++) {
2697
3094
  const block = blocks[bi];
2698
- if (!isRec6(block)) continue;
3095
+ if (!isRec7(block)) continue;
2699
3096
  checkReportChart(
2700
3097
  block.chart,
2701
- strName7(block.dataset),
3098
+ strName8(block.dataset),
2702
3099
  strList2(block.values),
2703
- `report "${reportName}" \xB7 block "${strName7(block.name) ?? `#${bi}`}" chart`,
3100
+ `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
2704
3101
  `reports[${ri}].blocks[${bi}]`
2705
3102
  );
2706
3103
  }
2707
3104
  }
2708
3105
  const checkListChart = (container, where, path) => {
2709
- if (!isRec6(container)) return;
3106
+ if (!isRec7(container)) return;
2710
3107
  const chart = container.chart;
2711
- if (!isRec6(chart)) return;
3108
+ if (!isRec7(chart)) return;
2712
3109
  check({
2713
- dataset: strName7(chart.dataset),
3110
+ dataset: strName8(chart.dataset),
2714
3111
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
2715
3112
  values: { names: strList2(chart.values), path: `${path}.chart.values` },
2716
3113
  where,
@@ -2720,10 +3117,10 @@ function validateChartBindings(stack) {
2720
3117
  const views = asArray10(stack.views);
2721
3118
  for (let vi = 0; vi < views.length; vi++) {
2722
3119
  const view = views[vi];
2723
- if (!isRec6(view)) continue;
2724
- const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
3120
+ if (!isRec7(view)) continue;
3121
+ const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
2725
3122
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2726
- if (isRec6(view.listViews)) {
3123
+ if (isRec7(view.listViews)) {
2727
3124
  for (const [key, lv] of Object.entries(view.listViews)) {
2728
3125
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2729
3126
  }
@@ -2732,8 +3129,8 @@ function validateChartBindings(stack) {
2732
3129
  const objects = asArray10(stack.objects);
2733
3130
  for (let oi = 0; oi < objects.length; oi++) {
2734
3131
  const obj = objects[oi];
2735
- if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2736
- const objName = strName7(obj.name) ?? `#${oi}`;
3132
+ if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
3133
+ const objName = strName8(obj.name) ?? `#${oi}`;
2737
3134
  for (const [key, lv] of Object.entries(obj.listViews)) {
2738
3135
  checkListChart(
2739
3136
  lv,
@@ -2745,19 +3142,19 @@ function validateChartBindings(stack) {
2745
3142
  const pages = asArray10(stack.pages);
2746
3143
  for (let pi = 0; pi < pages.length; pi++) {
2747
3144
  const page = pages[pi];
2748
- if (!isRec6(page)) continue;
2749
- const pageName = strName7(page.name) ?? `#${pi}`;
3145
+ if (!isRec7(page)) continue;
3146
+ const pageName = strName8(page.name) ?? `#${pi}`;
2750
3147
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2751
- const props = isRec6(component.properties) ? component.properties : void 0;
2752
- if (!props || !strName7(props.dataset)) continue;
2753
- const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
2754
- const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
3148
+ const props = isRec7(component.properties) ? component.properties : void 0;
3149
+ if (!props || !strName8(props.dataset)) continue;
3150
+ const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
3151
+ const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
2755
3152
  check({
2756
- dataset: strName7(props.dataset),
3153
+ dataset: strName8(props.dataset),
2757
3154
  dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
2758
3155
  values: { names: strList2(props.values), path: `${path}.properties.values` },
2759
3156
  series: [...axisRefs, ...seriesRefs],
2760
- where: `page "${pageName}" \xB7 ${strName7(component.type) ?? "chart"}`,
3157
+ where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
2761
3158
  path: `${path}.properties`
2762
3159
  });
2763
3160
  }
@@ -2824,7 +3221,7 @@ function asArray12(v) {
2824
3221
  }
2825
3222
  return [];
2826
3223
  }
2827
- function strName8(v) {
3224
+ function strName9(v) {
2828
3225
  return typeof v === "string" && v.length > 0 ? v : void 0;
2829
3226
  }
2830
3227
  function collectNavExposures(stack) {
@@ -2833,18 +3230,18 @@ function collectNavExposures(stack) {
2833
3230
  for (let ai = 0; ai < apps.length; ai++) {
2834
3231
  const app = apps[ai];
2835
3232
  if (!app || typeof app !== "object") continue;
2836
- const appName = strName8(app.name) ?? `#${ai}`;
3233
+ const appName = strName9(app.name) ?? `#${ai}`;
2837
3234
  const walk = (items, basePath) => {
2838
3235
  const navItems = asArray12(items);
2839
3236
  for (let ni = 0; ni < navItems.length; ni++) {
2840
3237
  const nav = navItems[ni];
2841
3238
  if (!nav || typeof nav !== "object") continue;
2842
3239
  const navPath = `${basePath}[${ni}]`;
2843
- const objectName = strName8(nav.objectName);
3240
+ const objectName = strName9(nav.objectName);
2844
3241
  if (nav.type === "object" && objectName) {
2845
3242
  out.push({
2846
3243
  objectName,
2847
- where: `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
3244
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
2848
3245
  path: `${navPath}.objectName`
2849
3246
  });
2850
3247
  }
@@ -2868,7 +3265,7 @@ function validateNavAccess(stack) {
2868
3265
  if (exposures.length === 0) return findings;
2869
3266
  const ownObjects = /* @__PURE__ */ new Set();
2870
3267
  for (const obj of asArray12(stack.objects)) {
2871
- const n = strName8(obj.name);
3268
+ const n = strName9(obj.name);
2872
3269
  if (n) ownObjects.add(n);
2873
3270
  }
2874
3271
  const readable = /* @__PURE__ */ new Set();
@@ -2898,13 +3295,13 @@ function validateNavAccess(stack) {
2898
3295
 
2899
3296
  // src/validate-nav-target-refs.ts
2900
3297
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2901
- var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3298
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2902
3299
  function asArray13(v) {
2903
- if (Array.isArray(v)) return v.filter(isRec7);
2904
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
3300
+ if (Array.isArray(v)) return v.filter(isRec8);
3301
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
2905
3302
  return [];
2906
3303
  }
2907
- function strName9(v) {
3304
+ function strName10(v) {
2908
3305
  return typeof v === "string" && v.length > 0 ? v : void 0;
2909
3306
  }
2910
3307
  var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
@@ -2916,14 +3313,14 @@ var NAV_TARGETS = [
2916
3313
  function namesOf(collection) {
2917
3314
  const out = /* @__PURE__ */ new Set();
2918
3315
  for (const entry of asArray13(collection)) {
2919
- const n = strName9(entry.name);
3316
+ const n = strName10(entry.name);
2920
3317
  if (n) out.add(n);
2921
3318
  }
2922
3319
  return out;
2923
3320
  }
2924
3321
  function validateNavTargetRefs(stack) {
2925
3322
  const findings = [];
2926
- if (!isRec7(stack)) return findings;
3323
+ if (!isRec8(stack)) return findings;
2927
3324
  const apps = asArray13(stack.apps);
2928
3325
  if (apps.length === 0) return findings;
2929
3326
  const declared = /* @__PURE__ */ new Map();
@@ -2931,16 +3328,16 @@ function validateNavTargetRefs(stack) {
2931
3328
  declared.set(collection, namesOf(stack[collection]));
2932
3329
  }
2933
3330
  for (const [ai, app] of apps.entries()) {
2934
- const appName = strName9(app.name) ?? `#${ai}`;
3331
+ const appName = strName10(app.name) ?? `#${ai}`;
2935
3332
  const walk = (items, basePath) => {
2936
3333
  if (!Array.isArray(items)) return;
2937
3334
  for (const [ni, raw] of items.entries()) {
2938
- if (!isRec7(raw)) continue;
3335
+ if (!isRec8(raw)) continue;
2939
3336
  const nav = raw;
2940
3337
  const navPath = `${basePath}[${ni}]`;
2941
3338
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
2942
3339
  if (nav.type !== type) continue;
2943
- const target = strName9(nav[prop]);
3340
+ const target = strName10(nav[prop]);
2944
3341
  if (!target || isInterpolated2(target)) continue;
2945
3342
  const known = declared.get(collection);
2946
3343
  if (known.has(target)) continue;
@@ -2948,7 +3345,7 @@ function validateNavTargetRefs(stack) {
2948
3345
  findings.push({
2949
3346
  severity: "warning",
2950
3347
  rule: NAV_TARGET_UNRESOLVED,
2951
- where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
3348
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
2952
3349
  path: `${navPath}.${prop}`,
2953
3350
  message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
2954
3351
  hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
@@ -2969,42 +3366,42 @@ function validateNavTargetRefs(stack) {
2969
3366
  // src/validate-nav-object-servability.ts
2970
3367
  import { canServeApiOperation } from "@objectstack/spec/data";
2971
3368
  var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2972
- var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3369
+ var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2973
3370
  function asArray14(v) {
2974
- if (Array.isArray(v)) return v.filter(isRec8);
2975
- if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
3371
+ if (Array.isArray(v)) return v.filter(isRec9);
3372
+ if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
2976
3373
  return [];
2977
3374
  }
2978
- function strName10(v) {
3375
+ function strName11(v) {
2979
3376
  return typeof v === "string" && v.length > 0 ? v : void 0;
2980
3377
  }
2981
3378
  var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2982
3379
  function validateNavObjectServability(stack) {
2983
3380
  const findings = [];
2984
- if (!isRec8(stack)) return findings;
3381
+ if (!isRec9(stack)) return findings;
2985
3382
  const apps = asArray14(stack.apps);
2986
3383
  if (apps.length === 0) return findings;
2987
3384
  const ownEnable = /* @__PURE__ */ new Map();
2988
3385
  const objects = asArray14(stack.objects);
2989
3386
  for (const [oi, obj] of objects.entries()) {
2990
- const n = strName10(obj.name);
3387
+ const n = strName11(obj.name);
2991
3388
  if (!n) continue;
2992
3389
  ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
2993
3390
  }
2994
3391
  if (ownEnable.size === 0) return findings;
2995
3392
  for (const [ai, app] of apps.entries()) {
2996
- const appName = strName10(app.name) ?? `#${ai}`;
3393
+ const appName = strName11(app.name) ?? `#${ai}`;
2997
3394
  const walk = (items, basePath) => {
2998
3395
  if (!Array.isArray(items)) return;
2999
3396
  for (const [ni, raw] of items.entries()) {
3000
- if (!isRec8(raw)) continue;
3397
+ if (!isRec9(raw)) continue;
3001
3398
  const nav = raw;
3002
3399
  const navPath = `${basePath}[${ni}]`;
3003
3400
  if (nav.type === "object") {
3004
- const target = strName10(nav.objectName);
3401
+ const target = strName11(nav.objectName);
3005
3402
  const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
3006
3403
  if (target && declared && !canServeApiOperation(declared.enable, "list")) {
3007
- const enable = isRec8(declared.enable) ? declared.enable : {};
3404
+ const enable = isRec9(declared.enable) ? declared.enable : {};
3008
3405
  const apiDisabled = enable.apiEnabled === false;
3009
3406
  const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
3010
3407
  const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
@@ -3012,7 +3409,7 @@ function validateNavObjectServability(stack) {
3012
3409
  findings.push({
3013
3410
  severity: "error",
3014
3411
  rule: NAV_OBJECT_UNSERVABLE,
3015
- where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3412
+ where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
3016
3413
  // The nav entry is where the dead row is authored; the `enable`
3017
3414
  // key that condemns it is named in the message, because the fix
3018
3415
  // may belong at either end.
@@ -3039,27 +3436,27 @@ import { expandViewContainer } from "@objectstack/spec";
3039
3436
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
3040
3437
 
3041
3438
  // src/view-walk.ts
3042
- function isRec9(v) {
3439
+ function isRec10(v) {
3043
3440
  return !!v && typeof v === "object" && !Array.isArray(v);
3044
3441
  }
3045
- function strName11(v) {
3442
+ function strName12(v) {
3046
3443
  return typeof v === "string" && v.length > 0 ? v : void 0;
3047
3444
  }
3048
3445
  function viewObjectName(view) {
3049
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
3446
+ return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
3050
3447
  }
3051
3448
  function viewContainerSites(view, basePath) {
3052
- if (!isRec9(view)) return [];
3449
+ if (!isRec10(view)) return [];
3053
3450
  const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3054
- if (isRec9(view.form)) {
3451
+ if (isRec10(view.form)) {
3055
3452
  sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3056
3453
  }
3057
3454
  for (const key of ["listViews", "formViews"]) {
3058
3455
  const container = view[key];
3059
- if (!isRec9(container)) continue;
3456
+ if (!isRec10(container)) continue;
3060
3457
  const kind = key === "listViews" ? "listView" : "formView";
3061
3458
  for (const [subKey, sub] of Object.entries(container)) {
3062
- if (!isRec9(sub)) continue;
3459
+ if (!isRec10(sub)) continue;
3063
3460
  sites.push({
3064
3461
  view: sub,
3065
3462
  path: `${basePath}.${key}.${subKey}`,
@@ -3077,18 +3474,18 @@ function formViewSites(view, basePath) {
3077
3474
  // src/validate-translation-references.ts
3078
3475
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
3079
3476
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
3080
- function isRec10(v) {
3477
+ function isRec11(v) {
3081
3478
  return !!v && typeof v === "object" && !Array.isArray(v);
3082
3479
  }
3083
3480
  function asArray15(v) {
3084
3481
  if (Array.isArray(v)) return v;
3085
- if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
3482
+ if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
3086
3483
  return [];
3087
3484
  }
3088
- function strName12(v) {
3485
+ function strName13(v) {
3089
3486
  return typeof v === "string" && v.length > 0 ? v : void 0;
3090
3487
  }
3091
- function distance5(a, b) {
3488
+ function distance6(a, b) {
3092
3489
  const m = a.length;
3093
3490
  const n = b.length;
3094
3491
  if (m === 0) return n;
@@ -3104,7 +3501,7 @@ function distance5(a, b) {
3104
3501
  }
3105
3502
  return prev[n];
3106
3503
  }
3107
- function suggest6(target, known) {
3504
+ function suggest7(target, known) {
3108
3505
  const names = [...known];
3109
3506
  const segmentMatch = names.find(
3110
3507
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -3113,7 +3510,7 @@ function suggest6(target, known) {
3113
3510
  let best;
3114
3511
  let bestScore = Infinity;
3115
3512
  for (const candidate of names) {
3116
- const d = distance5(target, candidate);
3513
+ const d = distance6(target, candidate);
3117
3514
  if (d < bestScore) {
3118
3515
  bestScore = d;
3119
3516
  best = candidate;
@@ -3146,33 +3543,33 @@ function collectViewRecord(view, factsFor) {
3146
3543
  const addSections = (container, binding) => {
3147
3544
  if (!binding) return;
3148
3545
  for (const section of asArray15(container.sections)) {
3149
- const sectionName = strName12(section.name);
3546
+ const sectionName = strName13(section.name);
3150
3547
  if (sectionName) factsFor(binding).sections.add(sectionName);
3151
3548
  }
3152
3549
  };
3153
- const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
3154
- if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3155
- addView(recordObject ?? listBinding, strName12(view.name));
3550
+ const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
3551
+ if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3552
+ addView(recordObject ?? listBinding, strName13(view.name));
3156
3553
  const named = namedViewKeys(view);
3157
3554
  for (const family of ["listViews", "formViews"]) {
3158
3555
  const container = view[family];
3159
- if (!isRec10(container)) continue;
3556
+ if (!isRec11(container)) continue;
3160
3557
  const registryKeys = family === "listViews" ? named.list : named.form;
3161
3558
  let at = 0;
3162
3559
  for (const sub of Object.values(container)) {
3163
3560
  if (!sub || typeof sub !== "object") continue;
3164
3561
  const registryKey = registryKeys[at++];
3165
- if (!isRec10(sub)) continue;
3562
+ if (!isRec11(sub)) continue;
3166
3563
  const binding = bindingOf(sub) ?? listBinding;
3167
3564
  addView(binding, registryKey);
3168
3565
  addSections(sub, binding);
3169
3566
  }
3170
3567
  }
3171
- if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3568
+ if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3172
3569
  addSections(view, recordObject ?? listBinding);
3173
3570
  }
3174
3571
  function defaultListViewKey(object, container) {
3175
- if (!object || !isRec10(container.list)) return void 0;
3572
+ if (!object || !isRec11(container.list)) return void 0;
3176
3573
  const item = expandViewContainer(object, container).find(
3177
3574
  (i) => i.viewKind === "list" && i.isDefault
3178
3575
  );
@@ -3184,7 +3581,7 @@ function namedViewKeys(container) {
3184
3581
  const object = "probe";
3185
3582
  const prefix = `${object}.`;
3186
3583
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
3187
- const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3584
+ const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3188
3585
  const listCount = countEntries(container.listViews);
3189
3586
  const formCount = countEntries(container.formViews);
3190
3587
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -3202,14 +3599,14 @@ function readOptions(field) {
3202
3599
  values.add(opt);
3203
3600
  continue;
3204
3601
  }
3205
- if (!isRec10(opt)) continue;
3206
- const value = strName12(opt.value);
3602
+ if (!isRec11(opt)) continue;
3603
+ const value = strName13(opt.value);
3207
3604
  if (!value) continue;
3208
3605
  values.add(value);
3209
- const label2 = strName12(opt.label);
3606
+ const label2 = strName13(opt.label);
3210
3607
  if (label2) byLabel.set(label2.toLowerCase(), value);
3211
3608
  }
3212
- } else if (isRec10(raw)) {
3609
+ } else if (isRec11(raw)) {
3213
3610
  for (const [value, label2] of Object.entries(raw)) {
3214
3611
  values.add(value);
3215
3612
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -3230,23 +3627,23 @@ function buildUniverse(stack) {
3230
3627
  return facts;
3231
3628
  };
3232
3629
  for (const obj of asArray15(stack.objects)) {
3233
- const objectName = strName12(obj.name);
3630
+ const objectName = strName13(obj.name);
3234
3631
  if (!objectName) continue;
3235
3632
  const facts = factsFor(objectName);
3236
3633
  for (const field of asArray15(obj.fields)) {
3237
- const fieldName = strName12(field.name);
3634
+ const fieldName = strName13(field.name);
3238
3635
  if (fieldName) facts.fields.set(fieldName, field);
3239
3636
  }
3240
3637
  for (const action of asArray15(obj.actions)) {
3241
- const actionName = strName12(action.name);
3638
+ const actionName = strName13(action.name);
3242
3639
  if (actionName) facts.actions.set(actionName, action);
3243
3640
  }
3244
3641
  for (const view of asArray15(obj.views)) {
3245
- collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
3642
+ collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
3246
3643
  }
3247
3644
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
3248
3645
  for (const group of asArray15(obj.fieldGroups)) {
3249
- const key = strName12(group.key) ?? strName12(group.name);
3646
+ const key = strName13(group.key) ?? strName13(group.name);
3250
3647
  if (key) facts.sections.add(key);
3251
3648
  }
3252
3649
  }
@@ -3257,10 +3654,10 @@ function buildUniverse(stack) {
3257
3654
  for (let pi = 0; pi < pages.length; pi++) {
3258
3655
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
3259
3656
  if (!walked.objectName) continue;
3260
- const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3657
+ const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
3261
3658
  if (!props) continue;
3262
3659
  for (const section of asArray15(props.sections)) {
3263
- const sectionName = strName12(section.name);
3660
+ const sectionName = strName13(section.name);
3264
3661
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
3265
3662
  }
3266
3663
  }
@@ -3268,9 +3665,9 @@ function buildUniverse(stack) {
3268
3665
  const globalActions = /* @__PURE__ */ new Map();
3269
3666
  const actionOwners = /* @__PURE__ */ new Map();
3270
3667
  for (const action of asArray15(stack.actions)) {
3271
- const actionName = strName12(action.name);
3668
+ const actionName = strName13(action.name);
3272
3669
  if (!actionName) continue;
3273
- const owner = strName12(action.objectName) ?? strName12(action.object);
3670
+ const owner = strName13(action.objectName) ?? strName13(action.object);
3274
3671
  if (owner) {
3275
3672
  factsFor(owner).actions.set(actionName, action);
3276
3673
  actionOwners.set(actionName, owner);
@@ -3285,19 +3682,19 @@ function buildUniverse(stack) {
3285
3682
  }
3286
3683
  const apps = /* @__PURE__ */ new Map();
3287
3684
  for (const app of asArray15(stack.apps)) {
3288
- const appName = strName12(app.name);
3685
+ const appName = strName13(app.name);
3289
3686
  if (!appName) continue;
3290
3687
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
3291
3688
  const walkNav = (items) => {
3292
3689
  for (const item of asArray15(items)) {
3293
- const id = strName12(item.id);
3690
+ const id = strName13(item.id);
3294
3691
  if (id) navIds.add(id);
3295
3692
  if (item.children) walkNav(item.children);
3296
3693
  }
3297
3694
  };
3298
3695
  walkNav(app.navigation);
3299
3696
  for (const area of asArray15(app.areas)) {
3300
- const areaId = strName12(area.id);
3697
+ const areaId = strName13(area.id);
3301
3698
  if (areaId) navIds.add(areaId);
3302
3699
  walkNav(area.navigation);
3303
3700
  }
@@ -3305,20 +3702,20 @@ function buildUniverse(stack) {
3305
3702
  }
3306
3703
  const dashboards = /* @__PURE__ */ new Map();
3307
3704
  for (const dash of asArray15(stack.dashboards)) {
3308
- const dashName = strName12(dash.name);
3705
+ const dashName = strName13(dash.name);
3309
3706
  if (!dashName) continue;
3310
3707
  const widgets = /* @__PURE__ */ new Set();
3311
3708
  for (const widget of asArray15(dash.widgets)) {
3312
- const id = strName12(widget.id) ?? strName12(widget.name);
3709
+ const id = strName13(widget.id) ?? strName13(widget.name);
3313
3710
  if (id) widgets.add(id);
3314
3711
  }
3315
3712
  const actions = /* @__PURE__ */ new Set();
3316
3713
  const headerActions = [
3317
- ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3714
+ ...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
3318
3715
  ...asArray15(dash.actions)
3319
3716
  ];
3320
3717
  for (const action of headerActions) {
3321
- const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
3718
+ const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
3322
3719
  if (key) actions.add(key);
3323
3720
  }
3324
3721
  dashboards.set(dashName, { widgets, actions });
@@ -3330,7 +3727,7 @@ function localePath(bundleIndex, locale) {
3330
3727
  }
3331
3728
  function validateTranslationReferences(stack) {
3332
3729
  const findings = [];
3333
- if (!isRec10(stack)) return findings;
3730
+ if (!isRec11(stack)) return findings;
3334
3731
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3335
3732
  if (bundles.length === 0) return findings;
3336
3733
  const universe = buildUniverse(stack);
@@ -3339,13 +3736,13 @@ function validateTranslationReferences(stack) {
3339
3736
  };
3340
3737
  for (let bi = 0; bi < bundles.length; bi++) {
3341
3738
  const bundle = bundles[bi];
3342
- if (!isRec10(bundle)) continue;
3739
+ if (!isRec11(bundle)) continue;
3343
3740
  for (const [locale, rawData] of Object.entries(bundle)) {
3344
- if (!isRec10(rawData)) continue;
3741
+ if (!isRec11(rawData)) continue;
3345
3742
  const base = localePath(bi, locale);
3346
3743
  const inLocale = `locale "${locale}"`;
3347
3744
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
3348
- if (!isRec10(rawNode)) continue;
3745
+ if (!isRec11(rawNode)) continue;
3349
3746
  const objPath = `${base}.objects.${objectName}`;
3350
3747
  const facts = universe.objects.get(objectName);
3351
3748
  if (!facts) {
@@ -3353,7 +3750,7 @@ function validateTranslationReferences(stack) {
3353
3750
  orphan(
3354
3751
  `${inLocale} \xB7 object "${objectName}"`,
3355
3752
  objPath,
3356
- hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
3753
+ hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest7(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest7(objectName, universe.objects.keys()),
3357
3754
  `Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
3358
3755
  );
3359
3756
  continue;
@@ -3366,12 +3763,12 @@ function validateTranslationReferences(stack) {
3366
3763
  orphan(
3367
3764
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
3368
3765
  fieldPath,
3369
- `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest6(fieldName, facts.fields.keys()),
3766
+ `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest7(fieldName, facts.fields.keys()),
3370
3767
  `Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
3371
3768
  );
3372
3769
  continue;
3373
3770
  }
3374
- if (!isRec10(rawField)) continue;
3771
+ if (!isRec11(rawField)) continue;
3375
3772
  checkOptionKeys(findings, {
3376
3773
  optionMap: rawField.options,
3377
3774
  field,
@@ -3386,7 +3783,7 @@ function validateTranslationReferences(stack) {
3386
3783
  orphan(
3387
3784
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
3388
3785
  `${objPath}._views.${viewName}`,
3389
- `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest6(viewName, facts.views),
3786
+ `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest7(viewName, facts.views),
3390
3787
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
3391
3788
  );
3392
3789
  }
@@ -3395,7 +3792,7 @@ function validateTranslationReferences(stack) {
3395
3792
  orphan(
3396
3793
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
3397
3794
  `${objPath}._sections.${sectionName}`,
3398
- `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest6(sectionName, facts.sections),
3795
+ `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest7(sectionName, facts.sections),
3399
3796
  `Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
3400
3797
  );
3401
3798
  }
@@ -3406,7 +3803,7 @@ function validateTranslationReferences(stack) {
3406
3803
  orphan(
3407
3804
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
3408
3805
  actionPath,
3409
- `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest6(actionName, facts.actions.keys()),
3806
+ `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest7(actionName, facts.actions.keys()),
3410
3807
  `Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
3411
3808
  );
3412
3809
  continue;
@@ -3428,7 +3825,7 @@ function validateTranslationReferences(stack) {
3428
3825
  orphan(
3429
3826
  `${inLocale} \xB7 global action "${actionName}"`,
3430
3827
  actionPath,
3431
- owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest6(actionName, universe.globalActions.keys()),
3828
+ owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest7(actionName, universe.globalActions.keys()),
3432
3829
  owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
3433
3830
  );
3434
3831
  continue;
@@ -3448,18 +3845,18 @@ function validateTranslationReferences(stack) {
3448
3845
  orphan(
3449
3846
  `${inLocale} \xB7 app "${appName}"`,
3450
3847
  appPath,
3451
- `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest6(appName, universe.apps.keys()),
3848
+ `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest7(appName, universe.apps.keys()),
3452
3849
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
3453
3850
  );
3454
3851
  continue;
3455
3852
  }
3456
- if (!isRec10(rawApp)) continue;
3853
+ if (!isRec11(rawApp)) continue;
3457
3854
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
3458
3855
  if (navIds.has(navId)) continue;
3459
3856
  orphan(
3460
3857
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
3461
3858
  `${appPath}.navigation.${navId}`,
3462
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
3859
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
3463
3860
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
3464
3861
  );
3465
3862
  }
@@ -3471,18 +3868,18 @@ function validateTranslationReferences(stack) {
3471
3868
  orphan(
3472
3869
  `${inLocale} \xB7 dashboard "${dashName}"`,
3473
3870
  dashPath,
3474
- `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest6(dashName, universe.dashboards.keys()),
3871
+ `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest7(dashName, universe.dashboards.keys()),
3475
3872
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
3476
3873
  );
3477
3874
  continue;
3478
3875
  }
3479
- if (!isRec10(rawDash)) continue;
3876
+ if (!isRec11(rawDash)) continue;
3480
3877
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
3481
3878
  if (dash.widgets.has(widgetId)) continue;
3482
3879
  orphan(
3483
3880
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
3484
3881
  `${dashPath}.widgets.${widgetId}`,
3485
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
3882
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
3486
3883
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
3487
3884
  );
3488
3885
  }
@@ -3491,7 +3888,7 @@ function validateTranslationReferences(stack) {
3491
3888
  orphan(
3492
3889
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
3493
3890
  `${dashPath}.actions.${actionKey}`,
3494
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
3891
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
3495
3892
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
3496
3893
  );
3497
3894
  }
@@ -3501,7 +3898,7 @@ function validateTranslationReferences(stack) {
3501
3898
  return findings;
3502
3899
  }
3503
3900
  function asRecord(v) {
3504
- return isRec10(v) ? v : {};
3901
+ return isRec11(v) ? v : {};
3505
3902
  }
3506
3903
  function checkOptionKeys(findings, ctx) {
3507
3904
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3513,7 +3910,7 @@ function checkOptionKeys(findings, ctx) {
3513
3910
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3514
3911
  where: ctx.where,
3515
3912
  path: ctx.path,
3516
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3913
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName13(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3517
3914
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3518
3915
  });
3519
3916
  return;
@@ -3526,17 +3923,17 @@ function checkOptionKeys(findings, ctx) {
3526
3923
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3527
3924
  where: ctx.where,
3528
3925
  path: `${ctx.path}.${key}`,
3529
- message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest6(key, declared.values),
3926
+ message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest7(key, declared.values),
3530
3927
  hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
3531
3928
  });
3532
3929
  }
3533
3930
  }
3534
3931
  function checkActionParams(findings, ctx) {
3535
- const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3932
+ const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
3536
3933
  if (rawParams.length === 0) return;
3537
3934
  const declared = /* @__PURE__ */ new Set();
3538
3935
  for (const param of asArray15(ctx.action.params)) {
3539
- const name = strName12(param.name) ?? strName12(param.field);
3936
+ const name = strName13(param.name) ?? strName13(param.field);
3540
3937
  if (name) declared.add(name);
3541
3938
  }
3542
3939
  for (const paramName of rawParams) {
@@ -3546,40 +3943,40 @@ function checkActionParams(findings, ctx) {
3546
3943
  rule: TRANSLATION_TARGET_UNKNOWN,
3547
3944
  where: `${ctx.where} \xB7 param "${paramName}"`,
3548
3945
  path: `${ctx.path}.params.${paramName}`,
3549
- message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest6(paramName, declared),
3946
+ message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest7(paramName, declared),
3550
3947
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
3551
3948
  });
3552
3949
  }
3553
3950
  }
3554
3951
 
3555
3952
  // src/collection-entries.ts
3556
- function isRec11(v) {
3953
+ function isRec12(v) {
3557
3954
  return !!v && typeof v === "object" && !Array.isArray(v);
3558
3955
  }
3559
3956
  function collectionEntries(v, base) {
3560
3957
  if (Array.isArray(v)) {
3561
3958
  const out = [];
3562
3959
  for (let i = 0; i < v.length; i++) {
3563
- if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3960
+ if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3564
3961
  }
3565
3962
  return out;
3566
3963
  }
3567
- if (isRec11(v)) {
3568
- return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3964
+ if (isRec12(v)) {
3965
+ return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3569
3966
  }
3570
3967
  return [];
3571
3968
  }
3572
3969
 
3573
3970
  // src/validate-translatable-sections.ts
3574
3971
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3575
- function isRec12(v) {
3972
+ function isRec13(v) {
3576
3973
  return !!v && typeof v === "object" && !Array.isArray(v);
3577
3974
  }
3578
- function strName13(v) {
3975
+ function strName14(v) {
3579
3976
  return typeof v === "string" && v.length > 0 ? v : void 0;
3580
3977
  }
3581
3978
  function viewLabel(view) {
3582
- const name = strName13(view.name);
3979
+ const name = strName14(view.name);
3583
3980
  return name ? `view "${name}"` : "";
3584
3981
  }
3585
3982
  function joinWhere(...parts) {
@@ -3587,7 +3984,7 @@ function joinWhere(...parts) {
3587
3984
  }
3588
3985
  function collectViewSites(view, basePath, label2, sites) {
3589
3986
  const recordObject = viewObjectName(view);
3590
- const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3987
+ const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3591
3988
  for (const site of viewContainerSites(view, basePath)) {
3592
3989
  sites.push({
3593
3990
  path: `${site.path}.sections`,
@@ -3601,11 +3998,11 @@ function translatedObjectNames(stack) {
3601
3998
  const out = /* @__PURE__ */ new Set();
3602
3999
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3603
4000
  for (const bundle of bundles) {
3604
- if (!isRec12(bundle)) continue;
4001
+ if (!isRec13(bundle)) continue;
3605
4002
  for (const data of Object.values(bundle)) {
3606
- if (!isRec12(data) || !isRec12(data.objects)) continue;
4003
+ if (!isRec13(data) || !isRec13(data.objects)) continue;
3607
4004
  for (const [objectName, node] of Object.entries(data.objects)) {
3608
- if (isRec12(node)) out.add(objectName);
4005
+ if (isRec13(node)) out.add(objectName);
3609
4006
  }
3610
4007
  }
3611
4008
  }
@@ -3617,22 +4014,22 @@ function suggestedName(label2) {
3617
4014
  }
3618
4015
  function validateTranslatableSections(stack) {
3619
4016
  const findings = [];
3620
- if (!isRec12(stack)) return findings;
4017
+ if (!isRec13(stack)) return findings;
3621
4018
  const translated = translatedObjectNames(stack);
3622
4019
  if (translated.size === 0) return findings;
3623
4020
  const sites = [];
3624
4021
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3625
- const objectName = strName13(obj.name);
4022
+ const objectName = strName14(obj.name);
3626
4023
  if (!objectName) continue;
3627
4024
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3628
4025
  collectViewSites(
3629
- { ...view, object: strName13(view.object) ?? objectName },
4026
+ { ...view, object: strName14(view.object) ?? objectName },
3630
4027
  path,
3631
4028
  viewLabel(view),
3632
4029
  sites
3633
4030
  );
3634
4031
  }
3635
- if (isRec12(obj.listViews)) {
4032
+ if (isRec13(obj.listViews)) {
3636
4033
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3637
4034
  }
3638
4035
  }
@@ -3640,13 +4037,13 @@ function validateTranslatableSections(stack) {
3640
4037
  collectViewSites(view, path, viewLabel(view), sites);
3641
4038
  }
3642
4039
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3643
- const pageName = strName13(page.name);
4040
+ const pageName = strName14(page.name);
3644
4041
  const pageLabel = pageName ? `page "${pageName}"` : "";
3645
4042
  for (const walked of walkPageComponents(page, pagePath)) {
3646
4043
  if (!walked.objectName) continue;
3647
- const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
4044
+ const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
3648
4045
  if (!props) continue;
3649
- const type = strName13(walked.component.type) ?? "component";
4046
+ const type = strName14(walked.component.type) ?? "component";
3650
4047
  sites.push({
3651
4048
  path: `${walked.path}.properties.sections`,
3652
4049
  surface: joinWhere(pageLabel, type),
@@ -3661,9 +4058,9 @@ function validateTranslatableSections(stack) {
3661
4058
  if (!Array.isArray(site.sections)) continue;
3662
4059
  for (let i = 0; i < site.sections.length; i++) {
3663
4060
  const section = site.sections[i];
3664
- if (!isRec12(section)) continue;
3665
- if (strName13(section.name)) continue;
3666
- const heading = strName13(section.label);
4061
+ if (!isRec13(section)) continue;
4062
+ if (strName14(section.name)) continue;
4063
+ const heading = strName14(section.label);
3667
4064
  if (!heading) continue;
3668
4065
  const slug = suggestedName(heading);
3669
4066
  findings.push({
@@ -3681,10 +4078,10 @@ function validateTranslatableSections(stack) {
3681
4078
 
3682
4079
  // src/flow-walk.ts
3683
4080
  import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
3684
- function isRec13(v) {
4081
+ function isRec14(v) {
3685
4082
  return !!v && typeof v === "object" && !Array.isArray(v);
3686
4083
  }
3687
- function strName14(v) {
4084
+ function strName15(v) {
3688
4085
  return typeof v === "string" && v.length > 0 ? v : void 0;
3689
4086
  }
3690
4087
  var REGION_SLOTS = new Map(
@@ -3693,10 +4090,10 @@ var REGION_SLOTS = new Map(
3693
4090
  var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
3694
4091
  var MAX_REGION_DEPTH = 16;
3695
4092
  function flowNodeLabel(node, index) {
3696
- return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
4093
+ return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
3697
4094
  }
3698
4095
  function stripRegions(config) {
3699
- if (!isRec13(config)) return void 0;
4096
+ if (!isRec14(config)) return void 0;
3700
4097
  let out;
3701
4098
  for (const key of Object.keys(config)) {
3702
4099
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3707,11 +4104,11 @@ function stripRegions(config) {
3707
4104
  }
3708
4105
  function walkFlowNodes(flow, flowPath) {
3709
4106
  const out = [];
3710
- if (!isRec13(flow)) return out;
4107
+ if (!isRec14(flow)) return out;
3711
4108
  const visitList = (nodes, basePath, trail, depth) => {
3712
4109
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3713
4110
  nodes.forEach((raw, index) => {
3714
- if (!isRec13(raw)) return;
4111
+ if (!isRec14(raw)) return;
3715
4112
  const path = `${basePath}[${index}]`;
3716
4113
  out.push({
3717
4114
  node: raw,
@@ -3720,9 +4117,9 @@ function walkFlowNodes(flow, flowPath) {
3720
4117
  regionTrail: trail,
3721
4118
  depth
3722
4119
  });
3723
- const type = strName14(raw.type);
4120
+ const type = strName15(raw.type);
3724
4121
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3725
- if (!slots || !isRec13(raw.config)) return;
4122
+ if (!slots || !isRec14(raw.config)) return;
3726
4123
  const config = raw.config;
3727
4124
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3728
4125
  for (const slot of slots) {
@@ -3730,8 +4127,8 @@ function walkFlowNodes(flow, flowPath) {
3730
4127
  if (slot === "branches") {
3731
4128
  if (!Array.isArray(value)) continue;
3732
4129
  value.forEach((branch, b) => {
3733
- if (!isRec13(branch)) return;
3734
- const branchName = strName14(branch.name) ?? `#${b}`;
4130
+ if (!isRec14(branch)) return;
4131
+ const branchName = strName15(branch.name) ?? `#${b}`;
3735
4132
  visitList(
3736
4133
  branch.nodes,
3737
4134
  `${path}.config.branches[${b}].nodes`,
@@ -3741,7 +4138,7 @@ function walkFlowNodes(flow, flowPath) {
3741
4138
  });
3742
4139
  continue;
3743
4140
  }
3744
- if (!isRec13(value)) continue;
4141
+ if (!isRec14(value)) continue;
3745
4142
  visitList(
3746
4143
  value.nodes,
3747
4144
  `${path}.config.${slot}.nodes`,
@@ -3982,7 +4379,7 @@ function asArray17(v) {
3982
4379
  }
3983
4380
  return [];
3984
4381
  }
3985
- function strName15(v) {
4382
+ function strName16(v) {
3986
4383
  return typeof v === "string" && v.length > 0 ? v : void 0;
3987
4384
  }
3988
4385
  function surfaceOf(v) {
@@ -3993,17 +4390,17 @@ function validateAiSurfaceAffinity(stack) {
3993
4390
  if (!stack || typeof stack !== "object") return findings;
3994
4391
  const skillsByName = /* @__PURE__ */ new Map();
3995
4392
  for (const skill of asArray17(stack.skills)) {
3996
- const n = strName15(skill.name);
4393
+ const n = strName16(skill.name);
3997
4394
  if (n) skillsByName.set(n, skill);
3998
4395
  }
3999
4396
  const agents = asArray17(stack.agents);
4000
4397
  for (let ai = 0; ai < agents.length; ai++) {
4001
4398
  const agent = agents[ai];
4002
- const agentName = strName15(agent.name) ?? `#${ai}`;
4399
+ const agentName = strName16(agent.name) ?? `#${ai}`;
4003
4400
  const agentSurface = surfaceOf(agent.surface);
4004
4401
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
4005
4402
  for (let si = 0; si < skillRefs.length; si++) {
4006
- const ref = strName15(skillRefs[si]);
4403
+ const ref = strName16(skillRefs[si]);
4007
4404
  if (!ref) continue;
4008
4405
  const skill = skillsByName.get(ref);
4009
4406
  if (!skill) continue;
@@ -4032,10 +4429,10 @@ function asArray18(v) {
4032
4429
  }
4033
4430
  return [];
4034
4431
  }
4035
- function strName16(v) {
4432
+ function strName17(v) {
4036
4433
  return typeof v === "string" && v.length > 0 ? v : void 0;
4037
4434
  }
4038
- function distance6(a, b) {
4435
+ function distance7(a, b) {
4039
4436
  const m = a.length;
4040
4437
  const n = b.length;
4041
4438
  if (m === 0) return n;
@@ -4051,14 +4448,14 @@ function distance6(a, b) {
4051
4448
  }
4052
4449
  return prev[n];
4053
4450
  }
4054
- function suggest7(target, known) {
4451
+ function suggest8(target, known) {
4055
4452
  for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
4056
4453
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
4057
4454
  }
4058
4455
  let best;
4059
4456
  let bestScore = Infinity;
4060
4457
  for (const candidate of known) {
4061
- const d = distance6(target, candidate);
4458
+ const d = distance7(target, candidate);
4062
4459
  if (d < bestScore) {
4063
4460
  bestScore = d;
4064
4461
  best = candidate;
@@ -4073,8 +4470,8 @@ function materialisesAsTool(action) {
4073
4470
  if (!ai || typeof ai !== "object") return false;
4074
4471
  const aiRec = ai;
4075
4472
  if (aiRec.exposed !== true) return false;
4076
- if (!strName16(aiRec.description)) return false;
4077
- const type = strName16(action.type);
4473
+ if (!strName17(aiRec.description)) return false;
4474
+ const type = strName17(action.type);
4078
4475
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
4079
4476
  if (type === "script") return Boolean(action.target || action.body);
4080
4477
  return Boolean(action.target);
@@ -4082,12 +4479,12 @@ function materialisesAsTool(action) {
4082
4479
  function collectToolUniverse(stack) {
4083
4480
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
4084
4481
  for (const tool of asArray18(stack.tools)) {
4085
- const n = strName16(tool.name);
4482
+ const n = strName17(tool.name);
4086
4483
  if (n) universe.add(n);
4087
4484
  }
4088
4485
  const addActionFamily = (actions) => {
4089
4486
  for (const action of asArray18(actions)) {
4090
- const n = strName16(action.name);
4487
+ const n = strName17(action.name);
4091
4488
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
4092
4489
  }
4093
4490
  };
@@ -4101,7 +4498,7 @@ function collectUnexposedActionNames(stack) {
4101
4498
  const names = /* @__PURE__ */ new Set();
4102
4499
  const scan = (actions) => {
4103
4500
  for (const action of asArray18(actions)) {
4104
- const n = strName16(action.name);
4501
+ const n = strName17(action.name);
4105
4502
  if (n && !materialisesAsTool(action)) names.add(n);
4106
4503
  }
4107
4504
  };
@@ -4127,10 +4524,10 @@ function validateAiToolReferences(stack) {
4127
4524
  const skills = asArray18(stack.skills);
4128
4525
  for (let si = 0; si < skills.length; si++) {
4129
4526
  const skill = skills[si];
4130
- const skillName = strName16(skill.name) ?? `#${si}`;
4527
+ const skillName = strName17(skill.name) ?? `#${si}`;
4131
4528
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
4132
4529
  for (let ti = 0; ti < refs.length; ti++) {
4133
- const ref = strName16(refs[ti]);
4530
+ const ref = strName17(refs[ti]);
4134
4531
  if (!ref || resolves(ref)) continue;
4135
4532
  const isPattern = ref.endsWith("*");
4136
4533
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -4139,7 +4536,7 @@ function validateAiToolReferences(stack) {
4139
4536
  rule: AI_SKILL_TOOL_UNRESOLVED,
4140
4537
  where: `skill "${skillName}" \xB7 tools`,
4141
4538
  path: `skills[${si}].tools[${ti}]`,
4142
- message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest7(ref, universe),
4539
+ message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest8(ref, universe),
4143
4540
  hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
4144
4541
  });
4145
4542
  }
@@ -4157,7 +4554,7 @@ function asArray19(v) {
4157
4554
  }
4158
4555
  return [];
4159
4556
  }
4160
- function strName17(v) {
4557
+ function strName18(v) {
4161
4558
  return typeof v === "string" && v.length > 0 ? v : void 0;
4162
4559
  }
4163
4560
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -4167,7 +4564,7 @@ function validateAiAgentAuthoring(stack) {
4167
4564
  const agents = asArray19(stack.agents);
4168
4565
  for (let ai = 0; ai < agents.length; ai++) {
4169
4566
  const agent = agents[ai];
4170
- const name = strName17(agent.name) ?? `#${ai}`;
4567
+ const name = strName18(agent.name) ?? `#${ai}`;
4171
4568
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
4172
4569
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
4173
4570
  findings.push({
@@ -4183,9 +4580,9 @@ function validateAiAgentAuthoring(stack) {
4183
4580
  const apps = asArray19(stack.apps);
4184
4581
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
4185
4582
  const app = apps[appIdx];
4186
- const defaultAgent = strName17(app.defaultAgent);
4583
+ const defaultAgent = strName18(app.defaultAgent);
4187
4584
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
4188
- const appName = strName17(app.name) ?? `#${appIdx}`;
4585
+ const appName = strName18(app.name) ?? `#${appIdx}`;
4189
4586
  findings.push({
4190
4587
  severity: "warning",
4191
4588
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -4201,6 +4598,38 @@ function validateAiAgentAuthoring(stack) {
4201
4598
  // src/validate-hook-body-writes.ts
4202
4599
  import { createRequire } from "module";
4203
4600
  import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
4601
+
4602
+ // src/checked-parse.ts
4603
+ function createSourceFileChecked(tsc, fileName, source, options) {
4604
+ const sourceFile = tsc.createSourceFile(
4605
+ fileName,
4606
+ source,
4607
+ options.target,
4608
+ options.setParentNodes,
4609
+ options.scriptKind
4610
+ );
4611
+ const diagnostics = sourceFile.parseDiagnostics;
4612
+ if (!diagnostics || diagnostics.length === 0) return { sourceFile };
4613
+ const first = diagnostics[0];
4614
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
4615
+ const offset = options.synthesizedLinesBefore ?? 0;
4616
+ return {
4617
+ sourceFile,
4618
+ failure: {
4619
+ message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
4620
+ line: Math.max(1, line + 1 - offset),
4621
+ column: character + 1,
4622
+ count: diagnostics.length
4623
+ }
4624
+ };
4625
+ }
4626
+ function describeParseFailure(failure) {
4627
+ const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
4628
+ return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
4629
+ }
4630
+ var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
4631
+
4632
+ // src/validate-hook-body-writes.ts
4204
4633
  var cachedTs = null;
4205
4634
  function loadTypeScript() {
4206
4635
  if (cachedTs) return cachedTs;
@@ -4215,6 +4644,8 @@ function loadTypeScript() {
4215
4644
  return cachedTs;
4216
4645
  }
4217
4646
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
4647
+ var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
4648
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
4218
4649
  var HOOK_BODY_WRITE_PATTERNS = [
4219
4650
  {
4220
4651
  id: "input-property-assign",
@@ -4281,13 +4712,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
4281
4712
  "owner",
4282
4713
  "record_type"
4283
4714
  ]);
4284
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4715
+ function unprovisionedAnchorWriteConsequence() {
4716
+ return `so the value can never land: the anchor exists only in the registered schema, which is what carries it PAST the write-path validator that refuses an undeclared name outright (INVALID_FIELD). The remote database is what rejects it \u2014 on a SQL remote with an untyped driver error ('no such column') that aborts the whole statement, so the correctly named fields in the same payload never land either; on a schemaless remote the key is persisted into a column no read surface returns (#4271).`;
4717
+ }
4718
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4285
4719
  function asArray20(v) {
4286
- if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4287
- if (isRec14(v)) {
4720
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4721
+ if (isRec15(v)) {
4288
4722
  return Object.entries(v).map(([name, def]) => ({
4289
4723
  name,
4290
- ...isRec14(def) ? def : {}
4724
+ ...isRec15(def) ? def : {}
4291
4725
  }));
4292
4726
  }
4293
4727
  return [];
@@ -4310,23 +4744,23 @@ function judgeableFieldsOf(index, objectName) {
4310
4744
  if (!declared || declared.size === 0) return void 0;
4311
4745
  return declared;
4312
4746
  }
4313
- function extractHookBodyWrites(source) {
4314
- return extractHookBodyWriteSet(source).writes;
4315
- }
4316
4747
  function extractHookBodyWriteSet(source) {
4317
4748
  if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
4318
4749
  return { writes: [], ctxRecordEscapes: false };
4319
4750
  }
4320
4751
  const tsc = loadTypeScript();
4321
- const sf = tsc.createSourceFile(
4752
+ const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
4753
+ tsc,
4322
4754
  "hook-body.ts",
4323
4755
  `async function __body(ctx) {
4324
4756
  ${source}
4325
4757
  }`,
4326
- tsc.ScriptTarget.Latest,
4327
- /* setParentNodes */
4328
- false,
4329
- tsc.ScriptKind.TS
4758
+ {
4759
+ target: tsc.ScriptTarget.Latest,
4760
+ setParentNodes: false,
4761
+ scriptKind: tsc.ScriptKind.TS,
4762
+ synthesizedLinesBefore: 1
4763
+ }
4330
4764
  );
4331
4765
  const writes = [];
4332
4766
  const recordRefs = [];
@@ -4423,7 +4857,8 @@ ${source}
4423
4857
  visit(sf);
4424
4858
  return {
4425
4859
  writes,
4426
- ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
4860
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
4861
+ ...parseFailure ? { parseFailure } : {}
4427
4862
  };
4428
4863
  }
4429
4864
  function validateHookBodyWrites(stack) {
@@ -4431,15 +4866,28 @@ function validateHookBodyWrites(stack) {
4431
4866
  const hooks = asArray20(stack.hooks);
4432
4867
  if (hooks.length === 0) return findings;
4433
4868
  let objectFields = null;
4869
+ let anchors = null;
4434
4870
  hooks.forEach((hook, hookIndex) => {
4435
4871
  const body = hook.body;
4436
- if (!isRec14(body) || body.language !== "js") return;
4872
+ if (!isRec15(body) || body.language !== "js") return;
4437
4873
  const source = body.source;
4438
4874
  if (typeof source !== "string" || source.trim() === "") return;
4439
- const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4875
+ const extracted = extractHookBodyWriteSet(source);
4876
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4877
+ if (extracted.parseFailure) {
4878
+ findings.push({
4879
+ severity: "warning",
4880
+ rule: HOOK_BODY_SOURCE_UNPARSEABLE,
4881
+ where: `hook "${hookName}" \u203A body`,
4882
+ path: `hooks[${hookIndex}].body.source`,
4883
+ message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
4884
+ hint: PARSE_FAILURE_HINT
4885
+ });
4886
+ }
4887
+ const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4440
4888
  if (writes.length === 0) return;
4441
4889
  objectFields ?? (objectFields = indexObjectFields2(stack));
4442
- const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4890
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4443
4891
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
4444
4892
  (o) => typeof o === "string" && o.trim() !== ""
4445
4893
  );
@@ -4453,7 +4901,20 @@ function validateHookBodyWrites(stack) {
4453
4901
  if (reported.has(dedupeKey)) continue;
4454
4902
  if (w.object === void 0) {
4455
4903
  if (!inputJudgeable) continue;
4456
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
4904
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4905
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
4906
+ reported.add(dedupeKey);
4907
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
4908
+ findings.push({
4909
+ severity: "warning",
4910
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4911
+ where,
4912
+ path,
4913
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
4914
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
4915
+ });
4916
+ continue;
4917
+ }
4457
4918
  if (targetSets.some((s) => s.has(w.field))) continue;
4458
4919
  reported.add(dedupeKey);
4459
4920
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -4469,7 +4930,20 @@ function validateHookBodyWrites(stack) {
4469
4930
  } else {
4470
4931
  const known = judgeableFieldsOf(objectFields, w.object);
4471
4932
  if (!known) continue;
4472
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
4933
+ if (known.has(w.field)) continue;
4934
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4935
+ if (!anchors.get(w.object)?.has(w.field)) continue;
4936
+ reported.add(dedupeKey);
4937
+ findings.push({
4938
+ severity: "warning",
4939
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4940
+ where,
4941
+ path,
4942
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
4943
+ hint: unprovisionedAnchorHint(w.object, w.field)
4944
+ });
4945
+ continue;
4946
+ }
4473
4947
  reported.add(dedupeKey);
4474
4948
  findings.push({
4475
4949
  severity: "warning",
@@ -4498,19 +4972,21 @@ function fixHint(field, declared) {
4498
4972
  import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
4499
4973
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
4500
4974
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
4975
+ var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
4976
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
4501
4977
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
4502
4978
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
4503
4979
  var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
4504
4980
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
4505
4981
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
4506
4982
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
4507
- var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4983
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4508
4984
  function asArray21(v) {
4509
- if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4510
- if (isRec15(v)) {
4985
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4986
+ if (isRec16(v)) {
4511
4987
  return Object.entries(v).map(([name, def]) => ({
4512
4988
  name,
4513
- ...isRec15(def) ? def : {}
4989
+ ...isRec16(def) ? def : {}
4514
4990
  }));
4515
4991
  }
4516
4992
  return [];
@@ -4528,7 +5004,7 @@ function collectActionBodies(stack) {
4528
5004
  const type = typeof action.type === "string" ? action.type : "script";
4529
5005
  if (type !== "script") return;
4530
5006
  const body = action.body;
4531
- if (!isRec15(body) || body.language !== "js") return;
5007
+ if (!isRec16(body) || body.language !== "js") return;
4532
5008
  const source = body.source;
4533
5009
  if (typeof source !== "string" || source.trim() === "") return;
4534
5010
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4547,17 +5023,28 @@ function collectActionBodies(stack) {
4547
5023
  }
4548
5024
  function validateActionBodyWrites(stack) {
4549
5025
  const findings = [];
4550
- if (!isRec15(stack)) return findings;
5026
+ if (!isRec16(stack)) return findings;
4551
5027
  const sites = collectActionBodies(stack);
4552
5028
  if (sites.length === 0) return findings;
4553
5029
  let objectFields = null;
5030
+ let anchors = null;
4554
5031
  for (const site of sites) {
4555
5032
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
4556
- const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
5033
+ const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
4557
5034
  const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
4558
5035
  const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
4559
- if (writes.length === 0 && recordWrites.length === 0) continue;
4560
5036
  const where = `action "${site.name}" \u203A body`;
5037
+ if (parseFailure) {
5038
+ findings.push({
5039
+ severity: "warning",
5040
+ rule: ACTION_BODY_SOURCE_UNPARSEABLE,
5041
+ where,
5042
+ path: site.path,
5043
+ message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
5044
+ hint: PARSE_FAILURE_HINT
5045
+ });
5046
+ }
5047
+ if (writes.length === 0 && recordWrites.length === 0) continue;
4561
5048
  if (recordWrites.length > 0 && !ctxRecordEscapes) {
4562
5049
  const reportedFields = /* @__PURE__ */ new Set();
4563
5050
  for (const w of recordWrites) {
@@ -4575,6 +5062,7 @@ function validateActionBodyWrites(stack) {
4575
5062
  }
4576
5063
  if (writes.length === 0) continue;
4577
5064
  objectFields ?? (objectFields = indexObjectFields2(stack));
5065
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4578
5066
  const reported = /* @__PURE__ */ new Set();
4579
5067
  for (const w of writes) {
4580
5068
  if (w.object === void 0) continue;
@@ -4582,7 +5070,20 @@ function validateActionBodyWrites(stack) {
4582
5070
  if (reported.has(dedupeKey)) continue;
4583
5071
  const known = judgeableFieldsOf(objectFields, w.object);
4584
5072
  if (!known) continue;
4585
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
5073
+ if (known.has(w.field)) continue;
5074
+ if (IMPLICIT_FIELDS2.has(w.field)) {
5075
+ if (!anchors.get(w.object)?.has(w.field)) continue;
5076
+ reported.add(dedupeKey);
5077
+ findings.push({
5078
+ severity: "warning",
5079
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
5080
+ where,
5081
+ path: site.path,
5082
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
5083
+ hint: unprovisionedAnchorHint(w.object, w.field)
5084
+ });
5085
+ continue;
5086
+ }
4586
5087
  reported.add(dedupeKey);
4587
5088
  findings.push({
4588
5089
  severity: "warning",
@@ -4604,14 +5105,15 @@ function fixHint2(field, declared) {
4604
5105
  // src/validate-flow-node-writes.ts
4605
5106
  import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
4606
5107
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
5108
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
4607
5109
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4608
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5110
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4609
5111
  function asArray22(v) {
4610
- if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4611
- if (isRec16(v)) {
5112
+ if (Array.isArray(v)) return v.filter((x) => isRec17(x));
5113
+ if (isRec17(v)) {
4612
5114
  return Object.entries(v).map(([name, def]) => ({
4613
5115
  name,
4614
- ...isRec16(def) ? def : {}
5116
+ ...isRec17(def) ? def : {}
4615
5117
  }));
4616
5118
  }
4617
5119
  return [];
@@ -4624,30 +5126,44 @@ function readLiteralObjectName(config) {
4624
5126
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4625
5127
  function validateFlowNodeWrites(stack) {
4626
5128
  const findings = [];
4627
- if (!isRec16(stack)) return findings;
5129
+ if (!isRec17(stack)) return findings;
4628
5130
  const flows = asArray22(stack.flows);
4629
5131
  if (flows.length === 0) return findings;
4630
5132
  let objectFields = null;
5133
+ let anchors = null;
4631
5134
  flows.forEach((flow, flowIndex) => {
4632
5135
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
4633
5136
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4634
5137
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4635
5138
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4636
- const config = isRec16(node.config) ? node.config : void 0;
5139
+ const config = isRec17(node.config) ? node.config : void 0;
4637
5140
  if (!config) return;
4638
5141
  const fields = config.fields;
4639
- if (!isRec16(fields)) return;
5142
+ if (!isRec17(fields)) return;
4640
5143
  const written = Object.keys(fields);
4641
5144
  if (written.length === 0) return;
4642
5145
  const objectName = readLiteralObjectName(config);
4643
5146
  if (!objectName) return;
4644
5147
  objectFields ?? (objectFields = indexObjectFields2(stack));
5148
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4645
5149
  const known = judgeableFieldsOf(objectFields, objectName);
4646
5150
  if (!known) return;
4647
5151
  const nodeName = flowNodeLabel(node, walkIndex);
4648
5152
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
4649
5153
  for (const fieldName of written) {
4650
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
5154
+ if (known.has(fieldName)) continue;
5155
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
5156
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
5157
+ findings.push({
5158
+ severity: "warning",
5159
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
5160
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
5161
+ path: `${nodePath}.config.fields.${fieldName}`,
5162
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
5163
+ hint: unprovisionedAnchorHint(objectName, fieldName)
5164
+ });
5165
+ continue;
5166
+ }
4651
5167
  if (fieldName.includes(".")) continue;
4652
5168
  findings.push({
4653
5169
  severity: "error",
@@ -4769,14 +5285,14 @@ import {
4769
5285
  chartAggregateResultKeys,
4770
5286
  isRecordContextBlockType
4771
5287
  } from "@objectstack/spec/ui";
4772
- import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
5288
+ import { VALID_AST_OPERATORS as VALID_AST_OPERATORS2 } from "@objectstack/spec/data";
4773
5289
 
4774
5290
  // src/zod-issue-format.ts
4775
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5291
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4776
5292
  var valueAtPath = (root, path) => {
4777
5293
  let cur = root;
4778
5294
  for (const key of path) {
4779
- if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
5295
+ if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
4780
5296
  cur = cur[key];
4781
5297
  }
4782
5298
  return cur;
@@ -4908,6 +5424,7 @@ function filterAttrValue(tsc, sf, attr) {
4908
5424
  };
4909
5425
  return perPosition(init.expression);
4910
5426
  }
5427
+ var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
4911
5428
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
4912
5429
  var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
4913
5430
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
@@ -4915,7 +5432,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4915
5432
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4916
5433
  function checkChartDrillDown(raw, push2) {
4917
5434
  if (raw === void 0 || raw === NOT_STATIC) return;
4918
- if (!isRec18(raw)) {
5435
+ if (!isRec19(raw)) {
4919
5436
  push2(
4920
5437
  "error",
4921
5438
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4938,7 +5455,7 @@ function checkChartDrillDown(raw, push2) {
4938
5455
  }
4939
5456
  function checkChartAggregate(raw, push2) {
4940
5457
  if (raw === void 0 || raw === NOT_STATIC) return;
4941
- if (!isRec18(raw)) {
5458
+ if (!isRec19(raw)) {
4942
5459
  push2(
4943
5460
  "error",
4944
5461
  REACT_CHART_AGGREGATE_INVALID,
@@ -4969,7 +5486,7 @@ function checkChartAggregate(raw, push2) {
4969
5486
  );
4970
5487
  }
4971
5488
  }
4972
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5489
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4973
5490
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4974
5491
  function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4975
5492
  const { values, where, path } = attrs;
@@ -4979,11 +5496,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
4979
5496
  const aggregate = values.get("aggregate");
4980
5497
  checkChartAggregate(aggregate, push2);
4981
5498
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4982
- if (!isRec18(aggregate)) return;
5499
+ if (!isRec19(aggregate)) return;
4983
5500
  const fn = strOf(aggregate.function);
4984
5501
  const field = strOf(aggregate.field);
4985
5502
  const groupBy = aggregate.groupBy;
4986
- const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
5503
+ const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
4987
5504
  const objectName = strOf(values.get("objectName"));
4988
5505
  const known = objectName ? objectFields.get(objectName) : void 0;
4989
5506
  if (objectName && known) {
@@ -5027,18 +5544,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
5027
5544
  );
5028
5545
  };
5029
5546
  const xAxisRaw = values.get("xAxis");
5030
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5547
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5031
5548
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
5032
5549
  axisRef(categoryAxis, categoryProp);
5033
5550
  const yAxisRaw = values.get("yAxis");
5034
5551
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
5035
5552
  for (const a of yAxisList) {
5036
- axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
5553
+ axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
5037
5554
  }
5038
5555
  const series = values.get("series");
5039
5556
  if (Array.isArray(series)) {
5040
5557
  for (const s of series) {
5041
- if (!isRec18(s)) continue;
5558
+ if (!isRec19(s)) continue;
5042
5559
  const dataKey = strOf(s.dataKey);
5043
5560
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
5044
5561
  }
@@ -5094,7 +5611,7 @@ function subformFieldRefs(value, basePath) {
5094
5611
  if (!Array.isArray(value)) return { child, parent };
5095
5612
  for (let i = 0; i < value.length; i++) {
5096
5613
  const sub = value[i];
5097
- if (!isRec18(sub)) continue;
5614
+ if (!isRec19(sub)) continue;
5098
5615
  const at = (key) => `${basePath}[${i}].${key}`;
5099
5616
  child.push({
5100
5617
  objectName: strOf(sub.childObject),
@@ -5119,7 +5636,7 @@ function filterFieldRefs(node, basePath, out) {
5119
5636
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
5120
5637
  return;
5121
5638
  }
5122
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5639
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS2.has(node[1].toLowerCase())) {
5123
5640
  out.push({ name: head, path: `${basePath}[0]` });
5124
5641
  }
5125
5642
  }
@@ -5139,20 +5656,20 @@ function reactFieldRefs(spec, values, basePath) {
5139
5656
  }
5140
5657
  for (const key of spec.nestedFields ?? []) {
5141
5658
  const v = readable(key);
5142
- if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5659
+ if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5143
5660
  }
5144
5661
  for (const key of spec.sections ?? []) {
5145
5662
  const v = readable(key);
5146
5663
  if (!Array.isArray(v)) continue;
5147
5664
  for (let i = 0; i < v.length; i++) {
5148
5665
  const section = v[i];
5149
- if (!isRec18(section)) continue;
5666
+ if (!isRec19(section)) continue;
5150
5667
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
5151
5668
  }
5152
5669
  }
5153
5670
  for (const key of spec.keyedByField ?? []) {
5154
5671
  const v = readable(key);
5155
- if (!isRec18(v)) continue;
5672
+ if (!isRec19(v)) continue;
5156
5673
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
5157
5674
  }
5158
5675
  for (const key of spec.filterArrays ?? []) {
@@ -5236,11 +5753,20 @@ function validateReactPageProps(stack) {
5236
5753
  if (typeof source !== "string" || source.trim() === "") continue;
5237
5754
  const name = String(page.name ?? `#${p}`);
5238
5755
  const tsc = loadTypeScript2();
5239
- let sf;
5240
- try {
5241
- sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
5242
- } catch {
5243
- continue;
5756
+ const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
5757
+ target: tsc.ScriptTarget.Latest,
5758
+ setParentNodes: true,
5759
+ scriptKind: tsc.ScriptKind.TSX
5760
+ });
5761
+ if (failure) {
5762
+ findings.push({
5763
+ severity: "warning",
5764
+ rule: REACT_PAGE_SOURCE_UNPARSEABLE,
5765
+ where: `page "${name}"`,
5766
+ path: `pages[${p}].source`,
5767
+ message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
5768
+ hint: PARSE_FAILURE_HINT
5769
+ });
5244
5770
  }
5245
5771
  const locals = localComponentNames(tsc, sf);
5246
5772
  const visit = (node) => {
@@ -5319,7 +5845,12 @@ function validateReactPageProps(stack) {
5319
5845
  searchTargets,
5320
5846
  where,
5321
5847
  `${path} \u203A searchableFields`,
5322
- "searchableFields"
5848
+ "searchableFields",
5849
+ // A `<ListView>` prop is a view-level narrowing — the checker's
5850
+ // default, spelled out here because the #8404 provenance index
5851
+ // follows it positionally.
5852
+ "narrowing",
5853
+ unprovisionedAnchors
5323
5854
  )
5324
5855
  );
5325
5856
  }
@@ -5338,9 +5869,26 @@ function validateReactPageProps(stack) {
5338
5869
  }
5339
5870
 
5340
5871
  // src/reference-integrity-suite.ts
5872
+ var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
5341
5873
  var REFERENCE_INTEGRITY_RULES = [
5342
5874
  { name: "validateObjectReferences", run: validateObjectReferences },
5343
- { name: "validateSearchableFields", run: validateSearchableFields },
5875
+ // [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
5876
+ // both judge a LIST VIEW's field references, and a standalone list view is
5877
+ // written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
5878
+ // an MCP/AI author has. Their walks read the flattened overlay shape that
5879
+ // door carries (see each rule's `views[]` self rung), and they resolve only
5880
+ // against `stack.objects`, which the per-write snapshot DOES carry — so the
5881
+ // crossing has no missing-collection false-positive channel. Measured over
5882
+ // the shipped view corpus before crossing (0 refusals; population in the
5883
+ // #9313 PR).
5884
+ { name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
5885
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
5886
+ // name written in metadata, resolved against the object's declared fields. It
5887
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
5888
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
5889
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
5890
+ // the refusal is the whole view, on every load, traced to nothing.
5891
+ { name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
5344
5892
  { name: "validateActionNameRefs", run: validateActionNameRefs },
5345
5893
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5346
5894
  { name: "validateChartBindings", run: validateChartBindings },
@@ -5446,9 +5994,11 @@ var REFERENCE_INTEGRITY_RULES = [
5446
5994
  // — only a page that is actually `kind:'react'` loads the compiler.
5447
5995
  { name: "validateReactPageProps", run: validateReactPageProps }
5448
5996
  ];
5449
- function validateReferenceIntegrity(stack) {
5997
+ function validateReferenceIntegrity(stack, options) {
5450
5998
  const findings = [];
5999
+ const writeType = options?.runtimeWriteType;
5451
6000
  for (const rule of REFERENCE_INTEGRITY_RULES) {
6001
+ if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
5452
6002
  findings.push(...rule.run(stack));
5453
6003
  }
5454
6004
  return findings;
@@ -5459,10 +6009,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
5459
6009
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
5460
6010
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5461
6011
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
5462
- function isRec19(v) {
6012
+ function isRec20(v) {
5463
6013
  return !!v && typeof v === "object" && !Array.isArray(v);
5464
6014
  }
5465
- function strName18(v) {
6015
+ function strName19(v) {
5466
6016
  return typeof v === "string" && v.length > 0 ? v : void 0;
5467
6017
  }
5468
6018
  function asArray25(v) {
@@ -5476,8 +6026,8 @@ var PROPS_SCHEMAS = ComponentPropsMap;
5476
6026
  var DATASOURCE_SUPPLIED_PROP = "object";
5477
6027
  function suppliedByDataSource(issue, component) {
5478
6028
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
5479
- const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
5480
- return strName18(dataSource?.object) !== void 0;
6029
+ const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
6030
+ return strName19(dataSource?.object) !== void 0;
5481
6031
  }
5482
6032
  function unrecognizedKeysFromUnionArm(issue) {
5483
6033
  if (issue.code !== "invalid_union") return void 0;
@@ -5494,18 +6044,18 @@ function unrecognizedKeysFromUnionArm(issue) {
5494
6044
  }
5495
6045
  function validateComponentProps(stack) {
5496
6046
  const findings = [];
5497
- if (!isRec19(stack)) return findings;
6047
+ if (!isRec20(stack)) return findings;
5498
6048
  const pages = asArray25(stack.pages);
5499
6049
  for (let pi = 0; pi < pages.length; pi++) {
5500
6050
  const page = pages[pi];
5501
- if (!isRec19(page)) continue;
5502
- const pageName = strName18(page.name) ?? `#${pi}`;
6051
+ if (!isRec20(page)) continue;
6052
+ const pageName = strName19(page.name) ?? `#${pi}`;
5503
6053
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5504
- const type = strName18(component.type);
6054
+ const type = strName19(component.type);
5505
6055
  if (!type) continue;
5506
6056
  const schema = PROPS_SCHEMAS[type];
5507
6057
  if (!schema) continue;
5508
- const props = isRec19(component.properties) ? component.properties : void 0;
6058
+ const props = isRec20(component.properties) ? component.properties : void 0;
5509
6059
  if (!props) continue;
5510
6060
  const where = `page "${pageName}" \xB7 ${type}`;
5511
6061
  const base = `${path}.properties`;
@@ -6683,10 +7233,10 @@ function asArray35(v) {
6683
7233
  }
6684
7234
  return [];
6685
7235
  }
6686
- function isRec20(v) {
7236
+ function isRec21(v) {
6687
7237
  return !!v && typeof v === "object" && !Array.isArray(v);
6688
7238
  }
6689
- function strName19(v) {
7239
+ function strName20(v) {
6690
7240
  return typeof v === "string" && v.length > 0 ? v : void 0;
6691
7241
  }
6692
7242
  function fieldNameOf(entry) {
@@ -6707,7 +7257,7 @@ function validateFormLayout(stack) {
6707
7257
  objectFields.set(name, new Set(fields));
6708
7258
  }
6709
7259
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6710
- const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
7260
+ const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
6711
7261
  const containerObject = viewObjectName(view);
6712
7262
  for (const site of formViewSites(view, viewPath)) {
6713
7263
  const objName = viewObjectName(site.view) ?? containerObject;
@@ -6717,7 +7267,7 @@ function validateFormLayout(stack) {
6717
7267
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6718
7268
  for (let s = 0; s < sections.length; s++) {
6719
7269
  const sec = sections[s];
6720
- const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
7270
+ const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6721
7271
  for (let f = 0; f < secFields.length; f++) {
6722
7272
  const entry = secFields[f];
6723
7273
  const fname = fieldNameOf(entry);
@@ -6732,7 +7282,7 @@ function validateFormLayout(stack) {
6732
7282
  hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6733
7283
  });
6734
7284
  }
6735
- const colSpan = isRec20(entry) ? entry.colSpan : void 0;
7285
+ const colSpan = isRec21(entry) ? entry.colSpan : void 0;
6736
7286
  if (colSpan != null) {
6737
7287
  findings.push({
6738
7288
  severity: "warning",
@@ -6895,12 +7445,12 @@ function bareRhsOnlyIdentifiers(ast) {
6895
7445
  for (const name of elsewhere) rhs.delete(name);
6896
7446
  return rhs;
6897
7447
  }
6898
- function isRec21(v) {
7448
+ function isRec22(v) {
6899
7449
  return !!v && typeof v === "object" && !Array.isArray(v);
6900
7450
  }
6901
7451
  function schemaIdOf(view) {
6902
7452
  const data = view.data;
6903
- if (!isRec21(data)) return void 0;
7453
+ if (!isRec22(data)) return void 0;
6904
7454
  if (data.provider !== "schema") return void 0;
6905
7455
  return typeof data.schemaId === "string" ? data.schemaId : void 0;
6906
7456
  }
@@ -7272,7 +7822,7 @@ function predicateSource2(v) {
7272
7822
  }
7273
7823
  return void 0;
7274
7824
  }
7275
- function isRec22(v) {
7825
+ function isRec23(v) {
7276
7826
  return !!v && typeof v === "object" && !Array.isArray(v);
7277
7827
  }
7278
7828
  function checkPredicate(source, scope, where, path, findings) {
@@ -7343,7 +7893,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
7343
7893
  if (!Array.isArray(entries) || depth > 12) return;
7344
7894
  for (let i = 0; i < entries.length; i++) {
7345
7895
  const entry = entries[i];
7346
- if (!isRec22(entry)) continue;
7896
+ if (!isRec23(entry)) continue;
7347
7897
  const path = `${base}[${i}]`;
7348
7898
  for (const key of PREDICATE_KEYS) {
7349
7899
  const source = predicateSource2(entry[key]);
@@ -7377,7 +7927,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
7377
7927
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7378
7928
  for (let s = 0; s < sections.length; s++) {
7379
7929
  const section = sections[s];
7380
- if (!isRec22(section)) continue;
7930
+ if (!isRec23(section)) continue;
7381
7931
  const sectionPath = `${site.path}.${bucket}[${s}]`;
7382
7932
  for (const key of PREDICATE_KEYS) {
7383
7933
  const source = predicateSource2(section[key]);
@@ -7661,6 +8211,7 @@ function validateSecurityPosture(stack, opts) {
7661
8211
  }
7662
8212
  }
7663
8213
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
8214
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
7664
8215
  const nowMs = opts?.nowMs ?? Date.now();
7665
8216
  for (const [i, seed] of asArray36(stack.data).entries()) {
7666
8217
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -7684,7 +8235,7 @@ function validateSecurityPosture(stack, opts) {
7684
8235
  }
7685
8236
  }
7686
8237
  const delegatedFrom = rec.delegated_from;
7687
- if (delegatedFrom != null && delegatedFrom !== "") {
8238
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
7688
8239
  const reason = rec.reason;
7689
8240
  if (typeof reason !== "string" || reason.trim().length === 0) {
7690
8241
  findings.push({
@@ -7850,6 +8401,8 @@ function validateOrgAxisRedLines(stack) {
7850
8401
  import { compileCelToFilter } from "@objectstack/formula";
7851
8402
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
7852
8403
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
8404
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
8405
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
7853
8406
  function asArray38(v) {
7854
8407
  if (Array.isArray(v)) return v;
7855
8408
  if (v && typeof v === "object") {
@@ -7874,10 +8427,67 @@ function sourceOf(condition) {
7874
8427
  return str2(input?.source);
7875
8428
  }
7876
8429
  var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
8430
+ function effectiveSharingModelOf(obj) {
8431
+ const m = obj.sharingModel;
8432
+ if (m === "private") return "private";
8433
+ if (m === "public_read") return "read";
8434
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
8435
+ if (m == null) {
8436
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
8437
+ return isSystem ? "public" : "private";
8438
+ }
8439
+ return "private";
8440
+ }
8441
+ function masterOf(obj) {
8442
+ for (const f of asArray38(obj.fields)) {
8443
+ if (f.type === "master_detail") {
8444
+ const ref = f.reference;
8445
+ if (typeof ref === "string" && ref) return ref;
8446
+ }
8447
+ }
8448
+ return void 0;
8449
+ }
8450
+ function anchorFindings(rule, index, objectsByName) {
8451
+ const object = str2(rule.object);
8452
+ if (!object) return [];
8453
+ const target = objectsByName.get(object);
8454
+ if (!target) return [];
8455
+ const name = str2(rule.name) || String(index);
8456
+ const where = `sharing rule "${name}" on object "${object}"`;
8457
+ const path = `sharingRules[${index}].object`;
8458
+ const owd = target.sharingModel;
8459
+ if (owd === "controlled_by_parent") {
8460
+ const master = masterOf(target);
8461
+ return [{
8462
+ severity: "error",
8463
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
8464
+ where,
8465
+ path,
8466
+ message: `Sharing rule "${name}" is anchored on object "${object}", which declares sharingModel 'controlled_by_parent'. A detail record has no record-level access of its own \u2014 its visibility is DERIVED from its master (ADR-0055), so it holds no shares to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is controlled by its parent (master-detail); share the master record instead"), so the rule's boot backfill fails, no \`sys_record_share\` row is ever written, and the recipients this rule names get whatever the MASTER grants them \u2014 which may be nothing. The grant is declared and does not exist.`,
8467
+ hint: `Move the rule onto the MASTER object` + (master ? ` \u2014 "${object}" derives from "${master}" through its master_detail field, so share "${master}" and the detail rows follow` : `, and share that instead; the detail rows follow`) + `. If "${object}" is meant to carry a record-level baseline of its own, that is a different decision: change its sharingModel to 'private' (owner + shares) or 'public_read', and this rule becomes enforceable where it stands.`
8468
+ }];
8469
+ }
8470
+ if (effectiveSharingModelOf(target) !== "public") return [];
8471
+ const declared = owd === "public_read_write" ? `declares sharingModel 'public_read_write'` : `declares no sharingModel and is a system object (\`isSystem: true\` or a \`sys_\` name), which ADR-0090 D1 resolves to public`;
8472
+ return [{
8473
+ severity: "error",
8474
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
8475
+ where,
8476
+ path,
8477
+ message: `Sharing rule "${name}" is anchored on object "${object}", which ${declared}. Its effective sharing model is therefore \`public\`, and sharing only ever WIDENS an OWD baseline \u2014 on the widest baseline there is nothing left to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is not under record-sharing enforcement"), so the rule's boot backfill fails and no \`sys_record_share\` row is ever written. Measured: \`buildReadFilter\` returns \`null\` for this object, i.e. NO record-level filter at all \u2014 every principal already reads every row, so this rule advertises a restriction that does not exist.`,
8478
+ hint: `Decide which half is wrong. If the ACCESS is right \u2014 everyone should read and write these records \u2014 the rule is dead metadata: delete it (ADR-0049 enforce-or-remove). If the RULE is right \u2014 only the named audience should reach these records \u2014 then "${object}"'s OWD is the defect: set sharingModel: 'private' (owner + shares) or 'public_read', and this rule starts enforcing. Do NOT re-home the rule onto another public object; that moves the inertness instead of removing it.`
8479
+ }];
8480
+ }
7877
8481
  function validateSharingRuleEnforceability(stack) {
7878
8482
  const findings = [];
7879
8483
  const cfg = stack ?? {};
8484
+ const objectsByName = /* @__PURE__ */ new Map();
8485
+ for (const obj of asArray38(cfg.objects)) {
8486
+ const name = str2(obj.name);
8487
+ if (name) objectsByName.set(name, obj);
8488
+ }
7880
8489
  asArray38(cfg.sharingRules).forEach((rule, index) => {
8490
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
7881
8491
  const input = toCompilerInput(rule.condition);
7882
8492
  if (input === null) return;
7883
8493
  const result = compileCelToFilter(input, { variables: {} });
@@ -8007,11 +8617,11 @@ import { createRequire as createRequire4 } from "module";
8007
8617
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
8008
8618
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
8009
8619
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
8010
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8620
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8011
8621
  function asArray40(v) {
8012
- if (Array.isArray(v)) return v.filter(isRec23);
8013
- if (isRec23(v)) {
8014
- return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
8622
+ if (Array.isArray(v)) return v.filter(isRec24);
8623
+ if (isRec24(v)) {
8624
+ return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
8015
8625
  }
8016
8626
  return [];
8017
8627
  }
@@ -8028,7 +8638,7 @@ function loadAjv() {
8028
8638
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
8029
8639
  );
8030
8640
  }
8031
- const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
8641
+ const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
8032
8642
  cachedAjv = ctor;
8033
8643
  return ctor;
8034
8644
  }
@@ -8043,7 +8653,7 @@ function loadAddFormats() {
8043
8653
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
8044
8654
  );
8045
8655
  }
8046
- const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
8656
+ const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
8047
8657
  cachedAddFormats = plugin;
8048
8658
  return plugin;
8049
8659
  }
@@ -8073,13 +8683,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
8073
8683
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
8074
8684
  for (const branch of ["then", "otherwise"]) {
8075
8685
  const nested = rule[branch];
8076
- if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8686
+ if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8077
8687
  }
8078
8688
  return out;
8079
8689
  }
8080
8690
  function walkObjectValidationRules(stack) {
8081
8691
  const walked = [];
8082
- if (!isRec23(stack)) return walked;
8692
+ if (!isRec24(stack)) return walked;
8083
8693
  for (const obj of asArray40(stack.objects)) {
8084
8694
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8085
8695
  const validations = obj.validations;
@@ -8114,7 +8724,7 @@ function validateRuleCompilability(stack) {
8114
8724
  });
8115
8725
  }
8116
8726
  }
8117
- if (rule.type === "json_schema" && isRec23(rule.schema)) {
8727
+ if (rule.type === "json_schema" && isRec24(rule.schema)) {
8118
8728
  try {
8119
8729
  createRuntimeAjv().compile(rule.schema);
8120
8730
  } catch (err) {
@@ -8134,7 +8744,7 @@ function validateRuleCompilability(stack) {
8134
8744
 
8135
8745
  // src/validate-rule-schema-formats.ts
8136
8746
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
8137
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8747
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8138
8748
  var SUBSCHEMA_KEYS = [
8139
8749
  "additionalItems",
8140
8750
  "additionalProperties",
@@ -8158,7 +8768,7 @@ var SUBSCHEMA_MAP_KEYS = [
8158
8768
  var MAX_SCHEMA_WALK_DEPTH = 32;
8159
8769
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
8160
8770
  function collectFormatUses(schema, pointer, out, depth) {
8161
- if (!isRec24(schema)) return;
8771
+ if (!isRec25(schema)) return;
8162
8772
  if (typeof schema.format === "string") {
8163
8773
  out.push({ pointer: `${pointer}/format`, name: schema.format });
8164
8774
  }
@@ -8177,7 +8787,7 @@ function collectFormatUses(schema, pointer, out, depth) {
8177
8787
  }
8178
8788
  for (const key of SUBSCHEMA_MAP_KEYS) {
8179
8789
  const value = schema[key];
8180
- if (!isRec24(value)) continue;
8790
+ if (!isRec25(value)) continue;
8181
8791
  for (const [name, entry] of Object.entries(value)) {
8182
8792
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
8183
8793
  }
@@ -8185,13 +8795,13 @@ function collectFormatUses(schema, pointer, out, depth) {
8185
8795
  const items = schema.items;
8186
8796
  if (Array.isArray(items)) {
8187
8797
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
8188
- } else if (isRec24(items)) {
8798
+ } else if (isRec25(items)) {
8189
8799
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
8190
8800
  }
8191
8801
  const dependencies = schema.dependencies;
8192
- if (isRec24(dependencies)) {
8802
+ if (isRec25(dependencies)) {
8193
8803
  for (const [name, entry] of Object.entries(dependencies)) {
8194
- if (!isRec24(entry)) continue;
8804
+ if (!isRec25(entry)) continue;
8195
8805
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
8196
8806
  }
8197
8807
  }
@@ -8218,9 +8828,9 @@ function nearestRegisteredFormat(name, registered) {
8218
8828
  let best = null;
8219
8829
  let bestDistance = Number.POSITIVE_INFINITY;
8220
8830
  for (const candidate of [...registered].sort()) {
8221
- const distance7 = editDistance2(authored, candidate);
8222
- if (distance7 < bestDistance) {
8223
- bestDistance = distance7;
8831
+ const distance8 = editDistance2(authored, candidate);
8832
+ if (distance8 < bestDistance) {
8833
+ bestDistance = distance8;
8224
8834
  best = candidate;
8225
8835
  }
8226
8836
  }
@@ -8230,7 +8840,7 @@ function validateRuleSchemaFormats(stack) {
8230
8840
  const findings = [];
8231
8841
  const pending = [];
8232
8842
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
8233
- if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
8843
+ if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
8234
8844
  const uses = [];
8235
8845
  collectFormatUses(rule.schema, "", uses, 0);
8236
8846
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -8263,7 +8873,7 @@ function asArray41(v) {
8263
8873
  }
8264
8874
  return [];
8265
8875
  }
8266
- function strName20(v) {
8876
+ function strName21(v) {
8267
8877
  return typeof v === "string" && v.length > 0 ? v : void 0;
8268
8878
  }
8269
8879
  function strList3(v) {
@@ -8278,7 +8888,7 @@ function collectNamePlacedActions(stack) {
8278
8888
  for (const n of strList3(list3[key])) placed.add(n);
8279
8889
  }
8280
8890
  for (const def of asArray41(list3.bulkActionDefs)) {
8281
- const n = strName20(def?.name);
8891
+ const n = strName21(def?.name);
8282
8892
  if (n) placed.add(n);
8283
8893
  }
8284
8894
  };
@@ -8304,7 +8914,7 @@ function validateActionLocations(stack) {
8304
8914
  const check = (action, path) => {
8305
8915
  if (!action || typeof action !== "object") return;
8306
8916
  if ("locations" in action) return;
8307
- const name = strName20(action.name);
8917
+ const name = strName21(action.name);
8308
8918
  if (!name) return;
8309
8919
  if (namePlaced.has(name)) return;
8310
8920
  findings.push({
@@ -9157,6 +9767,17 @@ function lintViewRefs(stack) {
9157
9767
  }
9158
9768
 
9159
9769
  // src/data-model-rules.ts
9770
+ function objectWhere(obj) {
9771
+ return `object "${obj?.name}"`;
9772
+ }
9773
+ function indexWhere(obj, idx, j, cols) {
9774
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
9775
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
9776
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
9777
+ }
9778
+ function fieldWhere(obj, fieldName) {
9779
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
9780
+ }
9160
9781
  function fieldEntries2(fields) {
9161
9782
  if (!fields) return [];
9162
9783
  if (Array.isArray(fields)) {
@@ -9190,11 +9811,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9190
9811
  for (let j = 0; j < declaredIndexes.length; j++) {
9191
9812
  const idx = declaredIndexes[j];
9192
9813
  if (idx?.unique !== true) continue;
9193
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
9814
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
9815
+ const cols = colList.join(", ");
9194
9816
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9195
9817
  issues.push({
9196
9818
  severity: "warning",
9197
9819
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
9820
+ where: indexWhere(obj, idx, j, colList),
9198
9821
  message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
9199
9822
  path: `objects[${i}].indexes[${j}]`,
9200
9823
  fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
@@ -9243,6 +9866,11 @@ function lintUniqueDeclarations(objects) {
9243
9866
  issues.push({
9244
9867
  severity: "warning",
9245
9868
  rule: UNIQUE_DOUBLE_DECLARATION,
9869
+ // More specific than `path` on purpose: this rule's finding is about ONE
9870
+ // column, but its `path` has always been the whole object (`objects[i]`)
9871
+ // because the defect straddles `fields.<name>.unique` and an entry of
9872
+ // `indexes`. `where` can name the column without picking one of the two.
9873
+ where: fieldWhere(obj, name),
9246
9874
  message,
9247
9875
  path: `objects[${i}]`,
9248
9876
  fix
@@ -9271,6 +9899,7 @@ function lintLegacyOrganizationComposites(objects) {
9271
9899
  issues.push({
9272
9900
  severity: "warning",
9273
9901
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
9902
+ where: indexWhere(obj, idx, j, cols),
9274
9903
  message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
9275
9904
  path: `objects[${i}].indexes[${j}]`,
9276
9905
  fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
@@ -9287,7 +9916,7 @@ var CLI_ONLY = ["cli"];
9287
9916
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9288
9917
  var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
9289
9918
  var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
9290
- var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
9919
+ var RUNTIME_OBJECT_ADVISORY_VOLUME = "Advisory-tier object rule: it cannot refuse a write, and it is held off the runtime door for advisory VOLUME (~8 findings per object write measured on unswept metadata, rendered in Studio since #4717), not refusal risk. Crossing it is a UX decision with its own card (#4716).";
9291
9920
  var EXPRESSION_INVALID = "expression-invalid";
9292
9921
  var AUTHORING_RULES = [
9293
9922
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -9348,8 +9977,14 @@ var AUTHORING_RULES = [
9348
9977
  input: "normalized",
9349
9978
  commands: ALL,
9350
9979
  source: "packages/lint/src/validate-functional-completeness.ts",
9351
- surfaces: CLI_ONLY,
9352
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9980
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
9981
+ // object rules cross together under the 2026-08-18 adjudication. The
9982
+ // false-positive budget the crossing owed was exempted on a measured
9983
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
9984
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
9985
+ // advisory-tier object rules deliberately do NOT ride.
9986
+ surfaces: CLI_AND_RUNTIME,
9987
+ runtimeTypes: ["object"],
9353
9988
  run: (stack) => validateFunctionalCompleteness(stack)
9354
9989
  },
9355
9990
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -9373,8 +10008,11 @@ var AUTHORING_RULES = [
9373
10008
  input: "normalized",
9374
10009
  commands: ALL,
9375
10010
  source: "packages/lint/src/validate-managed-api-methods.ts",
9376
- surfaces: CLI_ONLY,
9377
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10011
+ // Runtime publish gate (#4716): a managed object advertising a verb its
10012
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
10013
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
10014
+ surfaces: CLI_AND_RUNTIME,
10015
+ runtimeTypes: ["object"],
9378
10016
  run: (stack) => validateManagedApiMethods(stack)
9379
10017
  },
9380
10018
  // A view container in `views: []` that registers zero views: nothing appears
@@ -9396,19 +10034,38 @@ var AUTHORING_RULES = [
9396
10034
  },
9397
10035
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
9398
10036
  // chartConfig axis/series must resolve against the declared datasets.
10037
+ //
10038
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
10039
+ // widget bound to a dataset that resolves to nothing sailed through save AND
10040
+ // publish — this rule provably caught the body and was simply never invoked,
10041
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
10042
+ // hold a forward reference; publishing refuses it with the key path named.
10043
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
10044
+ // — without it every legitimate board reads as dangling, the 3-phantom
10045
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
10046
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
10047
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
10048
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
10049
+ // accepted knowingly rather than splitting the dataset limb into its own
10050
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
10051
+ // filter (registry machinery that weakens "delete a rule from the table and
10052
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
10053
+ // advisory channel and never block (#4463 P1).
9399
10054
  {
9400
10055
  name: "validateWidgetBindings",
9401
10056
  tier: "gating",
9402
10057
  input: "parsed",
9403
10058
  commands: ALL,
9404
10059
  source: "packages/lint/src/validate-widget-bindings.ts",
9405
- surfaces: CLI_ONLY,
9406
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10060
+ surfaces: CLI_AND_RUNTIME,
10061
+ runtimeTypes: ["dashboard"],
9407
10062
  run: (stack) => validateWidgetBindings(stack)
9408
10063
  },
9409
- // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
9410
- // target that resolves to no defined action ships a button that renders and
9411
- // silently does nothing on click. Unresolved `url` routes stay advisory.
10064
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
10065
+ // button that renders and refuses (or does nothing) on click: a `script`
10066
+ // target must name a defined action, a `modal` target must name a declared
10067
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
10068
+ // Unresolved `url` routes stay advisory.
9412
10069
  {
9413
10070
  name: "validateDashboardActionRefs",
9414
10071
  tier: "gating",
@@ -9433,6 +10090,28 @@ var AUTHORING_RULES = [
9433
10090
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9434
10091
  run: (stack) => validateFilterTokens(stack)
9435
10092
  },
10093
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
10094
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
10095
+ // no layer: the engine refuses it on a declared temporal field at query time
10096
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
10097
+ // literal string. This is the authoring-time refusal the ruling shipped
10098
+ // alongside the engine door, judging the filter literal in isolation —
10099
+ // ordering positions only, all three authored filter shapes. Like
10100
+ // `validateEmptyCombinators` it needs NO resolution context, so
10101
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
10102
+ // for every filter-carrying type the gate already maps: the write path is
10103
+ // the one door an AI author uses, and dashboards/views are where the preset
10104
+ // vocabulary is near enough to reach for.
10105
+ {
10106
+ name: "validatePresetComparands",
10107
+ tier: "gating",
10108
+ input: "parsed",
10109
+ commands: ALL,
10110
+ source: "packages/lint/src/validate-preset-comparands.ts",
10111
+ surfaces: CLI_AND_RUNTIME,
10112
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
10113
+ run: (stack) => validatePresetComparands(stack)
10114
+ },
9436
10115
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9437
10116
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9438
10117
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -9476,9 +10155,31 @@ var AUTHORING_RULES = [
9476
10155
  // collection in the snapshot and return nothing, and the two that would
9477
10156
  // load `typescript` need a hook/action/react body the snapshot never
9478
10157
  // carries — which is what `runtime-lazy-deps.test.ts` pins.
10158
+ //
10159
+ // [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
10160
+ // taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
10161
+ // dispatch the suite, and the suite's own per-member `runtimeTypes`
10162
+ // (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
10163
+ // that snapshot — the gate passes the written type through
10164
+ // `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
10165
+ // members that judge a list view's field references and resolve only
10166
+ // against the `objects` collection the snapshot carries
10167
+ // (`validateSearchableFields`, `validateSortableFields`). The whole suite
10168
+ // is NOT the right granularity for this door, measured not assumed, and
10169
+ // the crossing fails differently per body shape — both ways wrong:
10170
+ // `validateActionNameRefs` (error-tier) resolves `views[].list` /
10171
+ // `views[].listViews.*` action names against `stack.actions`, a collection
10172
+ // no per-write snapshot carries, so on a CONTAINER view write it would
10173
+ // refuse every stack-level action the body names (measured:
10174
+ // `action-name-undefined` on the snapshot shape, clean on the full stack)
10175
+ // — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
10176
+ // type the gate has. On a FLATTENED overlay it has no rung at all, so the
10177
+ // crossing would be a silent no-op that reads as coverage — the very shape
10178
+ // #9313 was filed about. Flow snapshots are unchanged: every member keeps
10179
+ // the default `flow` declaration.
9479
10180
  surfaces: CLI_AND_RUNTIME,
9480
- runtimeTypes: ["flow"],
9481
- run: (stack) => validateReferenceIntegrity(stack)
10181
+ runtimeTypes: ["flow", "view"],
10182
+ run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
9482
10183
  },
9483
10184
  // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
9484
10185
  // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
@@ -9651,7 +10352,7 @@ var AUTHORING_RULES = [
9651
10352
  commands: ALL,
9652
10353
  source: "packages/lint/src/validate-record-title.ts",
9653
10354
  surfaces: CLI_ONLY,
9654
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10355
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9655
10356
  run: (stack) => validateRecordTitle(stack)
9656
10357
  },
9657
10358
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -9664,7 +10365,7 @@ var AUTHORING_RULES = [
9664
10365
  commands: ALL,
9665
10366
  source: "packages/lint/src/validate-semantic-roles.ts",
9666
10367
  surfaces: CLI_ONLY,
9667
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10368
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9668
10369
  run: (stack) => validateSemanticRoles(stack)
9669
10370
  },
9670
10371
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -9875,7 +10576,7 @@ var AUTHORING_RULES = [
9875
10576
  commands: ALL,
9876
10577
  source: "packages/lint/src/lint-liveness-properties.ts",
9877
10578
  surfaces: CLI_ONLY,
9878
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10579
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9879
10580
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
9880
10581
  severity: "warning",
9881
10582
  rule: f.rule,
@@ -9894,8 +10595,12 @@ var AUTHORING_RULES = [
9894
10595
  input: "parsed",
9895
10596
  commands: ALL,
9896
10597
  source: "packages/lint/src/lint-autonumber-formats.ts",
9897
- surfaces: CLI_ONLY,
9898
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10598
+ // Runtime publish gate (#4716): an autonumber referencing a field the
10599
+ // object does not carry is broken from the first record; only the error
10600
+ // arm blocks — the optional-field arm is `warning` and rides the
10601
+ // advisory channel like every other non-error finding (#4463 P1).
10602
+ surfaces: CLI_AND_RUNTIME,
10603
+ runtimeTypes: ["object"],
9899
10604
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
9900
10605
  severity: f.severity,
9901
10606
  rule: f.rule,
@@ -9936,12 +10641,12 @@ var AUTHORING_RULES = [
9936
10641
  commands: ["validate", "build"],
9937
10642
  source: "packages/lint/src/data-model-rules.ts",
9938
10643
  surfaces: CLI_ONLY,
9939
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10644
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9940
10645
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
9941
10646
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9942
10647
  severity: f.severity === "suggestion" ? "info" : f.severity,
9943
10648
  rule: f.rule,
9944
- where: f.path,
10649
+ where: f.where,
9945
10650
  path: f.path,
9946
10651
  message: f.message,
9947
10652
  hint: f.fix ?? ""
@@ -9957,12 +10662,12 @@ var AUTHORING_RULES = [
9957
10662
  commands: ["validate", "build"],
9958
10663
  source: "packages/lint/src/data-model-rules.ts",
9959
10664
  surfaces: CLI_ONLY,
9960
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10665
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9961
10666
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
9962
10667
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9963
10668
  severity: f.severity === "suggestion" ? "info" : f.severity,
9964
10669
  rule: f.rule,
9965
- where: f.path,
10670
+ where: f.where,
9966
10671
  path: f.path,
9967
10672
  message: f.message,
9968
10673
  hint: f.fix ?? ""
@@ -9979,12 +10684,12 @@ var AUTHORING_RULES = [
9979
10684
  commands: ["validate", "build"],
9980
10685
  source: "packages/lint/src/data-model-rules.ts",
9981
10686
  surfaces: CLI_ONLY,
9982
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10687
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9983
10688
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
9984
10689
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9985
10690
  severity: f.severity === "suggestion" ? "info" : f.severity,
9986
10691
  rule: f.rule,
9987
- where: f.path,
10692
+ where: f.where,
9988
10693
  path: f.path,
9989
10694
  message: f.message,
9990
10695
  hint: f.fix ?? ""
@@ -10112,16 +10817,26 @@ var AUTHORING_RULES = [
10112
10817
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10113
10818
  run: (stack) => validateOrgAxisRedLines(stack)
10114
10819
  },
10115
- // #4698 — the "declared but never read" gate, for the one surface where the
10116
- // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
10117
- // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
10118
- // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
10119
- // that does not lower means the rule is SKIPPED at boot, so the grant is
10120
- // declared and does not exist. The lint calls that same compiler, from the
10121
- // same package, with the same options the verdict cannot drift from the
10122
- // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
10123
- // docblock states: the whole authorable surface is enforced, and this was the
10124
- // one field where that sentence was not yet true.
10820
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
10821
+ // sharing rule where the predicate is EXACT rather than inferred.
10822
+ //
10823
+ // - `condition` has a single runtime consumer
10824
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
10825
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
10826
+ // does not lower means the rule is SKIPPED at boot.
10827
+ // - `object` decides whether the grant is refused outright: reconcile hands
10828
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
10829
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
10830
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
10831
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
10832
+ // bypass set, federated anchors) are not, and are excluded by name.
10833
+ //
10834
+ // Either way the grant is declared and does not exist. The lint calls the
10835
+ // same compiler and mirrors the same verdict function, from the same inputs
10836
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
10837
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
10838
+ // surface is enforced, and these were the fields where that sentence was not
10839
+ // yet true.
10125
10840
  {
10126
10841
  name: "validateSharingRuleEnforceability",
10127
10842
  tier: "gating",
@@ -10129,7 +10844,7 @@ var AUTHORING_RULES = [
10129
10844
  commands: ALL,
10130
10845
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10131
10846
  surfaces: CLI_ONLY,
10132
- surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. The rule itself is snapshot-safe \u2014 it reads ONLY `stack.sharingRules[].condition` and needs no other collection \u2014 so widening it here is a `runtimeTypes: ['sharing_rule']` edit once the gate accepts that type, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10847
+ surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. This entry used to add that the rule reads ONLY `stack.sharingRules[].condition` and needs no other collection, so crossing was a lone `runtimeTypes` edit. #9698 FALSIFIED that: the anchor arm resolves `sharingRules[].object` against `stack.objects` to read the anchor's OWD, so the rule is now cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`, #8309), so the remaining gap is unchanged in SHAPE \u2014 the gate must accept a `sharing_rule` type and the snapshot must carry `sharingRules`, which it does not \u2014 but it is now TWO collections, not one. Crossing with `sharingRules` uncarried would enforce this id for zero of its inputs while the entry claimed the door (#7220). Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10133
10848
  run: (stack) => validateSharingRuleEnforceability(stack)
10134
10849
  },
10135
10850
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10168,8 +10883,15 @@ var AUTHORING_RULES = [
10168
10883
  input: "parsed",
10169
10884
  commands: ALL,
10170
10885
  source: "packages/lint/src/validate-rule-compilability.ts",
10171
- surfaces: CLI_ONLY,
10172
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10886
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
10887
+ // judged snapshot actually carries a `json_schema` validation — so an
10888
+ // ordinary object write (no `json_schema` anywhere) still loads no
10889
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
10890
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
10891
+ // carrying such a rule) is the measured, adjudicated price of refusing a
10892
+ // validation rule that would otherwise ship compiled-by-nothing.
10893
+ surfaces: CLI_AND_RUNTIME,
10894
+ runtimeTypes: ["object"],
10173
10895
  run: (stack) => validateRuleCompilability(stack)
10174
10896
  },
10175
10897
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10190,8 +10912,12 @@ var AUTHORING_RULES = [
10190
10912
  input: "parsed",
10191
10913
  commands: ALL,
10192
10914
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10193
- surfaces: CLI_ONLY,
10194
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10915
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
10916
+ // the two judgements over one artifact stay on one side of the wall
10917
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
10918
+ // format set is only enumerated once a schema actually names a format.
10919
+ surfaces: CLI_AND_RUNTIME,
10920
+ runtimeTypes: ["object"],
10195
10921
  run: (stack) => validateRuleSchemaFormats(stack)
10196
10922
  }
10197
10923
  ];
@@ -10233,7 +10959,20 @@ var TYPE_TO_STACK_KEY = {
10233
10959
  permission: "permissions",
10234
10960
  book: "books"
10235
10961
  };
10236
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
10962
+ function narrowObjectsToPackageClosure(objects, scope) {
10963
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
10964
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
10965
+ return objects.filter((entry) => {
10966
+ if (!entry || typeof entry !== "object") return true;
10967
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
10968
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
10969
+ if (reachable.has(owner)) return true;
10970
+ return isSystemObject(entry);
10971
+ });
10972
+ }
10973
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
10974
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
10975
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
10237
10976
  function runtimeAuthoringRulesFor(type) {
10238
10977
  return AUTHORING_RULES.filter(
10239
10978
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10259,7 +10998,8 @@ function buildRuntimeWriteSnapshots(args) {
10259
10998
  const itemName = typeof item.name === "string" ? item.name : void 0;
10260
10999
  const baseline = {};
10261
11000
  for (const key of CONTEXT_STACK_KEYS) {
10262
- const collection = args.context?.[key] ?? [];
11001
+ const raw = args.context?.[key] ?? [];
11002
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
10263
11003
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10264
11004
  }
10265
11005
  const candidate = {
@@ -10268,6 +11008,20 @@ function buildRuntimeWriteSnapshots(args) {
10268
11008
  };
10269
11009
  return { baseline, candidate };
10270
11010
  }
11011
+ var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
11012
+ var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
11013
+ var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
11014
+ function nameKeyFindingPath(path, candidate) {
11015
+ const m = TOP_LEVEL_INDEX.exec(path);
11016
+ if (!m) return path;
11017
+ const [, stackKey, index, rest] = m;
11018
+ if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
11019
+ const collection = candidate[stackKey];
11020
+ const entry = collection?.[Number(index)];
11021
+ const name = entry && typeof entry === "object" ? entry.name : void 0;
11022
+ if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
11023
+ return `${stackKey}.${name}${rest}`;
11024
+ }
10271
11025
  function runRules(rules, stack, ctx) {
10272
11026
  const findings = [];
10273
11027
  for (const rule of rules) {
@@ -10293,12 +11047,16 @@ function runRuntimeAuthoringRules(args) {
10293
11047
  const snapshots = buildRuntimeWriteSnapshots({
10294
11048
  type: args.type,
10295
11049
  item: args.item,
10296
- ...args.context !== void 0 ? { context: args.context } : {}
11050
+ ...args.context !== void 0 ? { context: args.context } : {},
11051
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10297
11052
  });
10298
11053
  if (!snapshots) return empty;
10299
- const ctx = { sduiManifest: args.sduiManifest };
11054
+ const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
10300
11055
  const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10301
- const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
11056
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
11057
+ const path = nameKeyFindingPath(f.path, snapshots.candidate);
11058
+ return path === f.path ? f : { ...f, path };
11059
+ });
10302
11060
  return {
10303
11061
  errors: added.filter((f) => f.severity === "error"),
10304
11062
  advisories: added.filter((f) => f.severity !== "error"),
@@ -10307,6 +11065,7 @@ function runRuntimeAuthoringRules(args) {
10307
11065
  }
10308
11066
  export {
10309
11067
  buildRuntimeWriteSnapshots,
11068
+ narrowObjectsToPackageClosure,
10310
11069
  runRuntimeAuthoringRules,
10311
11070
  runtimeAuthoringRulesFor,
10312
11071
  runtimeGatedTypes,