@objectstack/lint 17.0.0 → 17.1.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.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var runtime_exports = {};
22
22
  __export(runtime_exports, {
23
23
  buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
24
+ narrowObjectsToPackageClosure: () => narrowObjectsToPackageClosure,
24
25
  runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
25
26
  runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
26
27
  runtimeGatedTypes: () => runtimeGatedTypes,
@@ -276,10 +277,10 @@ var OUTCOME_CLAUSE = {
276
277
  "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
277
278
  "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)"
278
279
  };
279
- function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
280
+ function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
280
281
  const owner = objectName ? `'${objectName}'` : "this object";
281
- const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
282
- 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}`;
282
+ const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
283
+ 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}`;
283
284
  }
284
285
 
285
286
  // src/validate-expressions.ts
@@ -462,10 +463,10 @@ function validateStackExpressions(stack) {
462
463
  if (!nullableFields || nullableFields.size === 0) return;
463
464
  const source = celSourceOf(raw);
464
465
  if (!source) return;
465
- for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
466
+ for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
466
467
  issues.push({
467
468
  where,
468
- message: nullGuardMessage(subject, objectName, finding, outcome),
469
+ message: nullGuardMessage(subject, objectName, finding2, outcome),
469
470
  source,
470
471
  severity: "error"
471
472
  });
@@ -489,7 +490,7 @@ function validateStackExpressions(stack) {
489
490
  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";
490
491
  const FIELD_RULE_SLOT_CONSEQUENCE = {
491
492
  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)",
492
- 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",
493
+ 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",
493
494
  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",
494
495
  // Listed rather than left to the `??` below, so the map covers every slot
495
496
  // the field walk passes and the default stays unreachable. `FieldSchema`
@@ -548,14 +549,15 @@ function validateStackExpressions(stack) {
548
549
  if (retired.length > 0) {
549
550
  issues.push({
550
551
  where: `${at} \xB7 node '${node.id}' (script) callable`,
551
- 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
552
- // behaviour, never the retired key's fate — "rewrite it" reads two ways
553
- // over a branch that DELETES the key (template/recipients/variables/script),
554
- // "rewrite existing sources" only one. Plain-quoted (not a template literal)
552
+ 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
553
+ // sentence names the TOOL's behaviour, never the retired key's fate —
554
+ // "rewrite it" read two ways over a branch that DELETES the key
555
+ // (template/recipients/variables/script), and the tool never rewrote a
556
+ // source file at all. Plain-quoted (not a template literal)
555
557
  // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
556
558
  // widened scan (#7030) on the same textual shape as the spec corpus — no
557
559
  // interpolation lives in this clause, so nothing is lost switching quote style.
558
- "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
560
+ "Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
559
561
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
560
562
  });
561
563
  } else if (!fn) {
@@ -626,10 +628,10 @@ function validateStackExpressions(stack) {
626
628
  f.expression,
627
629
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
628
630
  );
629
- const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
630
- for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
631
- for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
632
- warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
631
+ const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
632
+ for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
633
+ for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
634
+ warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
633
635
  }
634
636
  }
635
637
  }
@@ -1278,7 +1280,6 @@ function asArray4(v) {
1278
1280
  function strName(v) {
1279
1281
  return typeof v === "string" && v.length > 0 ? v : void 0;
1280
1282
  }
1281
- var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
1282
1283
  var URL_COLLECTION_TO_STACK_KEY = {
1283
1284
  object: "objects",
1284
1285
  objects: "objects",
@@ -1323,13 +1324,10 @@ function collectKnownTargets(stack) {
1323
1324
  return { actions, objects, reports, dashboards, pages, views };
1324
1325
  }
1325
1326
  function resolveActionTarget(actionType, target, known) {
1326
- if (known.actions.has(target)) return true;
1327
1327
  if (actionType === "modal") {
1328
- if (known.objects.has(target)) return true;
1329
- const m = MODAL_VERB_RE.exec(target);
1330
- if (m && known.objects.has(m[1])) return true;
1328
+ return known.pages.has(target);
1331
1329
  }
1332
- return false;
1330
+ return known.actions.has(target);
1333
1331
  }
1334
1332
  function resolveUrlRoute(target, known) {
1335
1333
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
@@ -1359,14 +1357,13 @@ function validateDashboardActionRefs(stack) {
1359
1357
  const actionType = strName(action.actionType) ?? "url";
1360
1358
  if (actionType === "script" || actionType === "modal") {
1361
1359
  if (resolveActionTarget(actionType, target, known)) return;
1362
- const kindWord = actionType === "script" ? "script" : "modal";
1363
1360
  findings.push({
1364
1361
  severity: "error",
1365
1362
  rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
1366
1363
  where,
1367
1364
  path,
1368
- 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).`,
1369
- 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.`
1365
+ 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).`,
1366
+ 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.`
1370
1367
  });
1371
1368
  return;
1372
1369
  }
@@ -1408,7 +1405,7 @@ function validateDashboardActionRefs(stack) {
1408
1405
  var import_data4 = require("@objectstack/spec/data");
1409
1406
 
1410
1407
  // src/filter-walk.ts
1411
- var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1408
+ var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
1412
1409
  function asArray5(v) {
1413
1410
  if (Array.isArray(v)) return v;
1414
1411
  if (v && typeof v === "object") {
@@ -1512,8 +1509,146 @@ function validateFilterTokens(stack) {
1512
1509
  return out;
1513
1510
  }
1514
1511
 
1515
- // src/validate-empty-combinators.ts
1512
+ // src/validate-preset-comparands.ts
1516
1513
  var import_data5 = require("@objectstack/spec/data");
1514
+ var import_ui2 = require("@objectstack/spec/ui");
1515
+ var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
1516
+ var PRESET_COMPARAND_SURFACES = [
1517
+ { key: "dashboards", kind: "dashboard" },
1518
+ { key: "objects", kind: "object" },
1519
+ { key: "views", kind: "view" },
1520
+ { key: "reports", kind: "report" },
1521
+ { key: "datasets", kind: "dataset" },
1522
+ { key: "pages", kind: "page" },
1523
+ { key: "apps", kind: "app" },
1524
+ { key: "flows", kind: "flow" }
1525
+ ];
1526
+ var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
1527
+ var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
1528
+ var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
1529
+ "greater_than",
1530
+ "greater_than_or_equal",
1531
+ "less_than",
1532
+ "less_than_or_equal",
1533
+ "before",
1534
+ "after"
1535
+ ]);
1536
+ var MAX_DEPTH = 32;
1537
+ function isPlainObject(v) {
1538
+ return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
1539
+ }
1540
+ function finding(where, path, preset, operator) {
1541
+ return {
1542
+ severity: "error",
1543
+ rule: FILTER_PRESET_COMPARAND,
1544
+ where,
1545
+ path,
1546
+ message: (0, import_data5.bareDateRangePresetComparandMessage)(preset, operator),
1547
+ 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."
1548
+ };
1549
+ }
1550
+ function judgeConditionNode(node, path, where, out, depth) {
1551
+ if (depth > MAX_DEPTH) return;
1552
+ for (const [key, value] of Object.entries(node)) {
1553
+ const here = `${path}.${key}`;
1554
+ if (key === "$and" || key === "$or") {
1555
+ if (Array.isArray(value)) {
1556
+ value.forEach((arm, i) => {
1557
+ if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
1558
+ });
1559
+ }
1560
+ continue;
1561
+ }
1562
+ if (key === "$not") {
1563
+ if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
1564
+ continue;
1565
+ }
1566
+ if (key.startsWith("$")) continue;
1567
+ if (!isPlainObject(value)) continue;
1568
+ const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
1569
+ if (!hasOps) {
1570
+ judgeConditionNode(value, here, where, out, depth + 1);
1571
+ continue;
1572
+ }
1573
+ for (const [op, comparand] of Object.entries(value)) {
1574
+ if (ORDERING_DOLLAR_OPS.has(op) && (0, import_data5.isDateRangePresetName)(comparand)) {
1575
+ out.push(finding(where, `${here}.${op}`, comparand, op));
1576
+ continue;
1577
+ }
1578
+ if (op === "$between" && Array.isArray(comparand)) {
1579
+ comparand.forEach((endpoint, i) => {
1580
+ if ((0, import_data5.isDateRangePresetName)(endpoint)) {
1581
+ out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
1582
+ }
1583
+ });
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+ function judgeFilterRule(rule, path, where, out) {
1589
+ const operator = (0, import_ui2.normalizeFilterOperator)(rule.operator);
1590
+ if (typeof operator !== "string") return;
1591
+ const value = rule.value;
1592
+ if (ORDERING_RULE_OPS.has(operator) && (0, import_data5.isDateRangePresetName)(value)) {
1593
+ out.push(finding(where, `${path}.value`, value, operator));
1594
+ return;
1595
+ }
1596
+ if (operator === "between" && Array.isArray(value)) {
1597
+ value.forEach((endpoint, i) => {
1598
+ if ((0, import_data5.isDateRangePresetName)(endpoint)) {
1599
+ out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
1600
+ }
1601
+ });
1602
+ }
1603
+ }
1604
+ function judgeTriple(triple, path, where, out) {
1605
+ const op = triple[1];
1606
+ if (typeof op !== "string") return;
1607
+ const canonical = (0, import_data5.canonicalAstOperator)(op);
1608
+ const value = triple[2];
1609
+ if (ORDERING_INFIX_OPS.has(canonical) && (0, import_data5.isDateRangePresetName)(value)) {
1610
+ out.push(finding(where, `${path}[2]`, value, op));
1611
+ return;
1612
+ }
1613
+ if (canonical === "between" && Array.isArray(value)) {
1614
+ value.forEach((endpoint, i) => {
1615
+ if ((0, import_data5.isDateRangePresetName)(endpoint)) {
1616
+ out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
1617
+ }
1618
+ });
1619
+ }
1620
+ }
1621
+ function judgeFilterValue(node, path, where, out, depth) {
1622
+ if (depth > MAX_DEPTH) return;
1623
+ if (Array.isArray(node)) {
1624
+ if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && import_data5.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
1625
+ judgeTriple(node, path, where, out);
1626
+ return;
1627
+ }
1628
+ node.forEach((member, i) => {
1629
+ if (typeof member === "string") return;
1630
+ judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
1631
+ });
1632
+ return;
1633
+ }
1634
+ if (!isPlainObject(node)) return;
1635
+ if (typeof node.field === "string" && typeof node.operator === "string") {
1636
+ judgeFilterRule(node, path, where, out);
1637
+ return;
1638
+ }
1639
+ judgeConditionNode(node, path, where, out, depth);
1640
+ }
1641
+ function validatePresetComparands(stack) {
1642
+ if (!stack || typeof stack !== "object") return [];
1643
+ const out = [];
1644
+ walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
1645
+ judgeFilterValue(value, path, where, out, 0);
1646
+ });
1647
+ return out;
1648
+ }
1649
+
1650
+ // src/validate-empty-combinators.ts
1651
+ var import_data6 = require("@objectstack/spec/data");
1517
1652
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
1518
1653
  var FILTER_EMPTY_NODE = "filter-empty-node";
1519
1654
  var EMPTY_COMBINATOR_SURFACES = [
@@ -1532,12 +1667,12 @@ function isFilterNode(value) {
1532
1667
  return proto === Object.prototype || proto === null;
1533
1668
  }
1534
1669
  var VERDICT_OF = {
1535
- $and: (0, import_data5.reduceFilterVerdict)({ $and: [] }),
1536
- $or: (0, import_data5.reduceFilterVerdict)({ $or: [] }),
1537
- $not: (0, import_data5.reduceFilterVerdict)({ $not: {} }),
1538
- node: (0, import_data5.reduceFilterVerdict)({}),
1670
+ $and: (0, import_data6.reduceFilterVerdict)({ $and: [] }),
1671
+ $or: (0, import_data6.reduceFilterVerdict)({ $or: [] }),
1672
+ $not: (0, import_data6.reduceFilterVerdict)({ $not: {} }),
1673
+ node: (0, import_data6.reduceFilterVerdict)({}),
1539
1674
  /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
1540
- orWithEmptyBranch: (0, import_data5.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
1675
+ orWithEmptyBranch: (0, import_data6.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
1541
1676
  };
1542
1677
  function rows(verdict) {
1543
1678
  if (verdict === "true") return "matches EVERY row";
@@ -1822,9 +1957,10 @@ function validateObjectReferences(stack) {
1822
1957
  }
1823
1958
 
1824
1959
  // src/validate-searchable-fields.ts
1825
- var import_data6 = require("@objectstack/spec/data");
1960
+ var import_data7 = require("@objectstack/spec/data");
1826
1961
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1827
1962
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1963
+ var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
1828
1964
  function asArray7(v) {
1829
1965
  if (Array.isArray(v)) return v;
1830
1966
  if (v && typeof v === "object") {
@@ -1870,7 +2006,7 @@ function resolveAllowedSet(target) {
1870
2006
  fields = { ...fields };
1871
2007
  for (const f of systemDeclared) fields[f] = {};
1872
2008
  }
1873
- const { allowed, source } = (0, import_data6.resolveSearchFieldResolution)({
2009
+ const { allowed, source } = (0, import_data7.resolveSearchFieldResolution)({
1874
2010
  fields,
1875
2011
  searchableFields: target.searchableFields,
1876
2012
  displayField: target.displayField
@@ -1915,7 +2051,7 @@ function indexObjectSearchTargets(stack) {
1915
2051
  }
1916
2052
  return fieldsByObject;
1917
2053
  }
1918
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
2054
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
1919
2055
  const findings = [];
1920
2056
  if (!Array.isArray(declared) || declared.length === 0) return findings;
1921
2057
  if (!objectName) return findings;
@@ -1923,6 +2059,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1923
2059
  const target = fieldsByObject.get(objectName);
1924
2060
  if (!target) return findings;
1925
2061
  const known = target.names;
2062
+ const anchors = unprovisionedAnchors?.get(objectName);
1926
2063
  const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
1927
2064
  for (let i = 0; i < declared.length; i++) {
1928
2065
  const entry = declared[i];
@@ -1940,7 +2077,17 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1940
2077
  });
1941
2078
  continue;
1942
2079
  }
1943
- if ((0, import_data6.isVirtualSearchField)(target.fields[name])) {
2080
+ if (anchors?.has(name)) {
2081
+ findings.push({
2082
+ severity: "warning",
2083
+ rule: SEARCHABLE_FIELD_UNPROVISIONED,
2084
+ where,
2085
+ path: `${path}[${i}]`,
2086
+ 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.`),
2087
+ hint: unprovisionedAnchorHint(objectName, name)
2088
+ });
2089
+ }
2090
+ if ((0, import_data7.isVirtualSearchField)(target.fields[name])) {
1944
2091
  const vtype = target.fields[name]?.type;
1945
2092
  findings.push({
1946
2093
  severity: "error",
@@ -1968,7 +2115,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1968
2115
  }
1969
2116
  const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
1970
2117
  let why;
1971
- if (import_data6.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
2118
+ if (import_data7.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1972
2119
  why = "a system/audit column, which the auto-default set never includes";
1973
2120
  } else if (meta?.hidden) {
1974
2121
  why = "hidden";
@@ -1982,7 +2129,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1982
2129
  rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1983
2130
  where,
1984
2131
  path: `${path}[${i}]`,
1985
- message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data6.SEARCHABLE_TEXTUAL_TYPES, ...import_data6.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
2132
+ message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data7.SEARCHABLE_TEXTUAL_TYPES, ...import_data7.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
1986
2133
  hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1987
2134
  });
1988
2135
  }
@@ -1993,9 +2140,19 @@ function validateSearchableFields(stack) {
1993
2140
  if (!isRec3(stack)) return findings;
1994
2141
  const objects = asArray7(stack.objects);
1995
2142
  const fieldsByObject = indexObjectSearchTargets(stack);
2143
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
1996
2144
  const check = (declared, objectName, where, path, subject, role) => {
1997
2145
  findings.push(
1998
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
2146
+ ...checkSearchableFieldList(
2147
+ declared,
2148
+ objectName,
2149
+ fieldsByObject,
2150
+ where,
2151
+ path,
2152
+ subject,
2153
+ role,
2154
+ unprovisionedAnchors
2155
+ )
1999
2156
  );
2000
2157
  };
2001
2158
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2064,33 +2221,203 @@ function listViewObject(listView) {
2064
2221
  return isRec3(data) ? strName3(data.object) : void 0;
2065
2222
  }
2066
2223
 
2067
- // src/page-walk.ts
2224
+ // src/validate-sortable-fields.ts
2225
+ var import_data8 = require("@objectstack/spec/data");
2226
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
2227
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
2068
2228
  function isRec4(v) {
2069
2229
  return !!v && typeof v === "object" && !Array.isArray(v);
2070
2230
  }
2071
2231
  function strName4(v) {
2072
2232
  return typeof v === "string" && v.length > 0 ? v : void 0;
2073
2233
  }
2234
+ function readSortKeys(declared) {
2235
+ const fromShorthand = (raw, at) => {
2236
+ const trimmed = raw.trim();
2237
+ if (!trimmed) return void 0;
2238
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
2239
+ return bare ? { field: bare, at } : void 0;
2240
+ };
2241
+ if (typeof declared === "string") {
2242
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
2243
+ }
2244
+ if (Array.isArray(declared)) {
2245
+ const keys = [];
2246
+ for (let i = 0; i < declared.length; i++) {
2247
+ const el = declared[i];
2248
+ if (typeof el === "string") {
2249
+ const k = fromShorthand(el, `[${i}]`);
2250
+ if (k) keys.push(k);
2251
+ continue;
2252
+ }
2253
+ if (!isRec4(el)) continue;
2254
+ const field = strName4(el.field);
2255
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
2256
+ }
2257
+ return keys;
2258
+ }
2259
+ return [];
2260
+ }
2261
+ function suggest4(target, known) {
2262
+ let best;
2263
+ let bestScore = Infinity;
2264
+ for (const candidate of known) {
2265
+ const d = distance3(target, candidate);
2266
+ if (d < bestScore) {
2267
+ bestScore = d;
2268
+ best = candidate;
2269
+ }
2270
+ }
2271
+ const limit = Math.max(2, Math.floor(target.length / 3));
2272
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
2273
+ }
2274
+ function distance3(a, b) {
2275
+ const m = a.length;
2276
+ const n = b.length;
2277
+ if (m === 0) return n;
2278
+ if (n === 0) return m;
2279
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
2280
+ for (let i = 1; i <= m; i++) {
2281
+ const curr = [i, ...new Array(n).fill(0)];
2282
+ for (let j = 1; j <= n; j++) {
2283
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2284
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2285
+ }
2286
+ prev = curr;
2287
+ }
2288
+ return prev[n];
2289
+ }
2290
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
2291
+ const findings = [];
2292
+ if (declared === void 0 || declared === null) return findings;
2293
+ if (!objectName) return findings;
2294
+ if (!fieldsByObject.has(objectName)) return findings;
2295
+ const target = fieldsByObject.get(objectName);
2296
+ if (!target) return findings;
2297
+ const known = target.names;
2298
+ for (const key of readSortKeys(declared)) {
2299
+ const name = key.field;
2300
+ const head = name.split(".")[0];
2301
+ if (SYSTEM_FIELDS.has(head)) continue;
2302
+ if (!known.has(head)) {
2303
+ const dotted = name.includes(".");
2304
+ findings.push({
2305
+ severity: "error",
2306
+ rule: SORT_FIELD_UNKNOWN,
2307
+ where,
2308
+ path: `${path}${key.at}`,
2309
+ 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)),
2310
+ 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(", ")}.` : "")
2311
+ });
2312
+ continue;
2313
+ }
2314
+ const meta = target.fields[name];
2315
+ if ((0, import_data8.isVirtualSearchField)(meta)) {
2316
+ const vtype = meta?.type;
2317
+ findings.push({
2318
+ severity: "error",
2319
+ rule: SORT_FIELD_UNSORTABLE,
2320
+ where,
2321
+ path: `${path}${key.at}`,
2322
+ 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.`,
2323
+ 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.`
2324
+ });
2325
+ }
2326
+ }
2327
+ return findings;
2328
+ }
2329
+ function validateSortableFields(stack) {
2330
+ const findings = [];
2331
+ if (!isRec4(stack)) return findings;
2332
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
2333
+ const fieldsByObject = indexObjectSearchTargets(stack);
2334
+ const check = (declared, objectName, where, path, subject) => {
2335
+ findings.push(
2336
+ ...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
2337
+ );
2338
+ };
2339
+ for (let oi = 0; oi < objects.length; oi++) {
2340
+ const obj = objects[oi];
2341
+ if (!isRec4(obj)) continue;
2342
+ const objName = strName4(obj.name);
2343
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
2344
+ if (isRec4(obj.listViews)) {
2345
+ for (const [key, lv] of Object.entries(obj.listViews)) {
2346
+ if (!isRec4(lv)) continue;
2347
+ check(
2348
+ lv.sort,
2349
+ // A built-in list view belongs to its object; an inline `data.object`
2350
+ // may still retarget it (ADR-0047 allows the explicit binding).
2351
+ listViewObject2(lv) ?? objName,
2352
+ `${label2} \u203A listViews.${key}`,
2353
+ `objects[${oi}].listViews.${key}.sort`,
2354
+ "list-view sort"
2355
+ );
2356
+ }
2357
+ }
2358
+ }
2359
+ const views = Array.isArray(stack.views) ? stack.views : [];
2360
+ for (let vi = 0; vi < views.length; vi++) {
2361
+ const view = views[vi];
2362
+ if (!isRec4(view)) continue;
2363
+ const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
2364
+ const viewObject = strName4(view.objectName) ?? strName4(view.object);
2365
+ if (isRec4(view.list)) {
2366
+ check(
2367
+ view.list.sort,
2368
+ listViewObject2(view.list) ?? viewObject,
2369
+ `view "${viewLabel2}" \u203A list`,
2370
+ `views[${vi}].list.sort`,
2371
+ "list-view sort"
2372
+ );
2373
+ }
2374
+ if (isRec4(view.listViews)) {
2375
+ for (const [key, lv] of Object.entries(view.listViews)) {
2376
+ if (!isRec4(lv)) continue;
2377
+ check(
2378
+ lv.sort,
2379
+ listViewObject2(lv) ?? viewObject,
2380
+ `view "${viewLabel2}" \u203A listViews.${key}`,
2381
+ `views[${vi}].listViews.${key}.sort`,
2382
+ "list-view sort"
2383
+ );
2384
+ }
2385
+ }
2386
+ }
2387
+ return findings;
2388
+ }
2389
+ function listViewObject2(listView) {
2390
+ const data = listView.data;
2391
+ return isRec4(data) ? strName4(data.object) : void 0;
2392
+ }
2393
+
2394
+ // src/page-walk.ts
2395
+ function isRec5(v) {
2396
+ return !!v && typeof v === "object" && !Array.isArray(v);
2397
+ }
2398
+ function strName5(v) {
2399
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2400
+ }
2074
2401
  var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
2075
2402
  function isSourceAuthoredPage(page) {
2076
- const kind = strName4(page.kind);
2403
+ const kind = strName5(page.kind);
2077
2404
  return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
2078
2405
  }
2079
2406
  function walkPageComponents(page, pagePath) {
2080
2407
  const out = [];
2081
- if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
2082
- const pageObject = strName4(page.object);
2408
+ if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
2409
+ const pageObject = strName5(page.object);
2083
2410
  const visit = (node, path, inheritedObject) => {
2084
- if (!isRec4(node)) return;
2085
- const props = isRec4(node.properties) ? node.properties : void 0;
2086
- const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
2087
- const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
2411
+ if (!isRec5(node)) return;
2412
+ const props = isRec5(node.properties) ? node.properties : void 0;
2413
+ const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
2414
+ const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
2088
2415
  out.push({ component: node, path, objectName });
2089
2416
  if (!props) return;
2090
2417
  if (Array.isArray(props.items)) {
2091
2418
  for (let i = 0; i < props.items.length; i++) {
2092
2419
  const item = props.items[i];
2093
- if (!isRec4(item) || !Array.isArray(item.children)) continue;
2420
+ if (!isRec5(item) || !Array.isArray(item.children)) continue;
2094
2421
  for (let c = 0; c < item.children.length; c++) {
2095
2422
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
2096
2423
  }
@@ -2112,12 +2439,12 @@ function walkPageComponents(page, pagePath) {
2112
2439
  const regions = Array.isArray(page.regions) ? page.regions : [];
2113
2440
  for (let r = 0; r < regions.length; r++) {
2114
2441
  const region = regions[r];
2115
- if (!isRec4(region) || !Array.isArray(region.components)) continue;
2442
+ if (!isRec5(region) || !Array.isArray(region.components)) continue;
2116
2443
  for (let c = 0; c < region.components.length; c++) {
2117
2444
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
2118
2445
  }
2119
2446
  }
2120
- const slots = isRec4(page.slots) ? page.slots : void 0;
2447
+ const slots = isRec5(page.slots) ? page.slots : void 0;
2121
2448
  if (slots) {
2122
2449
  for (const [slot, value] of Object.entries(slots)) {
2123
2450
  const list3 = Array.isArray(value) ? value : [value];
@@ -2139,13 +2466,13 @@ function asArray8(v) {
2139
2466
  }
2140
2467
  return [];
2141
2468
  }
2142
- function strName5(v) {
2469
+ function strName6(v) {
2143
2470
  return typeof v === "string" && v.length > 0 ? v : void 0;
2144
2471
  }
2145
2472
  function strList(v) {
2146
2473
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2147
2474
  }
2148
- function distance3(a, b) {
2475
+ function distance4(a, b) {
2149
2476
  const m = a.length;
2150
2477
  const n = b.length;
2151
2478
  if (m === 0) return n;
@@ -2161,11 +2488,11 @@ function distance3(a, b) {
2161
2488
  }
2162
2489
  return prev[n];
2163
2490
  }
2164
- function suggest4(target, known) {
2491
+ function suggest5(target, known) {
2165
2492
  let best;
2166
2493
  let bestScore = Infinity;
2167
2494
  for (const candidate of known) {
2168
- const d = distance3(target, candidate);
2495
+ const d = distance4(target, candidate);
2169
2496
  if (d < bestScore) {
2170
2497
  bestScore = d;
2171
2498
  best = candidate;
@@ -2177,13 +2504,13 @@ function suggest4(target, known) {
2177
2504
  function collectActionNames(stack) {
2178
2505
  const names = /* @__PURE__ */ new Set();
2179
2506
  for (const action of asArray8(stack.actions)) {
2180
- const n = strName5(action?.name);
2507
+ const n = strName6(action?.name);
2181
2508
  if (n) names.add(n);
2182
2509
  }
2183
2510
  for (const obj of asArray8(stack.objects)) {
2184
2511
  if (!obj || typeof obj !== "object") continue;
2185
2512
  for (const action of asArray8(obj.actions)) {
2186
- const n = strName5(action?.name);
2513
+ const n = strName6(action?.name);
2187
2514
  if (n) names.add(n);
2188
2515
  }
2189
2516
  }
@@ -2200,7 +2527,7 @@ function validateActionNameRefs(stack) {
2200
2527
  rule: ACTION_NAME_UNDEFINED,
2201
2528
  where,
2202
2529
  path,
2203
- 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),
2530
+ 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),
2204
2531
  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(", ")}.` : "")
2205
2532
  });
2206
2533
  };
@@ -2226,7 +2553,7 @@ function validateActionNameRefs(stack) {
2226
2553
  if (!def || typeof def !== "object") continue;
2227
2554
  if (def.execution !== "aggregate") continue;
2228
2555
  if (def.actionDef !== void 0) continue;
2229
- const name = strName5(def.name);
2556
+ const name = strName6(def.name);
2230
2557
  if (!name) continue;
2231
2558
  check(
2232
2559
  name,
@@ -2241,7 +2568,7 @@ function validateActionNameRefs(stack) {
2241
2568
  for (let vi = 0; vi < views.length; vi++) {
2242
2569
  const view = views[vi];
2243
2570
  if (!view || typeof view !== "object") continue;
2244
- const viewName = strName5(view.name) ?? strName5(view.object) ?? `#${vi}`;
2571
+ const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
2245
2572
  const owner = `view "${viewName}"`;
2246
2573
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
2247
2574
  const listViews = view.listViews;
@@ -2257,7 +2584,7 @@ function validateActionNameRefs(stack) {
2257
2584
  if (!obj || typeof obj !== "object") continue;
2258
2585
  const objListViews = obj.listViews;
2259
2586
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
2260
- const owner = `object "${strName5(obj.name) ?? `#${oi}`}"`;
2587
+ const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
2261
2588
  for (const [key, lv] of Object.entries(objListViews)) {
2262
2589
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
2263
2590
  }
@@ -2266,7 +2593,7 @@ function validateActionNameRefs(stack) {
2266
2593
  for (let pi = 0; pi < pages.length; pi++) {
2267
2594
  const page = pages[pi];
2268
2595
  if (!page || typeof page !== "object") continue;
2269
- const pageName = strName5(page.name) ?? `#${pi}`;
2596
+ const pageName = strName6(page.name) ?? `#${pi}`;
2270
2597
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2271
2598
  const props = component.properties;
2272
2599
  if (!props || typeof props !== "object") continue;
@@ -2274,7 +2601,7 @@ function validateActionNameRefs(stack) {
2274
2601
  for (let ai = 0; ai < names.length; ai++) {
2275
2602
  check(
2276
2603
  names[ai],
2277
- `page "${pageName}" \xB7 component "${strName5(component.type) ?? "?"}"`,
2604
+ `page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
2278
2605
  `${path}.properties.actionNames[${ai}]`,
2279
2606
  "Quick-actions bar"
2280
2607
  );
@@ -2285,7 +2612,7 @@ function validateActionNameRefs(stack) {
2285
2612
  for (let ai = 0; ai < apps.length; ai++) {
2286
2613
  const app = apps[ai];
2287
2614
  if (!app || typeof app !== "object") continue;
2288
- const appName = strName5(app.name) ?? `#${ai}`;
2615
+ const appName = strName6(app.name) ?? `#${ai}`;
2289
2616
  const walkNav = (items, basePath) => {
2290
2617
  const navItems = asArray8(items);
2291
2618
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -2293,20 +2620,20 @@ function validateActionNameRefs(stack) {
2293
2620
  if (!nav || typeof nav !== "object") continue;
2294
2621
  const navPath = `${basePath}[${ni}]`;
2295
2622
  const actionDef = nav.actionDef;
2296
- const actionName = strName5(actionDef?.actionName);
2623
+ const actionName = strName6(actionDef?.actionName);
2297
2624
  if (nav.type === "action" && actionName) {
2298
2625
  check(
2299
2626
  actionName,
2300
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2627
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2301
2628
  `${navPath}.actionDef.actionName`,
2302
2629
  "Navigation action item"
2303
2630
  );
2304
2631
  }
2305
- const runAction = strName5(nav.runAction);
2632
+ const runAction = strName6(nav.runAction);
2306
2633
  if (nav.type === "object" && runAction) {
2307
2634
  check(
2308
2635
  runAction,
2309
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2636
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2310
2637
  `${navPath}.runAction`,
2311
2638
  "Navigation deep-link auto-run"
2312
2639
  );
@@ -2333,23 +2660,23 @@ function asArray9(v) {
2333
2660
  }
2334
2661
  return [];
2335
2662
  }
2336
- function strName6(v) {
2663
+ function strName7(v) {
2337
2664
  return typeof v === "string" && v.length > 0 ? v : void 0;
2338
2665
  }
2339
- function isRec5(v) {
2666
+ function isRec6(v) {
2340
2667
  return !!v && typeof v === "object" && !Array.isArray(v);
2341
2668
  }
2342
2669
  function fieldRefsFrom(value, basePath) {
2343
2670
  const out = [];
2344
2671
  const one = (v, path) => {
2345
- const bare = strName6(v);
2672
+ const bare = strName7(v);
2346
2673
  if (bare) {
2347
2674
  out.push({ name: bare, path });
2348
2675
  return;
2349
2676
  }
2350
- if (!isRec5(v)) return;
2351
- const named = strName6(v.field) ?? strName6(v.name);
2352
- if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
2677
+ if (!isRec6(v)) return;
2678
+ const named = strName7(v.field) ?? strName7(v.name);
2679
+ if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
2353
2680
  };
2354
2681
  if (Array.isArray(value)) {
2355
2682
  for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
@@ -2375,7 +2702,12 @@ var COMPONENT_FIELD_SPECS = {
2375
2702
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2376
2703
  "record:path": { props: ["statusField"] },
2377
2704
  "element:number": { props: ["field"] },
2378
- "element:filter": { props: ["fields"] },
2705
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
2706
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
2707
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
2708
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
2709
+ // reports an authored one by name with the element-retirement prescription —
2710
+ // the same #5775/#6629 residue class as the record-picker entries below.
2379
2711
  "element:form": { props: ["fields"] },
2380
2712
  // `labelField` is the one field-bearing prop this element declares. Its former
2381
2713
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -2405,18 +2737,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2405
2737
  const sections = Array.isArray(props[key]) ? props[key] : [];
2406
2738
  for (let si = 0; si < sections.length; si++) {
2407
2739
  const section = sections[si];
2408
- if (!isRec5(section)) continue;
2740
+ if (!isRec6(section)) continue;
2409
2741
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2410
2742
  }
2411
2743
  }
2412
2744
  return refs;
2413
2745
  }
2414
2746
  function relatedListFieldRefs(props, basePath, sep = ".") {
2415
- const add = isRec5(props.add) ? props.add : void 0;
2416
- const picker = add && isRec5(add.picker) ? add.picker : void 0;
2747
+ const add = isRec6(props.add) ? props.add : void 0;
2748
+ const picker = add && isRec6(add.picker) ? add.picker : void 0;
2417
2749
  const at = (key) => `${basePath}${sep}${key}`;
2418
2750
  return {
2419
- relatedObject: strName6(props.objectName),
2751
+ relatedObject: strName7(props.objectName),
2420
2752
  related: [
2421
2753
  ...fieldRefsFrom(props.columns, at("columns")),
2422
2754
  ...sortFieldRefs(props.sort, at("sort")),
@@ -2425,7 +2757,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2425
2757
  ...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
2426
2758
  ],
2427
2759
  parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
2428
- pickerObject: picker ? strName6(picker.object) : void 0,
2760
+ pickerObject: picker ? strName7(picker.object) : void 0,
2429
2761
  picker: picker ? [
2430
2762
  ...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
2431
2763
  ...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
@@ -2434,13 +2766,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2434
2766
  }
2435
2767
  function indexObjectFields(stack) {
2436
2768
  const objectFields = /* @__PURE__ */ new Map();
2437
- if (!isRec5(stack)) return objectFields;
2769
+ if (!isRec6(stack)) return objectFields;
2438
2770
  for (const obj of asArray9(stack.objects)) {
2439
- const name = strName6(obj.name);
2771
+ const name = strName7(obj.name);
2440
2772
  if (!name) continue;
2441
2773
  const names = /* @__PURE__ */ new Set();
2442
2774
  for (const f of asArray9(obj.fields)) {
2443
- const fn = strName6(f.name);
2775
+ const fn = strName7(f.name);
2444
2776
  if (fn) names.add(fn);
2445
2777
  }
2446
2778
  objectFields.set(name, names);
@@ -2488,15 +2820,15 @@ function validatePageFieldBindings(stack) {
2488
2820
  for (let pi = 0; pi < pages.length; pi++) {
2489
2821
  const page = pages[pi];
2490
2822
  if (!page || typeof page !== "object") continue;
2491
- const pageName = strName6(page.name) ?? `#${pi}`;
2823
+ const pageName = strName7(page.name) ?? `#${pi}`;
2492
2824
  const checkRefs = (refs, objectName, where) => {
2493
2825
  findings.push(
2494
2826
  ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2495
2827
  );
2496
2828
  };
2497
2829
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2498
- const type = strName6(component.type);
2499
- const props = isRec5(component.properties) ? component.properties : void 0;
2830
+ const type = strName7(component.type);
2831
+ const props = isRec6(component.properties) ? component.properties : void 0;
2500
2832
  if (!type || !props) continue;
2501
2833
  const where = `page "${pageName}" \xB7 ${type}`;
2502
2834
  const base = `${path}.properties`;
@@ -2511,16 +2843,16 @@ function validatePageFieldBindings(stack) {
2511
2843
  if (!refs) continue;
2512
2844
  checkRefs(refs, objectName, where);
2513
2845
  }
2514
- const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2846
+ const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
2515
2847
  if (cfg) {
2516
- const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2848
+ const cfgObject = strName7(cfg.source) ?? strName7(page.object);
2517
2849
  const base = `pages[${pi}].interfaceConfig`;
2518
2850
  const refs = [
2519
2851
  ...fieldRefsFrom(cfg.columns, `${base}.columns`),
2520
2852
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2521
2853
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2522
2854
  ];
2523
- const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2855
+ const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
2524
2856
  if (userFilters) {
2525
2857
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2526
2858
  }
@@ -2542,16 +2874,16 @@ function asArray10(v) {
2542
2874
  }
2543
2875
  return [];
2544
2876
  }
2545
- function strName7(v) {
2877
+ function strName8(v) {
2546
2878
  return typeof v === "string" && v.length > 0 ? v : void 0;
2547
2879
  }
2548
2880
  function strList2(v) {
2549
2881
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2550
2882
  }
2551
- function isRec6(v) {
2883
+ function isRec7(v) {
2552
2884
  return !!v && typeof v === "object" && !Array.isArray(v);
2553
2885
  }
2554
- function distance4(a, b) {
2886
+ function distance5(a, b) {
2555
2887
  const m = a.length;
2556
2888
  const n = b.length;
2557
2889
  if (m === 0) return n;
@@ -2567,11 +2899,11 @@ function distance4(a, b) {
2567
2899
  }
2568
2900
  return prev[n];
2569
2901
  }
2570
- function suggest5(target, known) {
2902
+ function suggest6(target, known) {
2571
2903
  let best;
2572
2904
  let bestScore = Infinity;
2573
2905
  for (const c of known) {
2574
- const d = distance4(target, c);
2906
+ const d = distance5(target, c);
2575
2907
  if (d < bestScore) {
2576
2908
  bestScore = d;
2577
2909
  best = c;
@@ -2587,16 +2919,16 @@ function list2(names) {
2587
2919
  function indexDatasets(stack) {
2588
2920
  const out = /* @__PURE__ */ new Map();
2589
2921
  for (const ds of asArray10(stack.datasets)) {
2590
- const name = strName7(ds.name);
2922
+ const name = strName8(ds.name);
2591
2923
  if (!name) continue;
2592
2924
  const dimensions = /* @__PURE__ */ new Set();
2593
2925
  for (const d of asArray10(ds.dimensions)) {
2594
- const n = strName7(d.name);
2926
+ const n = strName8(d.name);
2595
2927
  if (n) dimensions.add(n);
2596
2928
  }
2597
2929
  const measures = /* @__PURE__ */ new Set();
2598
2930
  for (const m of asArray10(ds.measures)) {
2599
- const n = strName7(m.name);
2931
+ const n = strName8(m.name);
2600
2932
  if (n) measures.add(n);
2601
2933
  }
2602
2934
  out.set(name, { dimensions, measures });
@@ -2619,7 +2951,7 @@ function validateChartBindings(stack) {
2619
2951
  where: binding.where,
2620
2952
  path: `${binding.path}.dataset`,
2621
2953
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
2622
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
2954
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
2623
2955
  });
2624
2956
  return;
2625
2957
  }
@@ -2631,7 +2963,7 @@ function validateChartBindings(stack) {
2631
2963
  where: binding.where,
2632
2964
  path,
2633
2965
  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.`,
2634
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
2966
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
2635
2967
  });
2636
2968
  };
2637
2969
  const measureRef = (name, path, selected2) => {
@@ -2642,7 +2974,7 @@ function validateChartBindings(stack) {
2642
2974
  where: binding.where,
2643
2975
  path,
2644
2976
  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.`,
2645
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
2977
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
2646
2978
  });
2647
2979
  return;
2648
2980
  }
@@ -2677,26 +3009,26 @@ function validateChartBindings(stack) {
2677
3009
  const reports = asArray10(stack.reports);
2678
3010
  for (let ri = 0; ri < reports.length; ri++) {
2679
3011
  const report = reports[ri];
2680
- if (!isRec6(report)) continue;
2681
- const reportName = strName7(report.name) ?? `#${ri}`;
3012
+ if (!isRec7(report)) continue;
3013
+ const reportName = strName8(report.name) ?? `#${ri}`;
2682
3014
  const checkReportChart = (chart, dataset, values, where, path) => {
2683
- if (!isRec6(chart)) return;
3015
+ if (!isRec7(chart)) return;
2684
3016
  check({
2685
3017
  dataset,
2686
3018
  // `values` is the report's measure SELECTION, not a chart ref; feeding
2687
3019
  // it in lets the yAxis "declared but not selected" check work without
2688
3020
  // reporting the selection itself twice.
2689
3021
  values: { names: values, path: `${path}.values` },
2690
- xAxis: strName7(chart.xAxis) ? { name: strName7(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
2691
- yAxis: strName7(chart.yAxis) ? { name: strName7(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
2692
- series: asArray10(chart.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
3022
+ xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
3023
+ yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
3024
+ series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
2693
3025
  where,
2694
3026
  path: `${path}.chart`
2695
3027
  });
2696
3028
  };
2697
3029
  checkReportChart(
2698
3030
  report.chart,
2699
- strName7(report.dataset),
3031
+ strName8(report.dataset),
2700
3032
  strList2(report.values),
2701
3033
  `report "${reportName}" \xB7 chart`,
2702
3034
  `reports[${ri}]`
@@ -2704,22 +3036,22 @@ function validateChartBindings(stack) {
2704
3036
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2705
3037
  for (let bi = 0; bi < blocks.length; bi++) {
2706
3038
  const block = blocks[bi];
2707
- if (!isRec6(block)) continue;
3039
+ if (!isRec7(block)) continue;
2708
3040
  checkReportChart(
2709
3041
  block.chart,
2710
- strName7(block.dataset),
3042
+ strName8(block.dataset),
2711
3043
  strList2(block.values),
2712
- `report "${reportName}" \xB7 block "${strName7(block.name) ?? `#${bi}`}" chart`,
3044
+ `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
2713
3045
  `reports[${ri}].blocks[${bi}]`
2714
3046
  );
2715
3047
  }
2716
3048
  }
2717
3049
  const checkListChart = (container, where, path) => {
2718
- if (!isRec6(container)) return;
3050
+ if (!isRec7(container)) return;
2719
3051
  const chart = container.chart;
2720
- if (!isRec6(chart)) return;
3052
+ if (!isRec7(chart)) return;
2721
3053
  check({
2722
- dataset: strName7(chart.dataset),
3054
+ dataset: strName8(chart.dataset),
2723
3055
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
2724
3056
  values: { names: strList2(chart.values), path: `${path}.chart.values` },
2725
3057
  where,
@@ -2729,10 +3061,10 @@ function validateChartBindings(stack) {
2729
3061
  const views = asArray10(stack.views);
2730
3062
  for (let vi = 0; vi < views.length; vi++) {
2731
3063
  const view = views[vi];
2732
- if (!isRec6(view)) continue;
2733
- const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
3064
+ if (!isRec7(view)) continue;
3065
+ const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
2734
3066
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2735
- if (isRec6(view.listViews)) {
3067
+ if (isRec7(view.listViews)) {
2736
3068
  for (const [key, lv] of Object.entries(view.listViews)) {
2737
3069
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2738
3070
  }
@@ -2741,8 +3073,8 @@ function validateChartBindings(stack) {
2741
3073
  const objects = asArray10(stack.objects);
2742
3074
  for (let oi = 0; oi < objects.length; oi++) {
2743
3075
  const obj = objects[oi];
2744
- if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2745
- const objName = strName7(obj.name) ?? `#${oi}`;
3076
+ if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
3077
+ const objName = strName8(obj.name) ?? `#${oi}`;
2746
3078
  for (const [key, lv] of Object.entries(obj.listViews)) {
2747
3079
  checkListChart(
2748
3080
  lv,
@@ -2754,19 +3086,19 @@ function validateChartBindings(stack) {
2754
3086
  const pages = asArray10(stack.pages);
2755
3087
  for (let pi = 0; pi < pages.length; pi++) {
2756
3088
  const page = pages[pi];
2757
- if (!isRec6(page)) continue;
2758
- const pageName = strName7(page.name) ?? `#${pi}`;
3089
+ if (!isRec7(page)) continue;
3090
+ const pageName = strName8(page.name) ?? `#${pi}`;
2759
3091
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2760
- const props = isRec6(component.properties) ? component.properties : void 0;
2761
- if (!props || !strName7(props.dataset)) continue;
2762
- const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
2763
- const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
3092
+ const props = isRec7(component.properties) ? component.properties : void 0;
3093
+ if (!props || !strName8(props.dataset)) continue;
3094
+ const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
3095
+ const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
2764
3096
  check({
2765
- dataset: strName7(props.dataset),
3097
+ dataset: strName8(props.dataset),
2766
3098
  dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
2767
3099
  values: { names: strList2(props.values), path: `${path}.properties.values` },
2768
3100
  series: [...axisRefs, ...seriesRefs],
2769
- where: `page "${pageName}" \xB7 ${strName7(component.type) ?? "chart"}`,
3101
+ where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
2770
3102
  path: `${path}.properties`
2771
3103
  });
2772
3104
  }
@@ -2833,7 +3165,7 @@ function asArray12(v) {
2833
3165
  }
2834
3166
  return [];
2835
3167
  }
2836
- function strName8(v) {
3168
+ function strName9(v) {
2837
3169
  return typeof v === "string" && v.length > 0 ? v : void 0;
2838
3170
  }
2839
3171
  function collectNavExposures(stack) {
@@ -2842,18 +3174,18 @@ function collectNavExposures(stack) {
2842
3174
  for (let ai = 0; ai < apps.length; ai++) {
2843
3175
  const app = apps[ai];
2844
3176
  if (!app || typeof app !== "object") continue;
2845
- const appName = strName8(app.name) ?? `#${ai}`;
3177
+ const appName = strName9(app.name) ?? `#${ai}`;
2846
3178
  const walk = (items, basePath) => {
2847
3179
  const navItems = asArray12(items);
2848
3180
  for (let ni = 0; ni < navItems.length; ni++) {
2849
3181
  const nav = navItems[ni];
2850
3182
  if (!nav || typeof nav !== "object") continue;
2851
3183
  const navPath = `${basePath}[${ni}]`;
2852
- const objectName = strName8(nav.objectName);
3184
+ const objectName = strName9(nav.objectName);
2853
3185
  if (nav.type === "object" && objectName) {
2854
3186
  out.push({
2855
3187
  objectName,
2856
- where: `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
3188
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
2857
3189
  path: `${navPath}.objectName`
2858
3190
  });
2859
3191
  }
@@ -2877,7 +3209,7 @@ function validateNavAccess(stack) {
2877
3209
  if (exposures.length === 0) return findings;
2878
3210
  const ownObjects = /* @__PURE__ */ new Set();
2879
3211
  for (const obj of asArray12(stack.objects)) {
2880
- const n = strName8(obj.name);
3212
+ const n = strName9(obj.name);
2881
3213
  if (n) ownObjects.add(n);
2882
3214
  }
2883
3215
  const readable = /* @__PURE__ */ new Set();
@@ -2907,13 +3239,13 @@ function validateNavAccess(stack) {
2907
3239
 
2908
3240
  // src/validate-nav-target-refs.ts
2909
3241
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2910
- var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3242
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2911
3243
  function asArray13(v) {
2912
- if (Array.isArray(v)) return v.filter(isRec7);
2913
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
3244
+ if (Array.isArray(v)) return v.filter(isRec8);
3245
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
2914
3246
  return [];
2915
3247
  }
2916
- function strName9(v) {
3248
+ function strName10(v) {
2917
3249
  return typeof v === "string" && v.length > 0 ? v : void 0;
2918
3250
  }
2919
3251
  var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
@@ -2925,14 +3257,14 @@ var NAV_TARGETS = [
2925
3257
  function namesOf(collection) {
2926
3258
  const out = /* @__PURE__ */ new Set();
2927
3259
  for (const entry of asArray13(collection)) {
2928
- const n = strName9(entry.name);
3260
+ const n = strName10(entry.name);
2929
3261
  if (n) out.add(n);
2930
3262
  }
2931
3263
  return out;
2932
3264
  }
2933
3265
  function validateNavTargetRefs(stack) {
2934
3266
  const findings = [];
2935
- if (!isRec7(stack)) return findings;
3267
+ if (!isRec8(stack)) return findings;
2936
3268
  const apps = asArray13(stack.apps);
2937
3269
  if (apps.length === 0) return findings;
2938
3270
  const declared = /* @__PURE__ */ new Map();
@@ -2940,16 +3272,16 @@ function validateNavTargetRefs(stack) {
2940
3272
  declared.set(collection, namesOf(stack[collection]));
2941
3273
  }
2942
3274
  for (const [ai, app] of apps.entries()) {
2943
- const appName = strName9(app.name) ?? `#${ai}`;
3275
+ const appName = strName10(app.name) ?? `#${ai}`;
2944
3276
  const walk = (items, basePath) => {
2945
3277
  if (!Array.isArray(items)) return;
2946
3278
  for (const [ni, raw] of items.entries()) {
2947
- if (!isRec7(raw)) continue;
3279
+ if (!isRec8(raw)) continue;
2948
3280
  const nav = raw;
2949
3281
  const navPath = `${basePath}[${ni}]`;
2950
3282
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
2951
3283
  if (nav.type !== type) continue;
2952
- const target = strName9(nav[prop]);
3284
+ const target = strName10(nav[prop]);
2953
3285
  if (!target || isInterpolated2(target)) continue;
2954
3286
  const known = declared.get(collection);
2955
3287
  if (known.has(target)) continue;
@@ -2957,7 +3289,7 @@ function validateNavTargetRefs(stack) {
2957
3289
  findings.push({
2958
3290
  severity: "warning",
2959
3291
  rule: NAV_TARGET_UNRESOLVED,
2960
- where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
3292
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
2961
3293
  path: `${navPath}.${prop}`,
2962
3294
  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.`,
2963
3295
  hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
@@ -2976,44 +3308,44 @@ function validateNavTargetRefs(stack) {
2976
3308
  }
2977
3309
 
2978
3310
  // src/validate-nav-object-servability.ts
2979
- var import_data7 = require("@objectstack/spec/data");
3311
+ var import_data9 = require("@objectstack/spec/data");
2980
3312
  var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2981
- var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3313
+ var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2982
3314
  function asArray14(v) {
2983
- if (Array.isArray(v)) return v.filter(isRec8);
2984
- if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
3315
+ if (Array.isArray(v)) return v.filter(isRec9);
3316
+ if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
2985
3317
  return [];
2986
3318
  }
2987
- function strName10(v) {
3319
+ function strName11(v) {
2988
3320
  return typeof v === "string" && v.length > 0 ? v : void 0;
2989
3321
  }
2990
3322
  var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2991
3323
  function validateNavObjectServability(stack) {
2992
3324
  const findings = [];
2993
- if (!isRec8(stack)) return findings;
3325
+ if (!isRec9(stack)) return findings;
2994
3326
  const apps = asArray14(stack.apps);
2995
3327
  if (apps.length === 0) return findings;
2996
3328
  const ownEnable = /* @__PURE__ */ new Map();
2997
3329
  const objects = asArray14(stack.objects);
2998
3330
  for (const [oi, obj] of objects.entries()) {
2999
- const n = strName10(obj.name);
3331
+ const n = strName11(obj.name);
3000
3332
  if (!n) continue;
3001
3333
  ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
3002
3334
  }
3003
3335
  if (ownEnable.size === 0) return findings;
3004
3336
  for (const [ai, app] of apps.entries()) {
3005
- const appName = strName10(app.name) ?? `#${ai}`;
3337
+ const appName = strName11(app.name) ?? `#${ai}`;
3006
3338
  const walk = (items, basePath) => {
3007
3339
  if (!Array.isArray(items)) return;
3008
3340
  for (const [ni, raw] of items.entries()) {
3009
- if (!isRec8(raw)) continue;
3341
+ if (!isRec9(raw)) continue;
3010
3342
  const nav = raw;
3011
3343
  const navPath = `${basePath}[${ni}]`;
3012
3344
  if (nav.type === "object") {
3013
- const target = strName10(nav.objectName);
3345
+ const target = strName11(nav.objectName);
3014
3346
  const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
3015
- if (target && declared && !(0, import_data7.canServeApiOperation)(declared.enable, "list")) {
3016
- const enable = isRec8(declared.enable) ? declared.enable : {};
3347
+ if (target && declared && !(0, import_data9.canServeApiOperation)(declared.enable, "list")) {
3348
+ const enable = isRec9(declared.enable) ? declared.enable : {};
3017
3349
  const apiDisabled = enable.apiEnabled === false;
3018
3350
  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(", ")})` : "");
3019
3351
  const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
@@ -3021,7 +3353,7 @@ function validateNavObjectServability(stack) {
3021
3353
  findings.push({
3022
3354
  severity: "error",
3023
3355
  rule: NAV_OBJECT_UNSERVABLE,
3024
- where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3356
+ where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
3025
3357
  // The nav entry is where the dead row is authored; the `enable`
3026
3358
  // key that condemns it is named in the message, because the fix
3027
3359
  // may belong at either end.
@@ -3048,27 +3380,27 @@ var import_spec = require("@objectstack/spec");
3048
3380
  var import_system4 = require("@objectstack/spec/system");
3049
3381
 
3050
3382
  // src/view-walk.ts
3051
- function isRec9(v) {
3383
+ function isRec10(v) {
3052
3384
  return !!v && typeof v === "object" && !Array.isArray(v);
3053
3385
  }
3054
- function strName11(v) {
3386
+ function strName12(v) {
3055
3387
  return typeof v === "string" && v.length > 0 ? v : void 0;
3056
3388
  }
3057
3389
  function viewObjectName(view) {
3058
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
3390
+ return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
3059
3391
  }
3060
3392
  function viewContainerSites(view, basePath) {
3061
- if (!isRec9(view)) return [];
3393
+ if (!isRec10(view)) return [];
3062
3394
  const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3063
- if (isRec9(view.form)) {
3395
+ if (isRec10(view.form)) {
3064
3396
  sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3065
3397
  }
3066
3398
  for (const key of ["listViews", "formViews"]) {
3067
3399
  const container = view[key];
3068
- if (!isRec9(container)) continue;
3400
+ if (!isRec10(container)) continue;
3069
3401
  const kind = key === "listViews" ? "listView" : "formView";
3070
3402
  for (const [subKey, sub] of Object.entries(container)) {
3071
- if (!isRec9(sub)) continue;
3403
+ if (!isRec10(sub)) continue;
3072
3404
  sites.push({
3073
3405
  view: sub,
3074
3406
  path: `${basePath}.${key}.${subKey}`,
@@ -3086,18 +3418,18 @@ function formViewSites(view, basePath) {
3086
3418
  // src/validate-translation-references.ts
3087
3419
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
3088
3420
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
3089
- function isRec10(v) {
3421
+ function isRec11(v) {
3090
3422
  return !!v && typeof v === "object" && !Array.isArray(v);
3091
3423
  }
3092
3424
  function asArray15(v) {
3093
3425
  if (Array.isArray(v)) return v;
3094
- if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
3426
+ if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
3095
3427
  return [];
3096
3428
  }
3097
- function strName12(v) {
3429
+ function strName13(v) {
3098
3430
  return typeof v === "string" && v.length > 0 ? v : void 0;
3099
3431
  }
3100
- function distance5(a, b) {
3432
+ function distance6(a, b) {
3101
3433
  const m = a.length;
3102
3434
  const n = b.length;
3103
3435
  if (m === 0) return n;
@@ -3113,7 +3445,7 @@ function distance5(a, b) {
3113
3445
  }
3114
3446
  return prev[n];
3115
3447
  }
3116
- function suggest6(target, known) {
3448
+ function suggest7(target, known) {
3117
3449
  const names = [...known];
3118
3450
  const segmentMatch = names.find(
3119
3451
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -3122,7 +3454,7 @@ function suggest6(target, known) {
3122
3454
  let best;
3123
3455
  let bestScore = Infinity;
3124
3456
  for (const candidate of names) {
3125
- const d = distance5(target, candidate);
3457
+ const d = distance6(target, candidate);
3126
3458
  if (d < bestScore) {
3127
3459
  bestScore = d;
3128
3460
  best = candidate;
@@ -3155,33 +3487,33 @@ function collectViewRecord(view, factsFor) {
3155
3487
  const addSections = (container, binding) => {
3156
3488
  if (!binding) return;
3157
3489
  for (const section of asArray15(container.sections)) {
3158
- const sectionName = strName12(section.name);
3490
+ const sectionName = strName13(section.name);
3159
3491
  if (sectionName) factsFor(binding).sections.add(sectionName);
3160
3492
  }
3161
3493
  };
3162
- const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
3163
- if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3164
- addView(recordObject ?? listBinding, strName12(view.name));
3494
+ const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
3495
+ if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3496
+ addView(recordObject ?? listBinding, strName13(view.name));
3165
3497
  const named = namedViewKeys(view);
3166
3498
  for (const family of ["listViews", "formViews"]) {
3167
3499
  const container = view[family];
3168
- if (!isRec10(container)) continue;
3500
+ if (!isRec11(container)) continue;
3169
3501
  const registryKeys = family === "listViews" ? named.list : named.form;
3170
3502
  let at = 0;
3171
3503
  for (const sub of Object.values(container)) {
3172
3504
  if (!sub || typeof sub !== "object") continue;
3173
3505
  const registryKey = registryKeys[at++];
3174
- if (!isRec10(sub)) continue;
3506
+ if (!isRec11(sub)) continue;
3175
3507
  const binding = bindingOf(sub) ?? listBinding;
3176
3508
  addView(binding, registryKey);
3177
3509
  addSections(sub, binding);
3178
3510
  }
3179
3511
  }
3180
- if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3512
+ if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3181
3513
  addSections(view, recordObject ?? listBinding);
3182
3514
  }
3183
3515
  function defaultListViewKey(object, container) {
3184
- if (!object || !isRec10(container.list)) return void 0;
3516
+ if (!object || !isRec11(container.list)) return void 0;
3185
3517
  const item = (0, import_spec.expandViewContainer)(object, container).find(
3186
3518
  (i) => i.viewKind === "list" && i.isDefault
3187
3519
  );
@@ -3193,7 +3525,7 @@ function namedViewKeys(container) {
3193
3525
  const object = "probe";
3194
3526
  const prefix = `${object}.`;
3195
3527
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
3196
- const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3528
+ const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3197
3529
  const listCount = countEntries(container.listViews);
3198
3530
  const formCount = countEntries(container.formViews);
3199
3531
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -3211,14 +3543,14 @@ function readOptions(field) {
3211
3543
  values.add(opt);
3212
3544
  continue;
3213
3545
  }
3214
- if (!isRec10(opt)) continue;
3215
- const value = strName12(opt.value);
3546
+ if (!isRec11(opt)) continue;
3547
+ const value = strName13(opt.value);
3216
3548
  if (!value) continue;
3217
3549
  values.add(value);
3218
- const label2 = strName12(opt.label);
3550
+ const label2 = strName13(opt.label);
3219
3551
  if (label2) byLabel.set(label2.toLowerCase(), value);
3220
3552
  }
3221
- } else if (isRec10(raw)) {
3553
+ } else if (isRec11(raw)) {
3222
3554
  for (const [value, label2] of Object.entries(raw)) {
3223
3555
  values.add(value);
3224
3556
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -3239,23 +3571,23 @@ function buildUniverse(stack) {
3239
3571
  return facts;
3240
3572
  };
3241
3573
  for (const obj of asArray15(stack.objects)) {
3242
- const objectName = strName12(obj.name);
3574
+ const objectName = strName13(obj.name);
3243
3575
  if (!objectName) continue;
3244
3576
  const facts = factsFor(objectName);
3245
3577
  for (const field of asArray15(obj.fields)) {
3246
- const fieldName = strName12(field.name);
3578
+ const fieldName = strName13(field.name);
3247
3579
  if (fieldName) facts.fields.set(fieldName, field);
3248
3580
  }
3249
3581
  for (const action of asArray15(obj.actions)) {
3250
- const actionName = strName12(action.name);
3582
+ const actionName = strName13(action.name);
3251
3583
  if (actionName) facts.actions.set(actionName, action);
3252
3584
  }
3253
3585
  for (const view of asArray15(obj.views)) {
3254
- collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
3586
+ collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
3255
3587
  }
3256
3588
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
3257
3589
  for (const group of asArray15(obj.fieldGroups)) {
3258
- const key = strName12(group.key) ?? strName12(group.name);
3590
+ const key = strName13(group.key) ?? strName13(group.name);
3259
3591
  if (key) facts.sections.add(key);
3260
3592
  }
3261
3593
  }
@@ -3266,10 +3598,10 @@ function buildUniverse(stack) {
3266
3598
  for (let pi = 0; pi < pages.length; pi++) {
3267
3599
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
3268
3600
  if (!walked.objectName) continue;
3269
- const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3601
+ const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
3270
3602
  if (!props) continue;
3271
3603
  for (const section of asArray15(props.sections)) {
3272
- const sectionName = strName12(section.name);
3604
+ const sectionName = strName13(section.name);
3273
3605
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
3274
3606
  }
3275
3607
  }
@@ -3277,9 +3609,9 @@ function buildUniverse(stack) {
3277
3609
  const globalActions = /* @__PURE__ */ new Map();
3278
3610
  const actionOwners = /* @__PURE__ */ new Map();
3279
3611
  for (const action of asArray15(stack.actions)) {
3280
- const actionName = strName12(action.name);
3612
+ const actionName = strName13(action.name);
3281
3613
  if (!actionName) continue;
3282
- const owner = strName12(action.objectName) ?? strName12(action.object);
3614
+ const owner = strName13(action.objectName) ?? strName13(action.object);
3283
3615
  if (owner) {
3284
3616
  factsFor(owner).actions.set(actionName, action);
3285
3617
  actionOwners.set(actionName, owner);
@@ -3294,19 +3626,19 @@ function buildUniverse(stack) {
3294
3626
  }
3295
3627
  const apps = /* @__PURE__ */ new Map();
3296
3628
  for (const app of asArray15(stack.apps)) {
3297
- const appName = strName12(app.name);
3629
+ const appName = strName13(app.name);
3298
3630
  if (!appName) continue;
3299
3631
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
3300
3632
  const walkNav = (items) => {
3301
3633
  for (const item of asArray15(items)) {
3302
- const id = strName12(item.id);
3634
+ const id = strName13(item.id);
3303
3635
  if (id) navIds.add(id);
3304
3636
  if (item.children) walkNav(item.children);
3305
3637
  }
3306
3638
  };
3307
3639
  walkNav(app.navigation);
3308
3640
  for (const area of asArray15(app.areas)) {
3309
- const areaId = strName12(area.id);
3641
+ const areaId = strName13(area.id);
3310
3642
  if (areaId) navIds.add(areaId);
3311
3643
  walkNav(area.navigation);
3312
3644
  }
@@ -3314,20 +3646,20 @@ function buildUniverse(stack) {
3314
3646
  }
3315
3647
  const dashboards = /* @__PURE__ */ new Map();
3316
3648
  for (const dash of asArray15(stack.dashboards)) {
3317
- const dashName = strName12(dash.name);
3649
+ const dashName = strName13(dash.name);
3318
3650
  if (!dashName) continue;
3319
3651
  const widgets = /* @__PURE__ */ new Set();
3320
3652
  for (const widget of asArray15(dash.widgets)) {
3321
- const id = strName12(widget.id) ?? strName12(widget.name);
3653
+ const id = strName13(widget.id) ?? strName13(widget.name);
3322
3654
  if (id) widgets.add(id);
3323
3655
  }
3324
3656
  const actions = /* @__PURE__ */ new Set();
3325
3657
  const headerActions = [
3326
- ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3658
+ ...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
3327
3659
  ...asArray15(dash.actions)
3328
3660
  ];
3329
3661
  for (const action of headerActions) {
3330
- const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
3662
+ const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
3331
3663
  if (key) actions.add(key);
3332
3664
  }
3333
3665
  dashboards.set(dashName, { widgets, actions });
@@ -3339,7 +3671,7 @@ function localePath(bundleIndex, locale) {
3339
3671
  }
3340
3672
  function validateTranslationReferences(stack) {
3341
3673
  const findings = [];
3342
- if (!isRec10(stack)) return findings;
3674
+ if (!isRec11(stack)) return findings;
3343
3675
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3344
3676
  if (bundles.length === 0) return findings;
3345
3677
  const universe = buildUniverse(stack);
@@ -3348,13 +3680,13 @@ function validateTranslationReferences(stack) {
3348
3680
  };
3349
3681
  for (let bi = 0; bi < bundles.length; bi++) {
3350
3682
  const bundle = bundles[bi];
3351
- if (!isRec10(bundle)) continue;
3683
+ if (!isRec11(bundle)) continue;
3352
3684
  for (const [locale, rawData] of Object.entries(bundle)) {
3353
- if (!isRec10(rawData)) continue;
3685
+ if (!isRec11(rawData)) continue;
3354
3686
  const base = localePath(bi, locale);
3355
3687
  const inLocale = `locale "${locale}"`;
3356
3688
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
3357
- if (!isRec10(rawNode)) continue;
3689
+ if (!isRec11(rawNode)) continue;
3358
3690
  const objPath = `${base}.objects.${objectName}`;
3359
3691
  const facts = universe.objects.get(objectName);
3360
3692
  if (!facts) {
@@ -3362,7 +3694,7 @@ function validateTranslationReferences(stack) {
3362
3694
  orphan(
3363
3695
  `${inLocale} \xB7 object "${objectName}"`,
3364
3696
  objPath,
3365
- (0, import_system4.hasPlatformObjectPrefix)(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()),
3697
+ (0, import_system4.hasPlatformObjectPrefix)(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()),
3366
3698
  `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())}.` : "")
3367
3699
  );
3368
3700
  continue;
@@ -3375,12 +3707,12 @@ function validateTranslationReferences(stack) {
3375
3707
  orphan(
3376
3708
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
3377
3709
  fieldPath,
3378
- `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()),
3710
+ `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()),
3379
3711
  `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())}.` : "")
3380
3712
  );
3381
3713
  continue;
3382
3714
  }
3383
- if (!isRec10(rawField)) continue;
3715
+ if (!isRec11(rawField)) continue;
3384
3716
  checkOptionKeys(findings, {
3385
3717
  optionMap: rawField.options,
3386
3718
  field,
@@ -3395,7 +3727,7 @@ function validateTranslationReferences(stack) {
3395
3727
  orphan(
3396
3728
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
3397
3729
  `${objPath}._views.${viewName}`,
3398
- `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),
3730
+ `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),
3399
3731
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
3400
3732
  );
3401
3733
  }
@@ -3404,7 +3736,7 @@ function validateTranslationReferences(stack) {
3404
3736
  orphan(
3405
3737
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
3406
3738
  `${objPath}._sections.${sectionName}`,
3407
- `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),
3739
+ `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),
3408
3740
  `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.`)
3409
3741
  );
3410
3742
  }
@@ -3415,7 +3747,7 @@ function validateTranslationReferences(stack) {
3415
3747
  orphan(
3416
3748
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
3417
3749
  actionPath,
3418
- `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()),
3750
+ `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()),
3419
3751
  `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())}.` : "")
3420
3752
  );
3421
3753
  continue;
@@ -3437,7 +3769,7 @@ function validateTranslationReferences(stack) {
3437
3769
  orphan(
3438
3770
  `${inLocale} \xB7 global action "${actionName}"`,
3439
3771
  actionPath,
3440
- 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()),
3772
+ 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()),
3441
3773
  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())}.` : "")
3442
3774
  );
3443
3775
  continue;
@@ -3457,18 +3789,18 @@ function validateTranslationReferences(stack) {
3457
3789
  orphan(
3458
3790
  `${inLocale} \xB7 app "${appName}"`,
3459
3791
  appPath,
3460
- `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()),
3792
+ `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()),
3461
3793
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
3462
3794
  );
3463
3795
  continue;
3464
3796
  }
3465
- if (!isRec10(rawApp)) continue;
3797
+ if (!isRec11(rawApp)) continue;
3466
3798
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
3467
3799
  if (navIds.has(navId)) continue;
3468
3800
  orphan(
3469
3801
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
3470
3802
  `${appPath}.navigation.${navId}`,
3471
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
3803
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
3472
3804
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
3473
3805
  );
3474
3806
  }
@@ -3480,18 +3812,18 @@ function validateTranslationReferences(stack) {
3480
3812
  orphan(
3481
3813
  `${inLocale} \xB7 dashboard "${dashName}"`,
3482
3814
  dashPath,
3483
- `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()),
3815
+ `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()),
3484
3816
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
3485
3817
  );
3486
3818
  continue;
3487
3819
  }
3488
- if (!isRec10(rawDash)) continue;
3820
+ if (!isRec11(rawDash)) continue;
3489
3821
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
3490
3822
  if (dash.widgets.has(widgetId)) continue;
3491
3823
  orphan(
3492
3824
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
3493
3825
  `${dashPath}.widgets.${widgetId}`,
3494
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
3826
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
3495
3827
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
3496
3828
  );
3497
3829
  }
@@ -3500,7 +3832,7 @@ function validateTranslationReferences(stack) {
3500
3832
  orphan(
3501
3833
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
3502
3834
  `${dashPath}.actions.${actionKey}`,
3503
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
3835
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
3504
3836
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
3505
3837
  );
3506
3838
  }
@@ -3510,7 +3842,7 @@ function validateTranslationReferences(stack) {
3510
3842
  return findings;
3511
3843
  }
3512
3844
  function asRecord(v) {
3513
- return isRec10(v) ? v : {};
3845
+ return isRec11(v) ? v : {};
3514
3846
  }
3515
3847
  function checkOptionKeys(findings, ctx) {
3516
3848
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3522,7 +3854,7 @@ function checkOptionKeys(findings, ctx) {
3522
3854
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3523
3855
  where: ctx.where,
3524
3856
  path: ctx.path,
3525
- 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.`,
3857
+ 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.`,
3526
3858
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3527
3859
  });
3528
3860
  return;
@@ -3535,17 +3867,17 @@ function checkOptionKeys(findings, ctx) {
3535
3867
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3536
3868
  where: ctx.where,
3537
3869
  path: `${ctx.path}.${key}`,
3538
- 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),
3870
+ 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),
3539
3871
  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)}.`
3540
3872
  });
3541
3873
  }
3542
3874
  }
3543
3875
  function checkActionParams(findings, ctx) {
3544
- const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3876
+ const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
3545
3877
  if (rawParams.length === 0) return;
3546
3878
  const declared = /* @__PURE__ */ new Set();
3547
3879
  for (const param of asArray15(ctx.action.params)) {
3548
- const name = strName12(param.name) ?? strName12(param.field);
3880
+ const name = strName13(param.name) ?? strName13(param.field);
3549
3881
  if (name) declared.add(name);
3550
3882
  }
3551
3883
  for (const paramName of rawParams) {
@@ -3555,40 +3887,40 @@ function checkActionParams(findings, ctx) {
3555
3887
  rule: TRANSLATION_TARGET_UNKNOWN,
3556
3888
  where: `${ctx.where} \xB7 param "${paramName}"`,
3557
3889
  path: `${ctx.path}.params.${paramName}`,
3558
- 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),
3890
+ 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),
3559
3891
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
3560
3892
  });
3561
3893
  }
3562
3894
  }
3563
3895
 
3564
3896
  // src/collection-entries.ts
3565
- function isRec11(v) {
3897
+ function isRec12(v) {
3566
3898
  return !!v && typeof v === "object" && !Array.isArray(v);
3567
3899
  }
3568
3900
  function collectionEntries(v, base) {
3569
3901
  if (Array.isArray(v)) {
3570
3902
  const out = [];
3571
3903
  for (let i = 0; i < v.length; i++) {
3572
- if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3904
+ if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3573
3905
  }
3574
3906
  return out;
3575
3907
  }
3576
- if (isRec11(v)) {
3577
- return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3908
+ if (isRec12(v)) {
3909
+ return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3578
3910
  }
3579
3911
  return [];
3580
3912
  }
3581
3913
 
3582
3914
  // src/validate-translatable-sections.ts
3583
3915
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3584
- function isRec12(v) {
3916
+ function isRec13(v) {
3585
3917
  return !!v && typeof v === "object" && !Array.isArray(v);
3586
3918
  }
3587
- function strName13(v) {
3919
+ function strName14(v) {
3588
3920
  return typeof v === "string" && v.length > 0 ? v : void 0;
3589
3921
  }
3590
3922
  function viewLabel(view) {
3591
- const name = strName13(view.name);
3923
+ const name = strName14(view.name);
3592
3924
  return name ? `view "${name}"` : "";
3593
3925
  }
3594
3926
  function joinWhere(...parts) {
@@ -3596,7 +3928,7 @@ function joinWhere(...parts) {
3596
3928
  }
3597
3929
  function collectViewSites(view, basePath, label2, sites) {
3598
3930
  const recordObject = viewObjectName(view);
3599
- const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3931
+ const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3600
3932
  for (const site of viewContainerSites(view, basePath)) {
3601
3933
  sites.push({
3602
3934
  path: `${site.path}.sections`,
@@ -3610,11 +3942,11 @@ function translatedObjectNames(stack) {
3610
3942
  const out = /* @__PURE__ */ new Set();
3611
3943
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3612
3944
  for (const bundle of bundles) {
3613
- if (!isRec12(bundle)) continue;
3945
+ if (!isRec13(bundle)) continue;
3614
3946
  for (const data of Object.values(bundle)) {
3615
- if (!isRec12(data) || !isRec12(data.objects)) continue;
3947
+ if (!isRec13(data) || !isRec13(data.objects)) continue;
3616
3948
  for (const [objectName, node] of Object.entries(data.objects)) {
3617
- if (isRec12(node)) out.add(objectName);
3949
+ if (isRec13(node)) out.add(objectName);
3618
3950
  }
3619
3951
  }
3620
3952
  }
@@ -3626,22 +3958,22 @@ function suggestedName(label2) {
3626
3958
  }
3627
3959
  function validateTranslatableSections(stack) {
3628
3960
  const findings = [];
3629
- if (!isRec12(stack)) return findings;
3961
+ if (!isRec13(stack)) return findings;
3630
3962
  const translated = translatedObjectNames(stack);
3631
3963
  if (translated.size === 0) return findings;
3632
3964
  const sites = [];
3633
3965
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3634
- const objectName = strName13(obj.name);
3966
+ const objectName = strName14(obj.name);
3635
3967
  if (!objectName) continue;
3636
3968
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3637
3969
  collectViewSites(
3638
- { ...view, object: strName13(view.object) ?? objectName },
3970
+ { ...view, object: strName14(view.object) ?? objectName },
3639
3971
  path,
3640
3972
  viewLabel(view),
3641
3973
  sites
3642
3974
  );
3643
3975
  }
3644
- if (isRec12(obj.listViews)) {
3976
+ if (isRec13(obj.listViews)) {
3645
3977
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3646
3978
  }
3647
3979
  }
@@ -3649,13 +3981,13 @@ function validateTranslatableSections(stack) {
3649
3981
  collectViewSites(view, path, viewLabel(view), sites);
3650
3982
  }
3651
3983
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3652
- const pageName = strName13(page.name);
3984
+ const pageName = strName14(page.name);
3653
3985
  const pageLabel = pageName ? `page "${pageName}"` : "";
3654
3986
  for (const walked of walkPageComponents(page, pagePath)) {
3655
3987
  if (!walked.objectName) continue;
3656
- const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
3988
+ const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
3657
3989
  if (!props) continue;
3658
- const type = strName13(walked.component.type) ?? "component";
3990
+ const type = strName14(walked.component.type) ?? "component";
3659
3991
  sites.push({
3660
3992
  path: `${walked.path}.properties.sections`,
3661
3993
  surface: joinWhere(pageLabel, type),
@@ -3670,9 +4002,9 @@ function validateTranslatableSections(stack) {
3670
4002
  if (!Array.isArray(site.sections)) continue;
3671
4003
  for (let i = 0; i < site.sections.length; i++) {
3672
4004
  const section = site.sections[i];
3673
- if (!isRec12(section)) continue;
3674
- if (strName13(section.name)) continue;
3675
- const heading = strName13(section.label);
4005
+ if (!isRec13(section)) continue;
4006
+ if (strName14(section.name)) continue;
4007
+ const heading = strName14(section.label);
3676
4008
  if (!heading) continue;
3677
4009
  const slug = suggestedName(heading);
3678
4010
  findings.push({
@@ -3690,10 +4022,10 @@ function validateTranslatableSections(stack) {
3690
4022
 
3691
4023
  // src/flow-walk.ts
3692
4024
  var import_automation2 = require("@objectstack/spec/automation");
3693
- function isRec13(v) {
4025
+ function isRec14(v) {
3694
4026
  return !!v && typeof v === "object" && !Array.isArray(v);
3695
4027
  }
3696
- function strName14(v) {
4028
+ function strName15(v) {
3697
4029
  return typeof v === "string" && v.length > 0 ? v : void 0;
3698
4030
  }
3699
4031
  var REGION_SLOTS = new Map(
@@ -3702,10 +4034,10 @@ var REGION_SLOTS = new Map(
3702
4034
  var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
3703
4035
  var MAX_REGION_DEPTH = 16;
3704
4036
  function flowNodeLabel(node, index) {
3705
- return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
4037
+ return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
3706
4038
  }
3707
4039
  function stripRegions(config) {
3708
- if (!isRec13(config)) return void 0;
4040
+ if (!isRec14(config)) return void 0;
3709
4041
  let out;
3710
4042
  for (const key of Object.keys(config)) {
3711
4043
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3716,11 +4048,11 @@ function stripRegions(config) {
3716
4048
  }
3717
4049
  function walkFlowNodes(flow, flowPath) {
3718
4050
  const out = [];
3719
- if (!isRec13(flow)) return out;
4051
+ if (!isRec14(flow)) return out;
3720
4052
  const visitList = (nodes, basePath, trail, depth) => {
3721
4053
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3722
4054
  nodes.forEach((raw, index) => {
3723
- if (!isRec13(raw)) return;
4055
+ if (!isRec14(raw)) return;
3724
4056
  const path = `${basePath}[${index}]`;
3725
4057
  out.push({
3726
4058
  node: raw,
@@ -3729,9 +4061,9 @@ function walkFlowNodes(flow, flowPath) {
3729
4061
  regionTrail: trail,
3730
4062
  depth
3731
4063
  });
3732
- const type = strName14(raw.type);
4064
+ const type = strName15(raw.type);
3733
4065
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3734
- if (!slots || !isRec13(raw.config)) return;
4066
+ if (!slots || !isRec14(raw.config)) return;
3735
4067
  const config = raw.config;
3736
4068
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3737
4069
  for (const slot of slots) {
@@ -3739,8 +4071,8 @@ function walkFlowNodes(flow, flowPath) {
3739
4071
  if (slot === "branches") {
3740
4072
  if (!Array.isArray(value)) continue;
3741
4073
  value.forEach((branch, b) => {
3742
- if (!isRec13(branch)) return;
3743
- const branchName = strName14(branch.name) ?? `#${b}`;
4074
+ if (!isRec14(branch)) return;
4075
+ const branchName = strName15(branch.name) ?? `#${b}`;
3744
4076
  visitList(
3745
4077
  branch.nodes,
3746
4078
  `${path}.config.branches[${b}].nodes`,
@@ -3750,7 +4082,7 @@ function walkFlowNodes(flow, flowPath) {
3750
4082
  });
3751
4083
  continue;
3752
4084
  }
3753
- if (!isRec13(value)) continue;
4085
+ if (!isRec14(value)) continue;
3754
4086
  visitList(
3755
4087
  value.nodes,
3756
4088
  `${path}.config.${slot}.nodes`,
@@ -3991,7 +4323,7 @@ function asArray17(v) {
3991
4323
  }
3992
4324
  return [];
3993
4325
  }
3994
- function strName15(v) {
4326
+ function strName16(v) {
3995
4327
  return typeof v === "string" && v.length > 0 ? v : void 0;
3996
4328
  }
3997
4329
  function surfaceOf(v) {
@@ -4002,17 +4334,17 @@ function validateAiSurfaceAffinity(stack) {
4002
4334
  if (!stack || typeof stack !== "object") return findings;
4003
4335
  const skillsByName = /* @__PURE__ */ new Map();
4004
4336
  for (const skill of asArray17(stack.skills)) {
4005
- const n = strName15(skill.name);
4337
+ const n = strName16(skill.name);
4006
4338
  if (n) skillsByName.set(n, skill);
4007
4339
  }
4008
4340
  const agents = asArray17(stack.agents);
4009
4341
  for (let ai = 0; ai < agents.length; ai++) {
4010
4342
  const agent = agents[ai];
4011
- const agentName = strName15(agent.name) ?? `#${ai}`;
4343
+ const agentName = strName16(agent.name) ?? `#${ai}`;
4012
4344
  const agentSurface = surfaceOf(agent.surface);
4013
4345
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
4014
4346
  for (let si = 0; si < skillRefs.length; si++) {
4015
- const ref = strName15(skillRefs[si]);
4347
+ const ref = strName16(skillRefs[si]);
4016
4348
  if (!ref) continue;
4017
4349
  const skill = skillsByName.get(ref);
4018
4350
  if (!skill) continue;
@@ -4041,10 +4373,10 @@ function asArray18(v) {
4041
4373
  }
4042
4374
  return [];
4043
4375
  }
4044
- function strName16(v) {
4376
+ function strName17(v) {
4045
4377
  return typeof v === "string" && v.length > 0 ? v : void 0;
4046
4378
  }
4047
- function distance6(a, b) {
4379
+ function distance7(a, b) {
4048
4380
  const m = a.length;
4049
4381
  const n = b.length;
4050
4382
  if (m === 0) return n;
@@ -4060,14 +4392,14 @@ function distance6(a, b) {
4060
4392
  }
4061
4393
  return prev[n];
4062
4394
  }
4063
- function suggest7(target, known) {
4395
+ function suggest8(target, known) {
4064
4396
  for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
4065
4397
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
4066
4398
  }
4067
4399
  let best;
4068
4400
  let bestScore = Infinity;
4069
4401
  for (const candidate of known) {
4070
- const d = distance6(target, candidate);
4402
+ const d = distance7(target, candidate);
4071
4403
  if (d < bestScore) {
4072
4404
  bestScore = d;
4073
4405
  best = candidate;
@@ -4082,8 +4414,8 @@ function materialisesAsTool(action) {
4082
4414
  if (!ai || typeof ai !== "object") return false;
4083
4415
  const aiRec = ai;
4084
4416
  if (aiRec.exposed !== true) return false;
4085
- if (!strName16(aiRec.description)) return false;
4086
- const type = strName16(action.type);
4417
+ if (!strName17(aiRec.description)) return false;
4418
+ const type = strName17(action.type);
4087
4419
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
4088
4420
  if (type === "script") return Boolean(action.target || action.body);
4089
4421
  return Boolean(action.target);
@@ -4091,12 +4423,12 @@ function materialisesAsTool(action) {
4091
4423
  function collectToolUniverse(stack) {
4092
4424
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
4093
4425
  for (const tool of asArray18(stack.tools)) {
4094
- const n = strName16(tool.name);
4426
+ const n = strName17(tool.name);
4095
4427
  if (n) universe.add(n);
4096
4428
  }
4097
4429
  const addActionFamily = (actions) => {
4098
4430
  for (const action of asArray18(actions)) {
4099
- const n = strName16(action.name);
4431
+ const n = strName17(action.name);
4100
4432
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
4101
4433
  }
4102
4434
  };
@@ -4110,7 +4442,7 @@ function collectUnexposedActionNames(stack) {
4110
4442
  const names = /* @__PURE__ */ new Set();
4111
4443
  const scan = (actions) => {
4112
4444
  for (const action of asArray18(actions)) {
4113
- const n = strName16(action.name);
4445
+ const n = strName17(action.name);
4114
4446
  if (n && !materialisesAsTool(action)) names.add(n);
4115
4447
  }
4116
4448
  };
@@ -4136,10 +4468,10 @@ function validateAiToolReferences(stack) {
4136
4468
  const skills = asArray18(stack.skills);
4137
4469
  for (let si = 0; si < skills.length; si++) {
4138
4470
  const skill = skills[si];
4139
- const skillName = strName16(skill.name) ?? `#${si}`;
4471
+ const skillName = strName17(skill.name) ?? `#${si}`;
4140
4472
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
4141
4473
  for (let ti = 0; ti < refs.length; ti++) {
4142
- const ref = strName16(refs[ti]);
4474
+ const ref = strName17(refs[ti]);
4143
4475
  if (!ref || resolves(ref)) continue;
4144
4476
  const isPattern = ref.endsWith("*");
4145
4477
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -4148,7 +4480,7 @@ function validateAiToolReferences(stack) {
4148
4480
  rule: AI_SKILL_TOOL_UNRESOLVED,
4149
4481
  where: `skill "${skillName}" \xB7 tools`,
4150
4482
  path: `skills[${si}].tools[${ti}]`,
4151
- 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),
4483
+ 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),
4152
4484
  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: ${import_system5.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
4153
4485
  });
4154
4486
  }
@@ -4166,7 +4498,7 @@ function asArray19(v) {
4166
4498
  }
4167
4499
  return [];
4168
4500
  }
4169
- function strName17(v) {
4501
+ function strName18(v) {
4170
4502
  return typeof v === "string" && v.length > 0 ? v : void 0;
4171
4503
  }
4172
4504
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -4176,7 +4508,7 @@ function validateAiAgentAuthoring(stack) {
4176
4508
  const agents = asArray19(stack.agents);
4177
4509
  for (let ai = 0; ai < agents.length; ai++) {
4178
4510
  const agent = agents[ai];
4179
- const name = strName17(agent.name) ?? `#${ai}`;
4511
+ const name = strName18(agent.name) ?? `#${ai}`;
4180
4512
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
4181
4513
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
4182
4514
  findings.push({
@@ -4192,9 +4524,9 @@ function validateAiAgentAuthoring(stack) {
4192
4524
  const apps = asArray19(stack.apps);
4193
4525
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
4194
4526
  const app = apps[appIdx];
4195
- const defaultAgent = strName17(app.defaultAgent);
4527
+ const defaultAgent = strName18(app.defaultAgent);
4196
4528
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
4197
- const appName = strName17(app.name) ?? `#${appIdx}`;
4529
+ const appName = strName18(app.name) ?? `#${appIdx}`;
4198
4530
  findings.push({
4199
4531
  severity: "warning",
4200
4532
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -4225,6 +4557,7 @@ function loadTypeScript() {
4225
4557
  return cachedTs;
4226
4558
  }
4227
4559
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
4560
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
4228
4561
  var HOOK_BODY_WRITE_PATTERNS = [
4229
4562
  {
4230
4563
  id: "input-property-assign",
@@ -4291,13 +4624,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
4291
4624
  "owner",
4292
4625
  "record_type"
4293
4626
  ]);
4294
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4627
+ function unprovisionedAnchorWriteConsequence() {
4628
+ 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).`;
4629
+ }
4630
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4295
4631
  function asArray20(v) {
4296
- if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4297
- if (isRec14(v)) {
4632
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4633
+ if (isRec15(v)) {
4298
4634
  return Object.entries(v).map(([name, def]) => ({
4299
4635
  name,
4300
- ...isRec14(def) ? def : {}
4636
+ ...isRec15(def) ? def : {}
4301
4637
  }));
4302
4638
  }
4303
4639
  return [];
@@ -4441,14 +4777,16 @@ function validateHookBodyWrites(stack) {
4441
4777
  const hooks = asArray20(stack.hooks);
4442
4778
  if (hooks.length === 0) return findings;
4443
4779
  let objectFields = null;
4780
+ let anchors = null;
4444
4781
  hooks.forEach((hook, hookIndex) => {
4445
4782
  const body = hook.body;
4446
- if (!isRec14(body) || body.language !== "js") return;
4783
+ if (!isRec15(body) || body.language !== "js") return;
4447
4784
  const source = body.source;
4448
4785
  if (typeof source !== "string" || source.trim() === "") return;
4449
4786
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4450
4787
  if (writes.length === 0) return;
4451
4788
  objectFields ?? (objectFields = indexObjectFields2(stack));
4789
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4452
4790
  const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4453
4791
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
4454
4792
  (o) => typeof o === "string" && o.trim() !== ""
@@ -4463,7 +4801,20 @@ function validateHookBodyWrites(stack) {
4463
4801
  if (reported.has(dedupeKey)) continue;
4464
4802
  if (w.object === void 0) {
4465
4803
  if (!inputJudgeable) continue;
4466
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
4804
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4805
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
4806
+ reported.add(dedupeKey);
4807
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
4808
+ findings.push({
4809
+ severity: "warning",
4810
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4811
+ where,
4812
+ path,
4813
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
4814
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
4815
+ });
4816
+ continue;
4817
+ }
4467
4818
  if (targetSets.some((s) => s.has(w.field))) continue;
4468
4819
  reported.add(dedupeKey);
4469
4820
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -4479,7 +4830,20 @@ function validateHookBodyWrites(stack) {
4479
4830
  } else {
4480
4831
  const known = judgeableFieldsOf(objectFields, w.object);
4481
4832
  if (!known) continue;
4482
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
4833
+ if (known.has(w.field)) continue;
4834
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4835
+ if (!anchors.get(w.object)?.has(w.field)) continue;
4836
+ reported.add(dedupeKey);
4837
+ findings.push({
4838
+ severity: "warning",
4839
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4840
+ where,
4841
+ path,
4842
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
4843
+ hint: unprovisionedAnchorHint(w.object, w.field)
4844
+ });
4845
+ continue;
4846
+ }
4483
4847
  reported.add(dedupeKey);
4484
4848
  findings.push({
4485
4849
  severity: "warning",
@@ -4508,19 +4872,20 @@ function fixHint(field, declared) {
4508
4872
  var import_shared2 = require("@objectstack/spec/shared");
4509
4873
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
4510
4874
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
4875
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
4511
4876
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
4512
4877
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
4513
4878
  var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
4514
4879
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
4515
4880
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
4516
4881
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
4517
- var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4882
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4518
4883
  function asArray21(v) {
4519
- if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4520
- if (isRec15(v)) {
4884
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4885
+ if (isRec16(v)) {
4521
4886
  return Object.entries(v).map(([name, def]) => ({
4522
4887
  name,
4523
- ...isRec15(def) ? def : {}
4888
+ ...isRec16(def) ? def : {}
4524
4889
  }));
4525
4890
  }
4526
4891
  return [];
@@ -4538,7 +4903,7 @@ function collectActionBodies(stack) {
4538
4903
  const type = typeof action.type === "string" ? action.type : "script";
4539
4904
  if (type !== "script") return;
4540
4905
  const body = action.body;
4541
- if (!isRec15(body) || body.language !== "js") return;
4906
+ if (!isRec16(body) || body.language !== "js") return;
4542
4907
  const source = body.source;
4543
4908
  if (typeof source !== "string" || source.trim() === "") return;
4544
4909
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4557,10 +4922,11 @@ function collectActionBodies(stack) {
4557
4922
  }
4558
4923
  function validateActionBodyWrites(stack) {
4559
4924
  const findings = [];
4560
- if (!isRec15(stack)) return findings;
4925
+ if (!isRec16(stack)) return findings;
4561
4926
  const sites = collectActionBodies(stack);
4562
4927
  if (sites.length === 0) return findings;
4563
4928
  let objectFields = null;
4929
+ let anchors = null;
4564
4930
  for (const site of sites) {
4565
4931
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
4566
4932
  const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
@@ -4585,6 +4951,7 @@ function validateActionBodyWrites(stack) {
4585
4951
  }
4586
4952
  if (writes.length === 0) continue;
4587
4953
  objectFields ?? (objectFields = indexObjectFields2(stack));
4954
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4588
4955
  const reported = /* @__PURE__ */ new Set();
4589
4956
  for (const w of writes) {
4590
4957
  if (w.object === void 0) continue;
@@ -4592,7 +4959,20 @@ function validateActionBodyWrites(stack) {
4592
4959
  if (reported.has(dedupeKey)) continue;
4593
4960
  const known = judgeableFieldsOf(objectFields, w.object);
4594
4961
  if (!known) continue;
4595
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
4962
+ if (known.has(w.field)) continue;
4963
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4964
+ if (!anchors.get(w.object)?.has(w.field)) continue;
4965
+ reported.add(dedupeKey);
4966
+ findings.push({
4967
+ severity: "warning",
4968
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
4969
+ where,
4970
+ path: site.path,
4971
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
4972
+ hint: unprovisionedAnchorHint(w.object, w.field)
4973
+ });
4974
+ continue;
4975
+ }
4596
4976
  reported.add(dedupeKey);
4597
4977
  findings.push({
4598
4978
  severity: "warning",
@@ -4614,14 +4994,15 @@ function fixHint2(field, declared) {
4614
4994
  // src/validate-flow-node-writes.ts
4615
4995
  var import_shared3 = require("@objectstack/spec/shared");
4616
4996
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4997
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
4617
4998
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4618
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4999
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4619
5000
  function asArray22(v) {
4620
- if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4621
- if (isRec16(v)) {
5001
+ if (Array.isArray(v)) return v.filter((x) => isRec17(x));
5002
+ if (isRec17(v)) {
4622
5003
  return Object.entries(v).map(([name, def]) => ({
4623
5004
  name,
4624
- ...isRec16(def) ? def : {}
5005
+ ...isRec17(def) ? def : {}
4625
5006
  }));
4626
5007
  }
4627
5008
  return [];
@@ -4634,30 +5015,44 @@ function readLiteralObjectName(config) {
4634
5015
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4635
5016
  function validateFlowNodeWrites(stack) {
4636
5017
  const findings = [];
4637
- if (!isRec16(stack)) return findings;
5018
+ if (!isRec17(stack)) return findings;
4638
5019
  const flows = asArray22(stack.flows);
4639
5020
  if (flows.length === 0) return findings;
4640
5021
  let objectFields = null;
5022
+ let anchors = null;
4641
5023
  flows.forEach((flow, flowIndex) => {
4642
5024
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
4643
5025
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4644
5026
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4645
5027
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4646
- const config = isRec16(node.config) ? node.config : void 0;
5028
+ const config = isRec17(node.config) ? node.config : void 0;
4647
5029
  if (!config) return;
4648
5030
  const fields = config.fields;
4649
- if (!isRec16(fields)) return;
5031
+ if (!isRec17(fields)) return;
4650
5032
  const written = Object.keys(fields);
4651
5033
  if (written.length === 0) return;
4652
5034
  const objectName = readLiteralObjectName(config);
4653
5035
  if (!objectName) return;
4654
5036
  objectFields ?? (objectFields = indexObjectFields2(stack));
5037
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4655
5038
  const known = judgeableFieldsOf(objectFields, objectName);
4656
5039
  if (!known) return;
4657
5040
  const nodeName = flowNodeLabel(node, walkIndex);
4658
5041
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
4659
5042
  for (const fieldName of written) {
4660
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
5043
+ if (known.has(fieldName)) continue;
5044
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
5045
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
5046
+ findings.push({
5047
+ severity: "warning",
5048
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
5049
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
5050
+ path: `${nodePath}.config.fields.${fieldName}`,
5051
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
5052
+ hint: unprovisionedAnchorHint(objectName, fieldName)
5053
+ });
5054
+ continue;
5055
+ }
4661
5056
  if (fieldName.includes(".")) continue;
4662
5057
  findings.push({
4663
5058
  severity: "error",
@@ -4770,15 +5165,15 @@ function validateReadonlyFlowWrites(stack) {
4770
5165
 
4771
5166
  // src/validate-react-page-props.ts
4772
5167
  var import_node_module2 = require("module");
4773
- var import_ui2 = require("@objectstack/spec/ui");
4774
- var import_data8 = require("@objectstack/spec/data");
5168
+ var import_ui3 = require("@objectstack/spec/ui");
5169
+ var import_data10 = require("@objectstack/spec/data");
4775
5170
 
4776
5171
  // src/zod-issue-format.ts
4777
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5172
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4778
5173
  var valueAtPath = (root, path) => {
4779
5174
  let cur = root;
4780
5175
  for (const key of path) {
4781
- if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
5176
+ if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
4782
5177
  cur = cur[key];
4783
5178
  }
4784
5179
  return cur;
@@ -4826,7 +5221,7 @@ function loadTypeScript2() {
4826
5221
  }
4827
5222
  var asArray24 = (v) => Array.isArray(v) ? v : [];
4828
5223
  var BLOCKS = new Map(
4829
- import_ui2.REACT_BLOCKS.map((b) => [
5224
+ import_ui3.REACT_BLOCKS.map((b) => [
4830
5225
  b.tag,
4831
5226
  {
4832
5227
  requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
@@ -4918,7 +5313,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4918
5313
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4919
5314
  function checkChartDrillDown(raw, push2) {
4920
5315
  if (raw === void 0 || raw === NOT_STATIC) return;
4921
- if (!isRec18(raw)) {
5316
+ if (!isRec19(raw)) {
4922
5317
  push2(
4923
5318
  "error",
4924
5319
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4927,7 +5322,7 @@ function checkChartDrillDown(raw, push2) {
4927
5322
  );
4928
5323
  return;
4929
5324
  }
4930
- const parsed = import_ui2.ChartDrillDownSchema.safeParse(raw);
5325
+ const parsed = import_ui3.ChartDrillDownSchema.safeParse(raw);
4931
5326
  if (parsed.success) return;
4932
5327
  for (const issue of parsed.error.issues) {
4933
5328
  const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
@@ -4941,7 +5336,7 @@ function checkChartDrillDown(raw, push2) {
4941
5336
  }
4942
5337
  function checkChartAggregate(raw, push2) {
4943
5338
  if (raw === void 0 || raw === NOT_STATIC) return;
4944
- if (!isRec18(raw)) {
5339
+ if (!isRec19(raw)) {
4945
5340
  push2(
4946
5341
  "error",
4947
5342
  REACT_CHART_AGGREGATE_INVALID,
@@ -4959,7 +5354,7 @@ function checkChartAggregate(raw, push2) {
4959
5354
  "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
4960
5355
  );
4961
5356
  }
4962
- const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
5357
+ const parsed = import_ui3.ChartAggregateSchema.safeParse(raw);
4963
5358
  if (parsed.success) return;
4964
5359
  for (const issue of parsed.error.issues) {
4965
5360
  if (groupByAbsent && issue.path[0] === "groupBy") continue;
@@ -4972,7 +5367,7 @@ function checkChartAggregate(raw, push2) {
4972
5367
  );
4973
5368
  }
4974
5369
  }
4975
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5370
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4976
5371
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4977
5372
  function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4978
5373
  const { values, where, path } = attrs;
@@ -4982,11 +5377,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
4982
5377
  const aggregate = values.get("aggregate");
4983
5378
  checkChartAggregate(aggregate, push2);
4984
5379
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4985
- if (!isRec18(aggregate)) return;
5380
+ if (!isRec19(aggregate)) return;
4986
5381
  const fn = strOf(aggregate.function);
4987
5382
  const field = strOf(aggregate.field);
4988
5383
  const groupBy = aggregate.groupBy;
4989
- const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
5384
+ const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
4990
5385
  const objectName = strOf(values.get("objectName"));
4991
5386
  const known = objectName ? objectFields.get(objectName) : void 0;
4992
5387
  if (objectName && known) {
@@ -5015,7 +5410,7 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
5015
5410
  fieldRef(field, "field");
5016
5411
  fieldRef(groupByField, "groupBy");
5017
5412
  }
5018
- const keys = (0, import_ui2.chartAggregateResultKeys)({ field, function: fn, groupBy });
5413
+ const keys = (0, import_ui3.chartAggregateResultKeys)({ field, function: fn, groupBy });
5019
5414
  const columns = [keys.category, keys.value].filter((k) => !!k);
5020
5415
  if (columns.length === 0) return;
5021
5416
  const axisRef = (name, prop) => {
@@ -5030,18 +5425,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
5030
5425
  );
5031
5426
  };
5032
5427
  const xAxisRaw = values.get("xAxis");
5033
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5428
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5034
5429
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
5035
5430
  axisRef(categoryAxis, categoryProp);
5036
5431
  const yAxisRaw = values.get("yAxis");
5037
5432
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
5038
5433
  for (const a of yAxisList) {
5039
- axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
5434
+ axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
5040
5435
  }
5041
5436
  const series = values.get("series");
5042
5437
  if (Array.isArray(series)) {
5043
5438
  for (const s of series) {
5044
- if (!isRec18(s)) continue;
5439
+ if (!isRec19(s)) continue;
5045
5440
  const dataKey = strOf(s.dataKey);
5046
5441
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
5047
5442
  }
@@ -5081,7 +5476,7 @@ var REACT_FIELD_SPECS = {
5081
5476
  };
5082
5477
  var PATH_SEP = " \u203A ";
5083
5478
  var SCHEMA_TYPE_BY_TAG = new Map(
5084
- import_ui2.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
5479
+ import_ui3.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
5085
5480
  );
5086
5481
  var FILTER_PROPS = new Set(
5087
5482
  Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
@@ -5097,7 +5492,7 @@ function subformFieldRefs(value, basePath) {
5097
5492
  if (!Array.isArray(value)) return { child, parent };
5098
5493
  for (let i = 0; i < value.length; i++) {
5099
5494
  const sub = value[i];
5100
- if (!isRec18(sub)) continue;
5495
+ if (!isRec19(sub)) continue;
5101
5496
  const at = (key) => `${basePath}[${i}].${key}`;
5102
5497
  child.push({
5103
5498
  objectName: strOf(sub.childObject),
@@ -5122,7 +5517,7 @@ function filterFieldRefs(node, basePath, out) {
5122
5517
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
5123
5518
  return;
5124
5519
  }
5125
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5520
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data10.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5126
5521
  out.push({ name: head, path: `${basePath}[0]` });
5127
5522
  }
5128
5523
  }
@@ -5142,20 +5537,20 @@ function reactFieldRefs(spec, values, basePath) {
5142
5537
  }
5143
5538
  for (const key of spec.nestedFields ?? []) {
5144
5539
  const v = readable(key);
5145
- if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5540
+ if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5146
5541
  }
5147
5542
  for (const key of spec.sections ?? []) {
5148
5543
  const v = readable(key);
5149
5544
  if (!Array.isArray(v)) continue;
5150
5545
  for (let i = 0; i < v.length; i++) {
5151
5546
  const section = v[i];
5152
- if (!isRec18(section)) continue;
5547
+ if (!isRec19(section)) continue;
5153
5548
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
5154
5549
  }
5155
5550
  }
5156
5551
  for (const key of spec.keyedByField ?? []) {
5157
5552
  const v = readable(key);
5158
- if (!isRec18(v)) continue;
5553
+ if (!isRec19(v)) continue;
5159
5554
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
5160
5555
  }
5161
5556
  for (const key of spec.filterArrays ?? []) {
@@ -5212,7 +5607,7 @@ function recordContextFinding(tag, schemaType, where, path) {
5212
5607
  where,
5213
5608
  path,
5214
5609
  message: `<${tag}> renders "${schemaType}", which reads its record from the record context a record page mounts \u2014 a kind:'react' page never mounts one, so the block renders empty no matter how it is bound (its objectName/recordId are not read by the renderer).`,
5215
- hint: `On a react page bind the record yourself: ${import_ui2.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
5610
+ hint: `On a react page bind the record yourself: ${import_ui3.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
5216
5611
  };
5217
5612
  }
5218
5613
  function localComponentNames(tsc, sf) {
@@ -5251,7 +5646,7 @@ function validateReactPageProps(stack) {
5251
5646
  const tag = node.tagName.getText(sf);
5252
5647
  const where = `page "${name}" \u203A <${tag}>`;
5253
5648
  const path = `pages[${p}].source`;
5254
- const recordType = import_ui2.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
5649
+ const recordType = import_ui3.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
5255
5650
  if (recordType && !locals.has(tag)) {
5256
5651
  findings.push(recordContextFinding(tag, recordType, where, path));
5257
5652
  tsc.forEachChild(node, visit);
@@ -5278,7 +5673,7 @@ function validateReactPageProps(stack) {
5278
5673
  }
5279
5674
  if (tag === "Block") {
5280
5675
  const blockType = strOf(values.get("type"));
5281
- if (blockType && (0, import_ui2.isRecordContextBlockType)(blockType)) {
5676
+ if (blockType && (0, import_ui3.isRecordContextBlockType)(blockType)) {
5282
5677
  findings.push(recordContextFinding(tag, blockType, where, path));
5283
5678
  tsc.forEachChild(node, visit);
5284
5679
  return;
@@ -5322,7 +5717,12 @@ function validateReactPageProps(stack) {
5322
5717
  searchTargets,
5323
5718
  where,
5324
5719
  `${path} \u203A searchableFields`,
5325
- "searchableFields"
5720
+ "searchableFields",
5721
+ // A `<ListView>` prop is a view-level narrowing — the checker's
5722
+ // default, spelled out here because the #8404 provenance index
5723
+ // follows it positionally.
5724
+ "narrowing",
5725
+ unprovisionedAnchors
5326
5726
  )
5327
5727
  );
5328
5728
  }
@@ -5344,6 +5744,13 @@ function validateReactPageProps(stack) {
5344
5744
  var REFERENCE_INTEGRITY_RULES = [
5345
5745
  { name: "validateObjectReferences", run: validateObjectReferences },
5346
5746
  { name: "validateSearchableFields", run: validateSearchableFields },
5747
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
5748
+ // name written in metadata, resolved against the object's declared fields. It
5749
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
5750
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
5751
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
5752
+ // the refusal is the whole view, on every load, traced to nothing.
5753
+ { name: "validateSortableFields", run: validateSortableFields },
5347
5754
  { name: "validateActionNameRefs", run: validateActionNameRefs },
5348
5755
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5349
5756
  { name: "validateChartBindings", run: validateChartBindings },
@@ -5458,14 +5865,14 @@ function validateReferenceIntegrity(stack) {
5458
5865
  }
5459
5866
 
5460
5867
  // src/validate-component-props.ts
5461
- var import_ui3 = require("@objectstack/spec/ui");
5868
+ var import_ui4 = require("@objectstack/spec/ui");
5462
5869
  var import_spec2 = require("@objectstack/spec");
5463
5870
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5464
5871
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
5465
- function isRec19(v) {
5872
+ function isRec20(v) {
5466
5873
  return !!v && typeof v === "object" && !Array.isArray(v);
5467
5874
  }
5468
- function strName18(v) {
5875
+ function strName19(v) {
5469
5876
  return typeof v === "string" && v.length > 0 ? v : void 0;
5470
5877
  }
5471
5878
  function asArray25(v) {
@@ -5475,12 +5882,12 @@ function asArray25(v) {
5475
5882
  }
5476
5883
  return [];
5477
5884
  }
5478
- var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
5885
+ var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
5479
5886
  var DATASOURCE_SUPPLIED_PROP = "object";
5480
5887
  function suppliedByDataSource(issue, component) {
5481
5888
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
5482
- const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
5483
- return strName18(dataSource?.object) !== void 0;
5889
+ const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
5890
+ return strName19(dataSource?.object) !== void 0;
5484
5891
  }
5485
5892
  function unrecognizedKeysFromUnionArm(issue) {
5486
5893
  if (issue.code !== "invalid_union") return void 0;
@@ -5497,18 +5904,18 @@ function unrecognizedKeysFromUnionArm(issue) {
5497
5904
  }
5498
5905
  function validateComponentProps(stack) {
5499
5906
  const findings = [];
5500
- if (!isRec19(stack)) return findings;
5907
+ if (!isRec20(stack)) return findings;
5501
5908
  const pages = asArray25(stack.pages);
5502
5909
  for (let pi = 0; pi < pages.length; pi++) {
5503
5910
  const page = pages[pi];
5504
- if (!isRec19(page)) continue;
5505
- const pageName = strName18(page.name) ?? `#${pi}`;
5911
+ if (!isRec20(page)) continue;
5912
+ const pageName = strName19(page.name) ?? `#${pi}`;
5506
5913
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5507
- const type = strName18(component.type);
5914
+ const type = strName19(component.type);
5508
5915
  if (!type) continue;
5509
5916
  const schema = PROPS_SCHEMAS[type];
5510
5917
  if (!schema) continue;
5511
- const props = isRec19(component.properties) ? component.properties : void 0;
5918
+ const props = isRec20(component.properties) ? component.properties : void 0;
5512
5919
  if (!props) continue;
5513
5920
  const where = `page "${pageName}" \xB7 ${type}`;
5514
5921
  const base = `${path}.properties`;
@@ -6504,7 +6911,7 @@ function validateApprovalApprovers(stack) {
6504
6911
  }
6505
6912
 
6506
6913
  // src/validate-record-title.ts
6507
- var import_data9 = require("@objectstack/spec/data");
6914
+ var import_data11 = require("@objectstack/spec/data");
6508
6915
  var TITLE_FORMAT_RETIRED = "title-format-retired";
6509
6916
  var TITLE_UNRESOLVABLE = "title-unresolvable";
6510
6917
  function asArray33(v) {
@@ -6532,7 +6939,7 @@ function validateRecordTitle(stack) {
6532
6939
  hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
6533
6940
  });
6534
6941
  }
6535
- const completeness = (0, import_data9.objectTitleCompleteness)(obj);
6942
+ const completeness = (0, import_data11.objectTitleCompleteness)(obj);
6536
6943
  if (completeness.status === "none") {
6537
6944
  findings.push({
6538
6945
  severity: "warning",
@@ -6679,10 +7086,10 @@ function asArray35(v) {
6679
7086
  }
6680
7087
  return [];
6681
7088
  }
6682
- function isRec20(v) {
7089
+ function isRec21(v) {
6683
7090
  return !!v && typeof v === "object" && !Array.isArray(v);
6684
7091
  }
6685
- function strName19(v) {
7092
+ function strName20(v) {
6686
7093
  return typeof v === "string" && v.length > 0 ? v : void 0;
6687
7094
  }
6688
7095
  function fieldNameOf(entry) {
@@ -6703,7 +7110,7 @@ function validateFormLayout(stack) {
6703
7110
  objectFields.set(name, new Set(fields));
6704
7111
  }
6705
7112
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6706
- const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
7113
+ const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
6707
7114
  const containerObject = viewObjectName(view);
6708
7115
  for (const site of formViewSites(view, viewPath)) {
6709
7116
  const objName = viewObjectName(site.view) ?? containerObject;
@@ -6713,7 +7120,7 @@ function validateFormLayout(stack) {
6713
7120
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6714
7121
  for (let s = 0; s < sections.length; s++) {
6715
7122
  const sec = sections[s];
6716
- const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
7123
+ const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6717
7124
  for (let f = 0; f < secFields.length; f++) {
6718
7125
  const entry = secFields[f];
6719
7126
  const fname = fieldNameOf(entry);
@@ -6728,7 +7135,7 @@ function validateFormLayout(stack) {
6728
7135
  hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6729
7136
  });
6730
7137
  }
6731
- const colSpan = isRec20(entry) ? entry.colSpan : void 0;
7138
+ const colSpan = isRec21(entry) ? entry.colSpan : void 0;
6732
7139
  if (colSpan != null) {
6733
7140
  findings.push({
6734
7141
  severity: "warning",
@@ -6886,12 +7293,12 @@ function bareRhsOnlyIdentifiers(ast) {
6886
7293
  for (const name of elsewhere) rhs.delete(name);
6887
7294
  return rhs;
6888
7295
  }
6889
- function isRec21(v) {
7296
+ function isRec22(v) {
6890
7297
  return !!v && typeof v === "object" && !Array.isArray(v);
6891
7298
  }
6892
7299
  function schemaIdOf(view) {
6893
7300
  const data = view.data;
6894
- if (!isRec21(data)) return void 0;
7301
+ if (!isRec22(data)) return void 0;
6895
7302
  if (data.provider !== "schema") return void 0;
6896
7303
  return typeof data.schemaId === "string" ? data.schemaId : void 0;
6897
7304
  }
@@ -7263,7 +7670,7 @@ function predicateSource2(v) {
7263
7670
  }
7264
7671
  return void 0;
7265
7672
  }
7266
- function isRec22(v) {
7673
+ function isRec23(v) {
7267
7674
  return !!v && typeof v === "object" && !Array.isArray(v);
7268
7675
  }
7269
7676
  function checkPredicate(source, scope, where, path, findings) {
@@ -7334,7 +7741,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
7334
7741
  if (!Array.isArray(entries) || depth > 12) return;
7335
7742
  for (let i = 0; i < entries.length; i++) {
7336
7743
  const entry = entries[i];
7337
- if (!isRec22(entry)) continue;
7744
+ if (!isRec23(entry)) continue;
7338
7745
  const path = `${base}[${i}]`;
7339
7746
  for (const key of PREDICATE_KEYS) {
7340
7747
  const source = predicateSource2(entry[key]);
@@ -7368,7 +7775,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
7368
7775
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7369
7776
  for (let s = 0; s < sections.length; s++) {
7370
7777
  const section = sections[s];
7371
- if (!isRec22(section)) continue;
7778
+ if (!isRec23(section)) continue;
7372
7779
  const sectionPath = `${site.path}.${bucket}[${s}]`;
7373
7780
  for (const key of PREDICATE_KEYS) {
7374
7781
  const source = predicateSource2(section[key]);
@@ -7652,6 +8059,7 @@ function validateSecurityPosture(stack, opts) {
7652
8059
  }
7653
8060
  }
7654
8061
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
8062
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
7655
8063
  const nowMs = opts?.nowMs ?? Date.now();
7656
8064
  for (const [i, seed] of asArray36(stack.data).entries()) {
7657
8065
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -7675,7 +8083,7 @@ function validateSecurityPosture(stack, opts) {
7675
8083
  }
7676
8084
  }
7677
8085
  const delegatedFrom = rec.delegated_from;
7678
- if (delegatedFrom != null && delegatedFrom !== "") {
8086
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
7679
8087
  const reason = rec.reason;
7680
8088
  if (typeof reason !== "string" || reason.trim().length === 0) {
7681
8089
  findings.push({
@@ -7841,6 +8249,8 @@ function validateOrgAxisRedLines(stack) {
7841
8249
  var import_formula6 = require("@objectstack/formula");
7842
8250
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
7843
8251
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
8252
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
8253
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
7844
8254
  function asArray38(v) {
7845
8255
  if (Array.isArray(v)) return v;
7846
8256
  if (v && typeof v === "object") {
@@ -7865,10 +8275,67 @@ function sourceOf(condition) {
7865
8275
  return str2(input?.source);
7866
8276
  }
7867
8277
  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).";
8278
+ function effectiveSharingModelOf(obj) {
8279
+ const m = obj.sharingModel;
8280
+ if (m === "private") return "private";
8281
+ if (m === "public_read") return "read";
8282
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
8283
+ if (m == null) {
8284
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
8285
+ return isSystem ? "public" : "private";
8286
+ }
8287
+ return "private";
8288
+ }
8289
+ function masterOf(obj) {
8290
+ for (const f of asArray38(obj.fields)) {
8291
+ if (f.type === "master_detail") {
8292
+ const ref = f.reference;
8293
+ if (typeof ref === "string" && ref) return ref;
8294
+ }
8295
+ }
8296
+ return void 0;
8297
+ }
8298
+ function anchorFindings(rule, index, objectsByName) {
8299
+ const object = str2(rule.object);
8300
+ if (!object) return [];
8301
+ const target = objectsByName.get(object);
8302
+ if (!target) return [];
8303
+ const name = str2(rule.name) || String(index);
8304
+ const where = `sharing rule "${name}" on object "${object}"`;
8305
+ const path = `sharingRules[${index}].object`;
8306
+ const owd = target.sharingModel;
8307
+ if (owd === "controlled_by_parent") {
8308
+ const master = masterOf(target);
8309
+ return [{
8310
+ severity: "error",
8311
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
8312
+ where,
8313
+ path,
8314
+ 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.`,
8315
+ 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.`
8316
+ }];
8317
+ }
8318
+ if (effectiveSharingModelOf(target) !== "public") return [];
8319
+ 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`;
8320
+ return [{
8321
+ severity: "error",
8322
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
8323
+ where,
8324
+ path,
8325
+ 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.`,
8326
+ 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.`
8327
+ }];
8328
+ }
7868
8329
  function validateSharingRuleEnforceability(stack) {
7869
8330
  const findings = [];
7870
8331
  const cfg = stack ?? {};
8332
+ const objectsByName = /* @__PURE__ */ new Map();
8333
+ for (const obj of asArray38(cfg.objects)) {
8334
+ const name = str2(obj.name);
8335
+ if (name) objectsByName.set(name, obj);
8336
+ }
7871
8337
  asArray38(cfg.sharingRules).forEach((rule, index) => {
8338
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
7872
8339
  const input = toCompilerInput(rule.condition);
7873
8340
  if (input === null) return;
7874
8341
  const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
@@ -7994,11 +8461,11 @@ var import_meta4 = {};
7994
8461
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
7995
8462
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
7996
8463
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
7997
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8464
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7998
8465
  function asArray40(v) {
7999
- if (Array.isArray(v)) return v.filter(isRec23);
8000
- if (isRec23(v)) {
8001
- return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
8466
+ if (Array.isArray(v)) return v.filter(isRec24);
8467
+ if (isRec24(v)) {
8468
+ return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
8002
8469
  }
8003
8470
  return [];
8004
8471
  }
@@ -8015,7 +8482,7 @@ function loadAjv() {
8015
8482
  `@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.`
8016
8483
  );
8017
8484
  }
8018
- const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
8485
+ const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
8019
8486
  cachedAjv = ctor;
8020
8487
  return ctor;
8021
8488
  }
@@ -8030,7 +8497,7 @@ function loadAddFormats() {
8030
8497
  `@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.`
8031
8498
  );
8032
8499
  }
8033
- const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
8500
+ const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
8034
8501
  cachedAddFormats = plugin;
8035
8502
  return plugin;
8036
8503
  }
@@ -8060,13 +8527,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
8060
8527
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
8061
8528
  for (const branch of ["then", "otherwise"]) {
8062
8529
  const nested = rule[branch];
8063
- if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8530
+ if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8064
8531
  }
8065
8532
  return out;
8066
8533
  }
8067
8534
  function walkObjectValidationRules(stack) {
8068
8535
  const walked = [];
8069
- if (!isRec23(stack)) return walked;
8536
+ if (!isRec24(stack)) return walked;
8070
8537
  for (const obj of asArray40(stack.objects)) {
8071
8538
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8072
8539
  const validations = obj.validations;
@@ -8101,7 +8568,7 @@ function validateRuleCompilability(stack) {
8101
8568
  });
8102
8569
  }
8103
8570
  }
8104
- if (rule.type === "json_schema" && isRec23(rule.schema)) {
8571
+ if (rule.type === "json_schema" && isRec24(rule.schema)) {
8105
8572
  try {
8106
8573
  createRuntimeAjv().compile(rule.schema);
8107
8574
  } catch (err) {
@@ -8121,7 +8588,7 @@ function validateRuleCompilability(stack) {
8121
8588
 
8122
8589
  // src/validate-rule-schema-formats.ts
8123
8590
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
8124
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8591
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8125
8592
  var SUBSCHEMA_KEYS = [
8126
8593
  "additionalItems",
8127
8594
  "additionalProperties",
@@ -8145,7 +8612,7 @@ var SUBSCHEMA_MAP_KEYS = [
8145
8612
  var MAX_SCHEMA_WALK_DEPTH = 32;
8146
8613
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
8147
8614
  function collectFormatUses(schema, pointer, out, depth) {
8148
- if (!isRec24(schema)) return;
8615
+ if (!isRec25(schema)) return;
8149
8616
  if (typeof schema.format === "string") {
8150
8617
  out.push({ pointer: `${pointer}/format`, name: schema.format });
8151
8618
  }
@@ -8164,7 +8631,7 @@ function collectFormatUses(schema, pointer, out, depth) {
8164
8631
  }
8165
8632
  for (const key of SUBSCHEMA_MAP_KEYS) {
8166
8633
  const value = schema[key];
8167
- if (!isRec24(value)) continue;
8634
+ if (!isRec25(value)) continue;
8168
8635
  for (const [name, entry] of Object.entries(value)) {
8169
8636
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
8170
8637
  }
@@ -8172,13 +8639,13 @@ function collectFormatUses(schema, pointer, out, depth) {
8172
8639
  const items = schema.items;
8173
8640
  if (Array.isArray(items)) {
8174
8641
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
8175
- } else if (isRec24(items)) {
8642
+ } else if (isRec25(items)) {
8176
8643
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
8177
8644
  }
8178
8645
  const dependencies = schema.dependencies;
8179
- if (isRec24(dependencies)) {
8646
+ if (isRec25(dependencies)) {
8180
8647
  for (const [name, entry] of Object.entries(dependencies)) {
8181
- if (!isRec24(entry)) continue;
8648
+ if (!isRec25(entry)) continue;
8182
8649
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
8183
8650
  }
8184
8651
  }
@@ -8205,9 +8672,9 @@ function nearestRegisteredFormat(name, registered) {
8205
8672
  let best = null;
8206
8673
  let bestDistance = Number.POSITIVE_INFINITY;
8207
8674
  for (const candidate of [...registered].sort()) {
8208
- const distance7 = editDistance2(authored, candidate);
8209
- if (distance7 < bestDistance) {
8210
- bestDistance = distance7;
8675
+ const distance8 = editDistance2(authored, candidate);
8676
+ if (distance8 < bestDistance) {
8677
+ bestDistance = distance8;
8211
8678
  best = candidate;
8212
8679
  }
8213
8680
  }
@@ -8217,7 +8684,7 @@ function validateRuleSchemaFormats(stack) {
8217
8684
  const findings = [];
8218
8685
  const pending = [];
8219
8686
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
8220
- if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
8687
+ if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
8221
8688
  const uses = [];
8222
8689
  collectFormatUses(rule.schema, "", uses, 0);
8223
8690
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -8250,7 +8717,7 @@ function asArray41(v) {
8250
8717
  }
8251
8718
  return [];
8252
8719
  }
8253
- function strName20(v) {
8720
+ function strName21(v) {
8254
8721
  return typeof v === "string" && v.length > 0 ? v : void 0;
8255
8722
  }
8256
8723
  function strList3(v) {
@@ -8265,7 +8732,7 @@ function collectNamePlacedActions(stack) {
8265
8732
  for (const n of strList3(list3[key])) placed.add(n);
8266
8733
  }
8267
8734
  for (const def of asArray41(list3.bulkActionDefs)) {
8268
- const n = strName20(def?.name);
8735
+ const n = strName21(def?.name);
8269
8736
  if (n) placed.add(n);
8270
8737
  }
8271
8738
  };
@@ -8291,7 +8758,7 @@ function validateActionLocations(stack) {
8291
8758
  const check = (action, path) => {
8292
8759
  if (!action || typeof action !== "object") return;
8293
8760
  if ("locations" in action) return;
8294
- const name = strName20(action.name);
8761
+ const name = strName21(action.name);
8295
8762
  if (!name) return;
8296
8763
  if (namePlaced.has(name)) return;
8297
8764
  findings.push({
@@ -8317,7 +8784,7 @@ function validateActionLocations(stack) {
8317
8784
 
8318
8785
  // src/lint-flow-patterns.ts
8319
8786
  var import_automation5 = require("@objectstack/spec/automation");
8320
- var import_data10 = require("@objectstack/spec/data");
8787
+ var import_data12 = require("@objectstack/spec/data");
8321
8788
  function asArray42(v) {
8322
8789
  if (Array.isArray(v)) return v;
8323
8790
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -8592,7 +9059,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
8592
9059
  function filterCarriesNoCondition(filter) {
8593
9060
  if (filter === void 0 || filter === null) return true;
8594
9061
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
8595
- return (0, import_data10.reduceFilterVerdict)(filter) === "true";
9062
+ return (0, import_data12.reduceFilterVerdict)(filter) === "true";
8596
9063
  }
8597
9064
  function describeUnboundedFilter(filter) {
8598
9065
  if (filter === void 0 || filter === null) return "no `filter` key";
@@ -8959,7 +9426,7 @@ function lintLivenessProperties(stack) {
8959
9426
  }
8960
9427
 
8961
9428
  // src/lint-autonumber-formats.ts
8962
- var import_data11 = require("@objectstack/spec/data");
9429
+ var import_data13 = require("@objectstack/spec/data");
8963
9430
  var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
8964
9431
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
8965
9432
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
@@ -8985,8 +9452,8 @@ function lintAutonumberFormats(stack) {
8985
9452
  const name = typeof f.name === "string" ? f.name : "(unnamed field)";
8986
9453
  const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
8987
9454
  if (!fmt) continue;
8988
- const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
8989
- const refs = (0, import_data11.referencedFields)(tokens);
9455
+ const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
9456
+ const refs = (0, import_data13.referencedFields)(tokens);
8990
9457
  const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
8991
9458
  for (const t of tokens) {
8992
9459
  if (t.kind !== "literal") continue;
@@ -9141,6 +9608,17 @@ function lintViewRefs(stack) {
9141
9608
  }
9142
9609
 
9143
9610
  // src/data-model-rules.ts
9611
+ function objectWhere(obj) {
9612
+ return `object "${obj?.name}"`;
9613
+ }
9614
+ function indexWhere(obj, idx, j, cols) {
9615
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
9616
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
9617
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
9618
+ }
9619
+ function fieldWhere(obj, fieldName) {
9620
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
9621
+ }
9144
9622
  function fieldEntries2(fields) {
9145
9623
  if (!fields) return [];
9146
9624
  if (Array.isArray(fields)) {
@@ -9174,11 +9652,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9174
9652
  for (let j = 0; j < declaredIndexes.length; j++) {
9175
9653
  const idx = declaredIndexes[j];
9176
9654
  if (idx?.unique !== true) continue;
9177
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
9655
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
9656
+ const cols = colList.join(", ");
9178
9657
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9179
9658
  issues.push({
9180
9659
  severity: "warning",
9181
9660
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
9661
+ where: indexWhere(obj, idx, j, colList),
9182
9662
  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).`,
9183
9663
  path: `objects[${i}].indexes[${j}]`,
9184
9664
  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).`
@@ -9227,6 +9707,11 @@ function lintUniqueDeclarations(objects) {
9227
9707
  issues.push({
9228
9708
  severity: "warning",
9229
9709
  rule: UNIQUE_DOUBLE_DECLARATION,
9710
+ // More specific than `path` on purpose: this rule's finding is about ONE
9711
+ // column, but its `path` has always been the whole object (`objects[i]`)
9712
+ // because the defect straddles `fields.<name>.unique` and an entry of
9713
+ // `indexes`. `where` can name the column without picking one of the two.
9714
+ where: fieldWhere(obj, name),
9230
9715
  message,
9231
9716
  path: `objects[${i}]`,
9232
9717
  fix
@@ -9255,6 +9740,7 @@ function lintLegacyOrganizationComposites(objects) {
9255
9740
  issues.push({
9256
9741
  severity: "warning",
9257
9742
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
9743
+ where: indexWhere(obj, idx, j, cols),
9258
9744
  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.`,
9259
9745
  path: `objects[${i}].indexes[${j}]`,
9260
9746
  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.`
@@ -9271,7 +9757,7 @@ var CLI_ONLY = ["cli"];
9271
9757
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9272
9758
  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.";
9273
9759
  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.";
9274
- 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.";
9760
+ 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).";
9275
9761
  var EXPRESSION_INVALID = "expression-invalid";
9276
9762
  var AUTHORING_RULES = [
9277
9763
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -9332,8 +9818,14 @@ var AUTHORING_RULES = [
9332
9818
  input: "normalized",
9333
9819
  commands: ALL,
9334
9820
  source: "packages/lint/src/validate-functional-completeness.ts",
9335
- surfaces: CLI_ONLY,
9336
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9821
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
9822
+ // object rules cross together under the 2026-08-18 adjudication. The
9823
+ // false-positive budget the crossing owed was exempted on a measured
9824
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
9825
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
9826
+ // advisory-tier object rules deliberately do NOT ride.
9827
+ surfaces: CLI_AND_RUNTIME,
9828
+ runtimeTypes: ["object"],
9337
9829
  run: (stack) => validateFunctionalCompleteness(stack)
9338
9830
  },
9339
9831
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -9357,8 +9849,11 @@ var AUTHORING_RULES = [
9357
9849
  input: "normalized",
9358
9850
  commands: ALL,
9359
9851
  source: "packages/lint/src/validate-managed-api-methods.ts",
9360
- surfaces: CLI_ONLY,
9361
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9852
+ // Runtime publish gate (#4716): a managed object advertising a verb its
9853
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
9854
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
9855
+ surfaces: CLI_AND_RUNTIME,
9856
+ runtimeTypes: ["object"],
9362
9857
  run: (stack) => validateManagedApiMethods(stack)
9363
9858
  },
9364
9859
  // A view container in `views: []` that registers zero views: nothing appears
@@ -9380,19 +9875,38 @@ var AUTHORING_RULES = [
9380
9875
  },
9381
9876
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
9382
9877
  // chartConfig axis/series must resolve against the declared datasets.
9878
+ //
9879
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
9880
+ // widget bound to a dataset that resolves to nothing sailed through save AND
9881
+ // publish — this rule provably caught the body and was simply never invoked,
9882
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
9883
+ // hold a forward reference; publishing refuses it with the key path named.
9884
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
9885
+ // — without it every legitimate board reads as dangling, the 3-phantom
9886
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
9887
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
9888
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
9889
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
9890
+ // accepted knowingly rather than splitting the dataset limb into its own
9891
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
9892
+ // filter (registry machinery that weakens "delete a rule from the table and
9893
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
9894
+ // advisory channel and never block (#4463 P1).
9383
9895
  {
9384
9896
  name: "validateWidgetBindings",
9385
9897
  tier: "gating",
9386
9898
  input: "parsed",
9387
9899
  commands: ALL,
9388
9900
  source: "packages/lint/src/validate-widget-bindings.ts",
9389
- surfaces: CLI_ONLY,
9390
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9901
+ surfaces: CLI_AND_RUNTIME,
9902
+ runtimeTypes: ["dashboard"],
9391
9903
  run: (stack) => validateWidgetBindings(stack)
9392
9904
  },
9393
- // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
9394
- // target that resolves to no defined action ships a button that renders and
9395
- // silently does nothing on click. Unresolved `url` routes stay advisory.
9905
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
9906
+ // button that renders and refuses (or does nothing) on click: a `script`
9907
+ // target must name a defined action, a `modal` target must name a declared
9908
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
9909
+ // Unresolved `url` routes stay advisory.
9396
9910
  {
9397
9911
  name: "validateDashboardActionRefs",
9398
9912
  tier: "gating",
@@ -9417,6 +9931,28 @@ var AUTHORING_RULES = [
9417
9931
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9418
9932
  run: (stack) => validateFilterTokens(stack)
9419
9933
  },
9934
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
9935
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
9936
+ // no layer: the engine refuses it on a declared temporal field at query time
9937
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
9938
+ // literal string. This is the authoring-time refusal the ruling shipped
9939
+ // alongside the engine door, judging the filter literal in isolation —
9940
+ // ordering positions only, all three authored filter shapes. Like
9941
+ // `validateEmptyCombinators` it needs NO resolution context, so
9942
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
9943
+ // for every filter-carrying type the gate already maps: the write path is
9944
+ // the one door an AI author uses, and dashboards/views are where the preset
9945
+ // vocabulary is near enough to reach for.
9946
+ {
9947
+ name: "validatePresetComparands",
9948
+ tier: "gating",
9949
+ input: "parsed",
9950
+ commands: ALL,
9951
+ source: "packages/lint/src/validate-preset-comparands.ts",
9952
+ surfaces: CLI_AND_RUNTIME,
9953
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
9954
+ run: (stack) => validatePresetComparands(stack)
9955
+ },
9420
9956
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9421
9957
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9422
9958
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -9635,7 +10171,7 @@ var AUTHORING_RULES = [
9635
10171
  commands: ALL,
9636
10172
  source: "packages/lint/src/validate-record-title.ts",
9637
10173
  surfaces: CLI_ONLY,
9638
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10174
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9639
10175
  run: (stack) => validateRecordTitle(stack)
9640
10176
  },
9641
10177
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -9648,7 +10184,7 @@ var AUTHORING_RULES = [
9648
10184
  commands: ALL,
9649
10185
  source: "packages/lint/src/validate-semantic-roles.ts",
9650
10186
  surfaces: CLI_ONLY,
9651
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10187
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9652
10188
  run: (stack) => validateSemanticRoles(stack)
9653
10189
  },
9654
10190
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -9859,7 +10395,7 @@ var AUTHORING_RULES = [
9859
10395
  commands: ALL,
9860
10396
  source: "packages/lint/src/lint-liveness-properties.ts",
9861
10397
  surfaces: CLI_ONLY,
9862
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10398
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9863
10399
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
9864
10400
  severity: "warning",
9865
10401
  rule: f.rule,
@@ -9878,8 +10414,12 @@ var AUTHORING_RULES = [
9878
10414
  input: "parsed",
9879
10415
  commands: ALL,
9880
10416
  source: "packages/lint/src/lint-autonumber-formats.ts",
9881
- surfaces: CLI_ONLY,
9882
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10417
+ // Runtime publish gate (#4716): an autonumber referencing a field the
10418
+ // object does not carry is broken from the first record; only the error
10419
+ // arm blocks — the optional-field arm is `warning` and rides the
10420
+ // advisory channel like every other non-error finding (#4463 P1).
10421
+ surfaces: CLI_AND_RUNTIME,
10422
+ runtimeTypes: ["object"],
9883
10423
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
9884
10424
  severity: f.severity,
9885
10425
  rule: f.rule,
@@ -9920,12 +10460,12 @@ var AUTHORING_RULES = [
9920
10460
  commands: ["validate", "build"],
9921
10461
  source: "packages/lint/src/data-model-rules.ts",
9922
10462
  surfaces: CLI_ONLY,
9923
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10463
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9924
10464
  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.",
9925
10465
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9926
10466
  severity: f.severity === "suggestion" ? "info" : f.severity,
9927
10467
  rule: f.rule,
9928
- where: f.path,
10468
+ where: f.where,
9929
10469
  path: f.path,
9930
10470
  message: f.message,
9931
10471
  hint: f.fix ?? ""
@@ -9941,12 +10481,12 @@ var AUTHORING_RULES = [
9941
10481
  commands: ["validate", "build"],
9942
10482
  source: "packages/lint/src/data-model-rules.ts",
9943
10483
  surfaces: CLI_ONLY,
9944
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10484
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9945
10485
  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.",
9946
10486
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9947
10487
  severity: f.severity === "suggestion" ? "info" : f.severity,
9948
10488
  rule: f.rule,
9949
- where: f.path,
10489
+ where: f.where,
9950
10490
  path: f.path,
9951
10491
  message: f.message,
9952
10492
  hint: f.fix ?? ""
@@ -9963,12 +10503,12 @@ var AUTHORING_RULES = [
9963
10503
  commands: ["validate", "build"],
9964
10504
  source: "packages/lint/src/data-model-rules.ts",
9965
10505
  surfaces: CLI_ONLY,
9966
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10506
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9967
10507
  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.",
9968
10508
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9969
10509
  severity: f.severity === "suggestion" ? "info" : f.severity,
9970
10510
  rule: f.rule,
9971
- where: f.path,
10511
+ where: f.where,
9972
10512
  path: f.path,
9973
10513
  message: f.message,
9974
10514
  hint: f.fix ?? ""
@@ -10096,16 +10636,26 @@ var AUTHORING_RULES = [
10096
10636
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10097
10637
  run: (stack) => validateOrgAxisRedLines(stack)
10098
10638
  },
10099
- // #4698 — the "declared but never read" gate, for the one surface where the
10100
- // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
10101
- // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
10102
- // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
10103
- // that does not lower means the rule is SKIPPED at boot, so the grant is
10104
- // declared and does not exist. The lint calls that same compiler, from the
10105
- // same package, with the same options the verdict cannot drift from the
10106
- // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
10107
- // docblock states: the whole authorable surface is enforced, and this was the
10108
- // one field where that sentence was not yet true.
10639
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
10640
+ // sharing rule where the predicate is EXACT rather than inferred.
10641
+ //
10642
+ // - `condition` has a single runtime consumer
10643
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
10644
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
10645
+ // does not lower means the rule is SKIPPED at boot.
10646
+ // - `object` decides whether the grant is refused outright: reconcile hands
10647
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
10648
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
10649
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
10650
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
10651
+ // bypass set, federated anchors) are not, and are excluded by name.
10652
+ //
10653
+ // Either way the grant is declared and does not exist. The lint calls the
10654
+ // same compiler and mirrors the same verdict function, from the same inputs
10655
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
10656
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
10657
+ // surface is enforced, and these were the fields where that sentence was not
10658
+ // yet true.
10109
10659
  {
10110
10660
  name: "validateSharingRuleEnforceability",
10111
10661
  tier: "gating",
@@ -10113,7 +10663,7 @@ var AUTHORING_RULES = [
10113
10663
  commands: ALL,
10114
10664
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10115
10665
  surfaces: CLI_ONLY,
10116
- 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.",
10666
+ 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.",
10117
10667
  run: (stack) => validateSharingRuleEnforceability(stack)
10118
10668
  },
10119
10669
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10152,8 +10702,15 @@ var AUTHORING_RULES = [
10152
10702
  input: "parsed",
10153
10703
  commands: ALL,
10154
10704
  source: "packages/lint/src/validate-rule-compilability.ts",
10155
- surfaces: CLI_ONLY,
10156
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10705
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
10706
+ // judged snapshot actually carries a `json_schema` validation — so an
10707
+ // ordinary object write (no `json_schema` anywhere) still loads no
10708
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
10709
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
10710
+ // carrying such a rule) is the measured, adjudicated price of refusing a
10711
+ // validation rule that would otherwise ship compiled-by-nothing.
10712
+ surfaces: CLI_AND_RUNTIME,
10713
+ runtimeTypes: ["object"],
10157
10714
  run: (stack) => validateRuleCompilability(stack)
10158
10715
  },
10159
10716
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10174,8 +10731,12 @@ var AUTHORING_RULES = [
10174
10731
  input: "parsed",
10175
10732
  commands: ALL,
10176
10733
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10177
- surfaces: CLI_ONLY,
10178
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10734
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
10735
+ // the two judgements over one artifact stay on one side of the wall
10736
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
10737
+ // format set is only enumerated once a schema actually names a format.
10738
+ surfaces: CLI_AND_RUNTIME,
10739
+ runtimeTypes: ["object"],
10179
10740
  run: (stack) => validateRuleSchemaFormats(stack)
10180
10741
  }
10181
10742
  ];
@@ -10217,7 +10778,20 @@ var TYPE_TO_STACK_KEY = {
10217
10778
  permission: "permissions",
10218
10779
  book: "books"
10219
10780
  };
10220
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
10781
+ function narrowObjectsToPackageClosure(objects, scope) {
10782
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
10783
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
10784
+ return objects.filter((entry) => {
10785
+ if (!entry || typeof entry !== "object") return true;
10786
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
10787
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
10788
+ if (reachable.has(owner)) return true;
10789
+ return isSystemObject(entry);
10790
+ });
10791
+ }
10792
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
10793
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
10794
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
10221
10795
  function runtimeAuthoringRulesFor(type) {
10222
10796
  return AUTHORING_RULES.filter(
10223
10797
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10243,7 +10817,8 @@ function buildRuntimeWriteSnapshots(args) {
10243
10817
  const itemName = typeof item.name === "string" ? item.name : void 0;
10244
10818
  const baseline = {};
10245
10819
  for (const key of CONTEXT_STACK_KEYS) {
10246
- const collection = args.context?.[key] ?? [];
10820
+ const raw = args.context?.[key] ?? [];
10821
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
10247
10822
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10248
10823
  }
10249
10824
  const candidate = {
@@ -10277,7 +10852,8 @@ function runRuntimeAuthoringRules(args) {
10277
10852
  const snapshots = buildRuntimeWriteSnapshots({
10278
10853
  type: args.type,
10279
10854
  item: args.item,
10280
- ...args.context !== void 0 ? { context: args.context } : {}
10855
+ ...args.context !== void 0 ? { context: args.context } : {},
10856
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10281
10857
  });
10282
10858
  if (!snapshots) return empty;
10283
10859
  const ctx = { sduiManifest: args.sduiManifest };
@@ -10292,6 +10868,7 @@ function runRuntimeAuthoringRules(args) {
10292
10868
  // Annotate the CommonJS export names for ESM import in node:
10293
10869
  0 && (module.exports = {
10294
10870
  buildRuntimeWriteSnapshots,
10871
+ narrowObjectsToPackageClosure,
10295
10872
  runRuntimeAuthoringRules,
10296
10873
  runtimeAuthoringRulesFor,
10297
10874
  runtimeGatedTypes,