@objectstack/lint 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.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++) {
@@ -2033,6 +2190,26 @@ function validateSearchableFields(stack) {
2033
2190
  if (!isRec3(view)) continue;
2034
2191
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
2035
2192
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
2193
+ if (view.viewKind === "list" && !isRec3(view.config)) {
2194
+ check(
2195
+ view.searchableFields,
2196
+ listViewObject(view) ?? viewObject,
2197
+ `view "${viewLabel2}" (flattened list overlay)`,
2198
+ `views[${vi}].searchableFields`,
2199
+ "list-view searchableFields",
2200
+ "narrowing"
2201
+ );
2202
+ }
2203
+ if (view.viewKind === "list" && isRec3(view.config)) {
2204
+ check(
2205
+ view.config.searchableFields,
2206
+ listViewObject(view.config) ?? viewObject,
2207
+ `view "${viewLabel2}" (ViewItem record)`,
2208
+ `views[${vi}].config.searchableFields`,
2209
+ "list-view searchableFields",
2210
+ "narrowing"
2211
+ );
2212
+ }
2036
2213
  if (isRec3(view.list)) {
2037
2214
  check(
2038
2215
  view.list.searchableFields,
@@ -2064,33 +2241,244 @@ function listViewObject(listView) {
2064
2241
  return isRec3(data) ? strName3(data.object) : void 0;
2065
2242
  }
2066
2243
 
2067
- // src/page-walk.ts
2244
+ // src/validate-sortable-fields.ts
2245
+ var import_data8 = require("@objectstack/spec/data");
2246
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
2247
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
2248
+ var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
2068
2249
  function isRec4(v) {
2069
2250
  return !!v && typeof v === "object" && !Array.isArray(v);
2070
2251
  }
2071
2252
  function strName4(v) {
2072
2253
  return typeof v === "string" && v.length > 0 ? v : void 0;
2073
2254
  }
2255
+ function readSortKeys(declared) {
2256
+ const fromShorthand = (raw, at) => {
2257
+ const trimmed = raw.trim();
2258
+ if (!trimmed) return void 0;
2259
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
2260
+ return bare ? { field: bare, at } : void 0;
2261
+ };
2262
+ if (typeof declared === "string") {
2263
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
2264
+ }
2265
+ if (Array.isArray(declared)) {
2266
+ const keys = [];
2267
+ for (let i = 0; i < declared.length; i++) {
2268
+ const el = declared[i];
2269
+ if (typeof el === "string") {
2270
+ const k = fromShorthand(el, `[${i}]`);
2271
+ if (k) keys.push(k);
2272
+ continue;
2273
+ }
2274
+ if (!isRec4(el)) continue;
2275
+ const field = strName4(el.field);
2276
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
2277
+ }
2278
+ return keys;
2279
+ }
2280
+ return [];
2281
+ }
2282
+ function suggest4(target, known) {
2283
+ let best;
2284
+ let bestScore = Infinity;
2285
+ for (const candidate of known) {
2286
+ const d = distance3(target, candidate);
2287
+ if (d < bestScore) {
2288
+ bestScore = d;
2289
+ best = candidate;
2290
+ }
2291
+ }
2292
+ const limit = Math.max(2, Math.floor(target.length / 3));
2293
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
2294
+ }
2295
+ function distance3(a, b) {
2296
+ const m = a.length;
2297
+ const n = b.length;
2298
+ if (m === 0) return n;
2299
+ if (n === 0) return m;
2300
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
2301
+ for (let i = 1; i <= m; i++) {
2302
+ const curr = [i, ...new Array(n).fill(0)];
2303
+ for (let j = 1; j <= n; j++) {
2304
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2305
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2306
+ }
2307
+ prev = curr;
2308
+ }
2309
+ return prev[n];
2310
+ }
2311
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
2312
+ const findings = [];
2313
+ if (declared === void 0 || declared === null) return findings;
2314
+ if (!objectName) return findings;
2315
+ if (!fieldsByObject.has(objectName)) return findings;
2316
+ const target = fieldsByObject.get(objectName);
2317
+ if (!target) return findings;
2318
+ const known = target.names;
2319
+ const anchors = unprovisionedAnchors?.get(objectName);
2320
+ for (const key of readSortKeys(declared)) {
2321
+ const name = key.field;
2322
+ const head = name.split(".")[0];
2323
+ if (SYSTEM_FIELDS.has(head)) {
2324
+ if (!name.includes(".") && anchors?.has(head)) {
2325
+ findings.push({
2326
+ severity: "warning",
2327
+ rule: SORT_FIELD_UNPROVISIONED,
2328
+ where,
2329
+ path: `${path}${key.at}`,
2330
+ message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
2331
+ hint: unprovisionedAnchorHint(objectName, name)
2332
+ });
2333
+ }
2334
+ continue;
2335
+ }
2336
+ if (!known.has(head)) {
2337
+ const dotted = name.includes(".");
2338
+ findings.push({
2339
+ severity: "error",
2340
+ rule: SORT_FIELD_UNKNOWN,
2341
+ where,
2342
+ path: `${path}${key.at}`,
2343
+ 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)),
2344
+ 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(", ")}.` : "")
2345
+ });
2346
+ continue;
2347
+ }
2348
+ const meta = target.fields[name];
2349
+ if ((0, import_data8.isVirtualSearchField)(meta)) {
2350
+ const vtype = meta?.type;
2351
+ findings.push({
2352
+ severity: "error",
2353
+ rule: SORT_FIELD_UNSORTABLE,
2354
+ where,
2355
+ path: `${path}${key.at}`,
2356
+ 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.`,
2357
+ 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.`
2358
+ });
2359
+ }
2360
+ }
2361
+ return findings;
2362
+ }
2363
+ function validateSortableFields(stack) {
2364
+ const findings = [];
2365
+ if (!isRec4(stack)) return findings;
2366
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
2367
+ const fieldsByObject = indexObjectSearchTargets(stack);
2368
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2369
+ const check = (declared, objectName, where, path, subject) => {
2370
+ findings.push(
2371
+ ...checkSortDeclaration(
2372
+ declared,
2373
+ objectName,
2374
+ fieldsByObject,
2375
+ where,
2376
+ path,
2377
+ subject,
2378
+ unprovisionedAnchors
2379
+ )
2380
+ );
2381
+ };
2382
+ for (let oi = 0; oi < objects.length; oi++) {
2383
+ const obj = objects[oi];
2384
+ if (!isRec4(obj)) continue;
2385
+ const objName = strName4(obj.name);
2386
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
2387
+ if (isRec4(obj.listViews)) {
2388
+ for (const [key, lv] of Object.entries(obj.listViews)) {
2389
+ if (!isRec4(lv)) continue;
2390
+ check(
2391
+ lv.sort,
2392
+ // A built-in list view belongs to its object; an inline `data.object`
2393
+ // may still retarget it (ADR-0047 allows the explicit binding).
2394
+ listViewObject2(lv) ?? objName,
2395
+ `${label2} \u203A listViews.${key}`,
2396
+ `objects[${oi}].listViews.${key}.sort`,
2397
+ "list-view sort"
2398
+ );
2399
+ }
2400
+ }
2401
+ }
2402
+ const views = Array.isArray(stack.views) ? stack.views : [];
2403
+ for (let vi = 0; vi < views.length; vi++) {
2404
+ const view = views[vi];
2405
+ if (!isRec4(view)) continue;
2406
+ const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
2407
+ const viewObject = strName4(view.objectName) ?? strName4(view.object);
2408
+ if (view.viewKind === "list" && !isRec4(view.config)) {
2409
+ check(
2410
+ view.sort,
2411
+ listViewObject2(view) ?? viewObject,
2412
+ `view "${viewLabel2}" (flattened list overlay)`,
2413
+ `views[${vi}].sort`,
2414
+ "list-view sort"
2415
+ );
2416
+ }
2417
+ if (view.viewKind === "list" && isRec4(view.config)) {
2418
+ check(
2419
+ view.config.sort,
2420
+ listViewObject2(view.config) ?? viewObject,
2421
+ `view "${viewLabel2}" (ViewItem record)`,
2422
+ `views[${vi}].config.sort`,
2423
+ "list-view sort"
2424
+ );
2425
+ }
2426
+ if (isRec4(view.list)) {
2427
+ check(
2428
+ view.list.sort,
2429
+ listViewObject2(view.list) ?? viewObject,
2430
+ `view "${viewLabel2}" \u203A list`,
2431
+ `views[${vi}].list.sort`,
2432
+ "list-view sort"
2433
+ );
2434
+ }
2435
+ if (isRec4(view.listViews)) {
2436
+ for (const [key, lv] of Object.entries(view.listViews)) {
2437
+ if (!isRec4(lv)) continue;
2438
+ check(
2439
+ lv.sort,
2440
+ listViewObject2(lv) ?? viewObject,
2441
+ `view "${viewLabel2}" \u203A listViews.${key}`,
2442
+ `views[${vi}].listViews.${key}.sort`,
2443
+ "list-view sort"
2444
+ );
2445
+ }
2446
+ }
2447
+ }
2448
+ return findings;
2449
+ }
2450
+ function listViewObject2(listView) {
2451
+ const data = listView.data;
2452
+ return isRec4(data) ? strName4(data.object) : void 0;
2453
+ }
2454
+
2455
+ // src/page-walk.ts
2456
+ function isRec5(v) {
2457
+ return !!v && typeof v === "object" && !Array.isArray(v);
2458
+ }
2459
+ function strName5(v) {
2460
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2461
+ }
2074
2462
  var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
2075
2463
  function isSourceAuthoredPage(page) {
2076
- const kind = strName4(page.kind);
2464
+ const kind = strName5(page.kind);
2077
2465
  return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
2078
2466
  }
2079
2467
  function walkPageComponents(page, pagePath) {
2080
2468
  const out = [];
2081
- if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
2082
- const pageObject = strName4(page.object);
2469
+ if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
2470
+ const pageObject = strName5(page.object);
2083
2471
  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;
2472
+ if (!isRec5(node)) return;
2473
+ const props = isRec5(node.properties) ? node.properties : void 0;
2474
+ const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
2475
+ const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
2088
2476
  out.push({ component: node, path, objectName });
2089
2477
  if (!props) return;
2090
2478
  if (Array.isArray(props.items)) {
2091
2479
  for (let i = 0; i < props.items.length; i++) {
2092
2480
  const item = props.items[i];
2093
- if (!isRec4(item) || !Array.isArray(item.children)) continue;
2481
+ if (!isRec5(item) || !Array.isArray(item.children)) continue;
2094
2482
  for (let c = 0; c < item.children.length; c++) {
2095
2483
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
2096
2484
  }
@@ -2112,12 +2500,12 @@ function walkPageComponents(page, pagePath) {
2112
2500
  const regions = Array.isArray(page.regions) ? page.regions : [];
2113
2501
  for (let r = 0; r < regions.length; r++) {
2114
2502
  const region = regions[r];
2115
- if (!isRec4(region) || !Array.isArray(region.components)) continue;
2503
+ if (!isRec5(region) || !Array.isArray(region.components)) continue;
2116
2504
  for (let c = 0; c < region.components.length; c++) {
2117
2505
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
2118
2506
  }
2119
2507
  }
2120
- const slots = isRec4(page.slots) ? page.slots : void 0;
2508
+ const slots = isRec5(page.slots) ? page.slots : void 0;
2121
2509
  if (slots) {
2122
2510
  for (const [slot, value] of Object.entries(slots)) {
2123
2511
  const list3 = Array.isArray(value) ? value : [value];
@@ -2139,13 +2527,13 @@ function asArray8(v) {
2139
2527
  }
2140
2528
  return [];
2141
2529
  }
2142
- function strName5(v) {
2530
+ function strName6(v) {
2143
2531
  return typeof v === "string" && v.length > 0 ? v : void 0;
2144
2532
  }
2145
2533
  function strList(v) {
2146
2534
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2147
2535
  }
2148
- function distance3(a, b) {
2536
+ function distance4(a, b) {
2149
2537
  const m = a.length;
2150
2538
  const n = b.length;
2151
2539
  if (m === 0) return n;
@@ -2161,11 +2549,11 @@ function distance3(a, b) {
2161
2549
  }
2162
2550
  return prev[n];
2163
2551
  }
2164
- function suggest4(target, known) {
2552
+ function suggest5(target, known) {
2165
2553
  let best;
2166
2554
  let bestScore = Infinity;
2167
2555
  for (const candidate of known) {
2168
- const d = distance3(target, candidate);
2556
+ const d = distance4(target, candidate);
2169
2557
  if (d < bestScore) {
2170
2558
  bestScore = d;
2171
2559
  best = candidate;
@@ -2177,13 +2565,13 @@ function suggest4(target, known) {
2177
2565
  function collectActionNames(stack) {
2178
2566
  const names = /* @__PURE__ */ new Set();
2179
2567
  for (const action of asArray8(stack.actions)) {
2180
- const n = strName5(action?.name);
2568
+ const n = strName6(action?.name);
2181
2569
  if (n) names.add(n);
2182
2570
  }
2183
2571
  for (const obj of asArray8(stack.objects)) {
2184
2572
  if (!obj || typeof obj !== "object") continue;
2185
2573
  for (const action of asArray8(obj.actions)) {
2186
- const n = strName5(action?.name);
2574
+ const n = strName6(action?.name);
2187
2575
  if (n) names.add(n);
2188
2576
  }
2189
2577
  }
@@ -2200,7 +2588,7 @@ function validateActionNameRefs(stack) {
2200
2588
  rule: ACTION_NAME_UNDEFINED,
2201
2589
  where,
2202
2590
  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),
2591
+ 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
2592
  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
2593
  });
2206
2594
  };
@@ -2226,7 +2614,7 @@ function validateActionNameRefs(stack) {
2226
2614
  if (!def || typeof def !== "object") continue;
2227
2615
  if (def.execution !== "aggregate") continue;
2228
2616
  if (def.actionDef !== void 0) continue;
2229
- const name = strName5(def.name);
2617
+ const name = strName6(def.name);
2230
2618
  if (!name) continue;
2231
2619
  check(
2232
2620
  name,
@@ -2241,7 +2629,7 @@ function validateActionNameRefs(stack) {
2241
2629
  for (let vi = 0; vi < views.length; vi++) {
2242
2630
  const view = views[vi];
2243
2631
  if (!view || typeof view !== "object") continue;
2244
- const viewName = strName5(view.name) ?? strName5(view.object) ?? `#${vi}`;
2632
+ const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
2245
2633
  const owner = `view "${viewName}"`;
2246
2634
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
2247
2635
  const listViews = view.listViews;
@@ -2257,7 +2645,7 @@ function validateActionNameRefs(stack) {
2257
2645
  if (!obj || typeof obj !== "object") continue;
2258
2646
  const objListViews = obj.listViews;
2259
2647
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
2260
- const owner = `object "${strName5(obj.name) ?? `#${oi}`}"`;
2648
+ const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
2261
2649
  for (const [key, lv] of Object.entries(objListViews)) {
2262
2650
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
2263
2651
  }
@@ -2266,7 +2654,7 @@ function validateActionNameRefs(stack) {
2266
2654
  for (let pi = 0; pi < pages.length; pi++) {
2267
2655
  const page = pages[pi];
2268
2656
  if (!page || typeof page !== "object") continue;
2269
- const pageName = strName5(page.name) ?? `#${pi}`;
2657
+ const pageName = strName6(page.name) ?? `#${pi}`;
2270
2658
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2271
2659
  const props = component.properties;
2272
2660
  if (!props || typeof props !== "object") continue;
@@ -2274,7 +2662,7 @@ function validateActionNameRefs(stack) {
2274
2662
  for (let ai = 0; ai < names.length; ai++) {
2275
2663
  check(
2276
2664
  names[ai],
2277
- `page "${pageName}" \xB7 component "${strName5(component.type) ?? "?"}"`,
2665
+ `page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
2278
2666
  `${path}.properties.actionNames[${ai}]`,
2279
2667
  "Quick-actions bar"
2280
2668
  );
@@ -2285,7 +2673,7 @@ function validateActionNameRefs(stack) {
2285
2673
  for (let ai = 0; ai < apps.length; ai++) {
2286
2674
  const app = apps[ai];
2287
2675
  if (!app || typeof app !== "object") continue;
2288
- const appName = strName5(app.name) ?? `#${ai}`;
2676
+ const appName = strName6(app.name) ?? `#${ai}`;
2289
2677
  const walkNav = (items, basePath) => {
2290
2678
  const navItems = asArray8(items);
2291
2679
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -2293,20 +2681,20 @@ function validateActionNameRefs(stack) {
2293
2681
  if (!nav || typeof nav !== "object") continue;
2294
2682
  const navPath = `${basePath}[${ni}]`;
2295
2683
  const actionDef = nav.actionDef;
2296
- const actionName = strName5(actionDef?.actionName);
2684
+ const actionName = strName6(actionDef?.actionName);
2297
2685
  if (nav.type === "action" && actionName) {
2298
2686
  check(
2299
2687
  actionName,
2300
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2688
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2301
2689
  `${navPath}.actionDef.actionName`,
2302
2690
  "Navigation action item"
2303
2691
  );
2304
2692
  }
2305
- const runAction = strName5(nav.runAction);
2693
+ const runAction = strName6(nav.runAction);
2306
2694
  if (nav.type === "object" && runAction) {
2307
2695
  check(
2308
2696
  runAction,
2309
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2697
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2310
2698
  `${navPath}.runAction`,
2311
2699
  "Navigation deep-link auto-run"
2312
2700
  );
@@ -2333,23 +2721,23 @@ function asArray9(v) {
2333
2721
  }
2334
2722
  return [];
2335
2723
  }
2336
- function strName6(v) {
2724
+ function strName7(v) {
2337
2725
  return typeof v === "string" && v.length > 0 ? v : void 0;
2338
2726
  }
2339
- function isRec5(v) {
2727
+ function isRec6(v) {
2340
2728
  return !!v && typeof v === "object" && !Array.isArray(v);
2341
2729
  }
2342
2730
  function fieldRefsFrom(value, basePath) {
2343
2731
  const out = [];
2344
2732
  const one = (v, path) => {
2345
- const bare = strName6(v);
2733
+ const bare = strName7(v);
2346
2734
  if (bare) {
2347
2735
  out.push({ name: bare, path });
2348
2736
  return;
2349
2737
  }
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"}` });
2738
+ if (!isRec6(v)) return;
2739
+ const named = strName7(v.field) ?? strName7(v.name);
2740
+ if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
2353
2741
  };
2354
2742
  if (Array.isArray(value)) {
2355
2743
  for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
@@ -2375,7 +2763,12 @@ var COMPONENT_FIELD_SPECS = {
2375
2763
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2376
2764
  "record:path": { props: ["statusField"] },
2377
2765
  "element:number": { props: ["field"] },
2378
- "element:filter": { props: ["fields"] },
2766
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
2767
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
2768
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
2769
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
2770
+ // reports an authored one by name with the element-retirement prescription —
2771
+ // the same #5775/#6629 residue class as the record-picker entries below.
2379
2772
  "element:form": { props: ["fields"] },
2380
2773
  // `labelField` is the one field-bearing prop this element declares. Its former
2381
2774
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -2405,18 +2798,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2405
2798
  const sections = Array.isArray(props[key]) ? props[key] : [];
2406
2799
  for (let si = 0; si < sections.length; si++) {
2407
2800
  const section = sections[si];
2408
- if (!isRec5(section)) continue;
2801
+ if (!isRec6(section)) continue;
2409
2802
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2410
2803
  }
2411
2804
  }
2412
2805
  return refs;
2413
2806
  }
2414
2807
  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;
2808
+ const add = isRec6(props.add) ? props.add : void 0;
2809
+ const picker = add && isRec6(add.picker) ? add.picker : void 0;
2417
2810
  const at = (key) => `${basePath}${sep}${key}`;
2418
2811
  return {
2419
- relatedObject: strName6(props.objectName),
2812
+ relatedObject: strName7(props.objectName),
2420
2813
  related: [
2421
2814
  ...fieldRefsFrom(props.columns, at("columns")),
2422
2815
  ...sortFieldRefs(props.sort, at("sort")),
@@ -2425,7 +2818,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2425
2818
  ...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
2426
2819
  ],
2427
2820
  parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
2428
- pickerObject: picker ? strName6(picker.object) : void 0,
2821
+ pickerObject: picker ? strName7(picker.object) : void 0,
2429
2822
  picker: picker ? [
2430
2823
  ...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
2431
2824
  ...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
@@ -2434,13 +2827,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2434
2827
  }
2435
2828
  function indexObjectFields(stack) {
2436
2829
  const objectFields = /* @__PURE__ */ new Map();
2437
- if (!isRec5(stack)) return objectFields;
2830
+ if (!isRec6(stack)) return objectFields;
2438
2831
  for (const obj of asArray9(stack.objects)) {
2439
- const name = strName6(obj.name);
2832
+ const name = strName7(obj.name);
2440
2833
  if (!name) continue;
2441
2834
  const names = /* @__PURE__ */ new Set();
2442
2835
  for (const f of asArray9(obj.fields)) {
2443
- const fn = strName6(f.name);
2836
+ const fn = strName7(f.name);
2444
2837
  if (fn) names.add(fn);
2445
2838
  }
2446
2839
  objectFields.set(name, names);
@@ -2488,15 +2881,15 @@ function validatePageFieldBindings(stack) {
2488
2881
  for (let pi = 0; pi < pages.length; pi++) {
2489
2882
  const page = pages[pi];
2490
2883
  if (!page || typeof page !== "object") continue;
2491
- const pageName = strName6(page.name) ?? `#${pi}`;
2884
+ const pageName = strName7(page.name) ?? `#${pi}`;
2492
2885
  const checkRefs = (refs, objectName, where) => {
2493
2886
  findings.push(
2494
2887
  ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2495
2888
  );
2496
2889
  };
2497
2890
  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;
2891
+ const type = strName7(component.type);
2892
+ const props = isRec6(component.properties) ? component.properties : void 0;
2500
2893
  if (!type || !props) continue;
2501
2894
  const where = `page "${pageName}" \xB7 ${type}`;
2502
2895
  const base = `${path}.properties`;
@@ -2511,16 +2904,16 @@ function validatePageFieldBindings(stack) {
2511
2904
  if (!refs) continue;
2512
2905
  checkRefs(refs, objectName, where);
2513
2906
  }
2514
- const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2907
+ const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
2515
2908
  if (cfg) {
2516
- const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2909
+ const cfgObject = strName7(cfg.source) ?? strName7(page.object);
2517
2910
  const base = `pages[${pi}].interfaceConfig`;
2518
2911
  const refs = [
2519
2912
  ...fieldRefsFrom(cfg.columns, `${base}.columns`),
2520
2913
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2521
2914
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2522
2915
  ];
2523
- const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2916
+ const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
2524
2917
  if (userFilters) {
2525
2918
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2526
2919
  }
@@ -2542,16 +2935,16 @@ function asArray10(v) {
2542
2935
  }
2543
2936
  return [];
2544
2937
  }
2545
- function strName7(v) {
2938
+ function strName8(v) {
2546
2939
  return typeof v === "string" && v.length > 0 ? v : void 0;
2547
2940
  }
2548
2941
  function strList2(v) {
2549
2942
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2550
2943
  }
2551
- function isRec6(v) {
2944
+ function isRec7(v) {
2552
2945
  return !!v && typeof v === "object" && !Array.isArray(v);
2553
2946
  }
2554
- function distance4(a, b) {
2947
+ function distance5(a, b) {
2555
2948
  const m = a.length;
2556
2949
  const n = b.length;
2557
2950
  if (m === 0) return n;
@@ -2567,11 +2960,11 @@ function distance4(a, b) {
2567
2960
  }
2568
2961
  return prev[n];
2569
2962
  }
2570
- function suggest5(target, known) {
2963
+ function suggest6(target, known) {
2571
2964
  let best;
2572
2965
  let bestScore = Infinity;
2573
2966
  for (const c of known) {
2574
- const d = distance4(target, c);
2967
+ const d = distance5(target, c);
2575
2968
  if (d < bestScore) {
2576
2969
  bestScore = d;
2577
2970
  best = c;
@@ -2587,16 +2980,16 @@ function list2(names) {
2587
2980
  function indexDatasets(stack) {
2588
2981
  const out = /* @__PURE__ */ new Map();
2589
2982
  for (const ds of asArray10(stack.datasets)) {
2590
- const name = strName7(ds.name);
2983
+ const name = strName8(ds.name);
2591
2984
  if (!name) continue;
2592
2985
  const dimensions = /* @__PURE__ */ new Set();
2593
2986
  for (const d of asArray10(ds.dimensions)) {
2594
- const n = strName7(d.name);
2987
+ const n = strName8(d.name);
2595
2988
  if (n) dimensions.add(n);
2596
2989
  }
2597
2990
  const measures = /* @__PURE__ */ new Set();
2598
2991
  for (const m of asArray10(ds.measures)) {
2599
- const n = strName7(m.name);
2992
+ const n = strName8(m.name);
2600
2993
  if (n) measures.add(n);
2601
2994
  }
2602
2995
  out.set(name, { dimensions, measures });
@@ -2619,7 +3012,7 @@ function validateChartBindings(stack) {
2619
3012
  where: binding.where,
2620
3013
  path: `${binding.path}.dataset`,
2621
3014
  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).`
3015
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
2623
3016
  });
2624
3017
  return;
2625
3018
  }
@@ -2631,7 +3024,7 @@ function validateChartBindings(stack) {
2631
3024
  where: binding.where,
2632
3025
  path,
2633
3026
  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.`
3027
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
2635
3028
  });
2636
3029
  };
2637
3030
  const measureRef = (name, path, selected2) => {
@@ -2642,7 +3035,7 @@ function validateChartBindings(stack) {
2642
3035
  where: binding.where,
2643
3036
  path,
2644
3037
  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.`
3038
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
2646
3039
  });
2647
3040
  return;
2648
3041
  }
@@ -2677,26 +3070,26 @@ function validateChartBindings(stack) {
2677
3070
  const reports = asArray10(stack.reports);
2678
3071
  for (let ri = 0; ri < reports.length; ri++) {
2679
3072
  const report = reports[ri];
2680
- if (!isRec6(report)) continue;
2681
- const reportName = strName7(report.name) ?? `#${ri}`;
3073
+ if (!isRec7(report)) continue;
3074
+ const reportName = strName8(report.name) ?? `#${ri}`;
2682
3075
  const checkReportChart = (chart, dataset, values, where, path) => {
2683
- if (!isRec6(chart)) return;
3076
+ if (!isRec7(chart)) return;
2684
3077
  check({
2685
3078
  dataset,
2686
3079
  // `values` is the report's measure SELECTION, not a chart ref; feeding
2687
3080
  // it in lets the yAxis "declared but not selected" check work without
2688
3081
  // reporting the selection itself twice.
2689
3082
  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),
3083
+ xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
3084
+ yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
3085
+ series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
2693
3086
  where,
2694
3087
  path: `${path}.chart`
2695
3088
  });
2696
3089
  };
2697
3090
  checkReportChart(
2698
3091
  report.chart,
2699
- strName7(report.dataset),
3092
+ strName8(report.dataset),
2700
3093
  strList2(report.values),
2701
3094
  `report "${reportName}" \xB7 chart`,
2702
3095
  `reports[${ri}]`
@@ -2704,22 +3097,22 @@ function validateChartBindings(stack) {
2704
3097
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2705
3098
  for (let bi = 0; bi < blocks.length; bi++) {
2706
3099
  const block = blocks[bi];
2707
- if (!isRec6(block)) continue;
3100
+ if (!isRec7(block)) continue;
2708
3101
  checkReportChart(
2709
3102
  block.chart,
2710
- strName7(block.dataset),
3103
+ strName8(block.dataset),
2711
3104
  strList2(block.values),
2712
- `report "${reportName}" \xB7 block "${strName7(block.name) ?? `#${bi}`}" chart`,
3105
+ `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
2713
3106
  `reports[${ri}].blocks[${bi}]`
2714
3107
  );
2715
3108
  }
2716
3109
  }
2717
3110
  const checkListChart = (container, where, path) => {
2718
- if (!isRec6(container)) return;
3111
+ if (!isRec7(container)) return;
2719
3112
  const chart = container.chart;
2720
- if (!isRec6(chart)) return;
3113
+ if (!isRec7(chart)) return;
2721
3114
  check({
2722
- dataset: strName7(chart.dataset),
3115
+ dataset: strName8(chart.dataset),
2723
3116
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
2724
3117
  values: { names: strList2(chart.values), path: `${path}.chart.values` },
2725
3118
  where,
@@ -2729,10 +3122,10 @@ function validateChartBindings(stack) {
2729
3122
  const views = asArray10(stack.views);
2730
3123
  for (let vi = 0; vi < views.length; vi++) {
2731
3124
  const view = views[vi];
2732
- if (!isRec6(view)) continue;
2733
- const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
3125
+ if (!isRec7(view)) continue;
3126
+ const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
2734
3127
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2735
- if (isRec6(view.listViews)) {
3128
+ if (isRec7(view.listViews)) {
2736
3129
  for (const [key, lv] of Object.entries(view.listViews)) {
2737
3130
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2738
3131
  }
@@ -2741,8 +3134,8 @@ function validateChartBindings(stack) {
2741
3134
  const objects = asArray10(stack.objects);
2742
3135
  for (let oi = 0; oi < objects.length; oi++) {
2743
3136
  const obj = objects[oi];
2744
- if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2745
- const objName = strName7(obj.name) ?? `#${oi}`;
3137
+ if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
3138
+ const objName = strName8(obj.name) ?? `#${oi}`;
2746
3139
  for (const [key, lv] of Object.entries(obj.listViews)) {
2747
3140
  checkListChart(
2748
3141
  lv,
@@ -2754,19 +3147,19 @@ function validateChartBindings(stack) {
2754
3147
  const pages = asArray10(stack.pages);
2755
3148
  for (let pi = 0; pi < pages.length; pi++) {
2756
3149
  const page = pages[pi];
2757
- if (!isRec6(page)) continue;
2758
- const pageName = strName7(page.name) ?? `#${pi}`;
3150
+ if (!isRec7(page)) continue;
3151
+ const pageName = strName8(page.name) ?? `#${pi}`;
2759
3152
  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);
3153
+ const props = isRec7(component.properties) ? component.properties : void 0;
3154
+ if (!props || !strName8(props.dataset)) continue;
3155
+ const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
3156
+ const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
2764
3157
  check({
2765
- dataset: strName7(props.dataset),
3158
+ dataset: strName8(props.dataset),
2766
3159
  dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
2767
3160
  values: { names: strList2(props.values), path: `${path}.properties.values` },
2768
3161
  series: [...axisRefs, ...seriesRefs],
2769
- where: `page "${pageName}" \xB7 ${strName7(component.type) ?? "chart"}`,
3162
+ where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
2770
3163
  path: `${path}.properties`
2771
3164
  });
2772
3165
  }
@@ -2833,7 +3226,7 @@ function asArray12(v) {
2833
3226
  }
2834
3227
  return [];
2835
3228
  }
2836
- function strName8(v) {
3229
+ function strName9(v) {
2837
3230
  return typeof v === "string" && v.length > 0 ? v : void 0;
2838
3231
  }
2839
3232
  function collectNavExposures(stack) {
@@ -2842,18 +3235,18 @@ function collectNavExposures(stack) {
2842
3235
  for (let ai = 0; ai < apps.length; ai++) {
2843
3236
  const app = apps[ai];
2844
3237
  if (!app || typeof app !== "object") continue;
2845
- const appName = strName8(app.name) ?? `#${ai}`;
3238
+ const appName = strName9(app.name) ?? `#${ai}`;
2846
3239
  const walk = (items, basePath) => {
2847
3240
  const navItems = asArray12(items);
2848
3241
  for (let ni = 0; ni < navItems.length; ni++) {
2849
3242
  const nav = navItems[ni];
2850
3243
  if (!nav || typeof nav !== "object") continue;
2851
3244
  const navPath = `${basePath}[${ni}]`;
2852
- const objectName = strName8(nav.objectName);
3245
+ const objectName = strName9(nav.objectName);
2853
3246
  if (nav.type === "object" && objectName) {
2854
3247
  out.push({
2855
3248
  objectName,
2856
- where: `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
3249
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
2857
3250
  path: `${navPath}.objectName`
2858
3251
  });
2859
3252
  }
@@ -2877,7 +3270,7 @@ function validateNavAccess(stack) {
2877
3270
  if (exposures.length === 0) return findings;
2878
3271
  const ownObjects = /* @__PURE__ */ new Set();
2879
3272
  for (const obj of asArray12(stack.objects)) {
2880
- const n = strName8(obj.name);
3273
+ const n = strName9(obj.name);
2881
3274
  if (n) ownObjects.add(n);
2882
3275
  }
2883
3276
  const readable = /* @__PURE__ */ new Set();
@@ -2907,13 +3300,13 @@ function validateNavAccess(stack) {
2907
3300
 
2908
3301
  // src/validate-nav-target-refs.ts
2909
3302
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2910
- var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3303
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2911
3304
  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 });
3305
+ if (Array.isArray(v)) return v.filter(isRec8);
3306
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
2914
3307
  return [];
2915
3308
  }
2916
- function strName9(v) {
3309
+ function strName10(v) {
2917
3310
  return typeof v === "string" && v.length > 0 ? v : void 0;
2918
3311
  }
2919
3312
  var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
@@ -2925,14 +3318,14 @@ var NAV_TARGETS = [
2925
3318
  function namesOf(collection) {
2926
3319
  const out = /* @__PURE__ */ new Set();
2927
3320
  for (const entry of asArray13(collection)) {
2928
- const n = strName9(entry.name);
3321
+ const n = strName10(entry.name);
2929
3322
  if (n) out.add(n);
2930
3323
  }
2931
3324
  return out;
2932
3325
  }
2933
3326
  function validateNavTargetRefs(stack) {
2934
3327
  const findings = [];
2935
- if (!isRec7(stack)) return findings;
3328
+ if (!isRec8(stack)) return findings;
2936
3329
  const apps = asArray13(stack.apps);
2937
3330
  if (apps.length === 0) return findings;
2938
3331
  const declared = /* @__PURE__ */ new Map();
@@ -2940,16 +3333,16 @@ function validateNavTargetRefs(stack) {
2940
3333
  declared.set(collection, namesOf(stack[collection]));
2941
3334
  }
2942
3335
  for (const [ai, app] of apps.entries()) {
2943
- const appName = strName9(app.name) ?? `#${ai}`;
3336
+ const appName = strName10(app.name) ?? `#${ai}`;
2944
3337
  const walk = (items, basePath) => {
2945
3338
  if (!Array.isArray(items)) return;
2946
3339
  for (const [ni, raw] of items.entries()) {
2947
- if (!isRec7(raw)) continue;
3340
+ if (!isRec8(raw)) continue;
2948
3341
  const nav = raw;
2949
3342
  const navPath = `${basePath}[${ni}]`;
2950
3343
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
2951
3344
  if (nav.type !== type) continue;
2952
- const target = strName9(nav[prop]);
3345
+ const target = strName10(nav[prop]);
2953
3346
  if (!target || isInterpolated2(target)) continue;
2954
3347
  const known = declared.get(collection);
2955
3348
  if (known.has(target)) continue;
@@ -2957,7 +3350,7 @@ function validateNavTargetRefs(stack) {
2957
3350
  findings.push({
2958
3351
  severity: "warning",
2959
3352
  rule: NAV_TARGET_UNRESOLVED,
2960
- where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
3353
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
2961
3354
  path: `${navPath}.${prop}`,
2962
3355
  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
3356
  hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
@@ -2976,44 +3369,44 @@ function validateNavTargetRefs(stack) {
2976
3369
  }
2977
3370
 
2978
3371
  // src/validate-nav-object-servability.ts
2979
- var import_data7 = require("@objectstack/spec/data");
3372
+ var import_data9 = require("@objectstack/spec/data");
2980
3373
  var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2981
- var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3374
+ var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2982
3375
  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 });
3376
+ if (Array.isArray(v)) return v.filter(isRec9);
3377
+ if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
2985
3378
  return [];
2986
3379
  }
2987
- function strName10(v) {
3380
+ function strName11(v) {
2988
3381
  return typeof v === "string" && v.length > 0 ? v : void 0;
2989
3382
  }
2990
3383
  var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2991
3384
  function validateNavObjectServability(stack) {
2992
3385
  const findings = [];
2993
- if (!isRec8(stack)) return findings;
3386
+ if (!isRec9(stack)) return findings;
2994
3387
  const apps = asArray14(stack.apps);
2995
3388
  if (apps.length === 0) return findings;
2996
3389
  const ownEnable = /* @__PURE__ */ new Map();
2997
3390
  const objects = asArray14(stack.objects);
2998
3391
  for (const [oi, obj] of objects.entries()) {
2999
- const n = strName10(obj.name);
3392
+ const n = strName11(obj.name);
3000
3393
  if (!n) continue;
3001
3394
  ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
3002
3395
  }
3003
3396
  if (ownEnable.size === 0) return findings;
3004
3397
  for (const [ai, app] of apps.entries()) {
3005
- const appName = strName10(app.name) ?? `#${ai}`;
3398
+ const appName = strName11(app.name) ?? `#${ai}`;
3006
3399
  const walk = (items, basePath) => {
3007
3400
  if (!Array.isArray(items)) return;
3008
3401
  for (const [ni, raw] of items.entries()) {
3009
- if (!isRec8(raw)) continue;
3402
+ if (!isRec9(raw)) continue;
3010
3403
  const nav = raw;
3011
3404
  const navPath = `${basePath}[${ni}]`;
3012
3405
  if (nav.type === "object") {
3013
- const target = strName10(nav.objectName);
3406
+ const target = strName11(nav.objectName);
3014
3407
  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 : {};
3408
+ if (target && declared && !(0, import_data9.canServeApiOperation)(declared.enable, "list")) {
3409
+ const enable = isRec9(declared.enable) ? declared.enable : {};
3017
3410
  const apiDisabled = enable.apiEnabled === false;
3018
3411
  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
3412
  const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
@@ -3021,7 +3414,7 @@ function validateNavObjectServability(stack) {
3021
3414
  findings.push({
3022
3415
  severity: "error",
3023
3416
  rule: NAV_OBJECT_UNSERVABLE,
3024
- where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3417
+ where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
3025
3418
  // The nav entry is where the dead row is authored; the `enable`
3026
3419
  // key that condemns it is named in the message, because the fix
3027
3420
  // may belong at either end.
@@ -3048,27 +3441,27 @@ var import_spec = require("@objectstack/spec");
3048
3441
  var import_system4 = require("@objectstack/spec/system");
3049
3442
 
3050
3443
  // src/view-walk.ts
3051
- function isRec9(v) {
3444
+ function isRec10(v) {
3052
3445
  return !!v && typeof v === "object" && !Array.isArray(v);
3053
3446
  }
3054
- function strName11(v) {
3447
+ function strName12(v) {
3055
3448
  return typeof v === "string" && v.length > 0 ? v : void 0;
3056
3449
  }
3057
3450
  function viewObjectName(view) {
3058
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
3451
+ return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
3059
3452
  }
3060
3453
  function viewContainerSites(view, basePath) {
3061
- if (!isRec9(view)) return [];
3454
+ if (!isRec10(view)) return [];
3062
3455
  const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3063
- if (isRec9(view.form)) {
3456
+ if (isRec10(view.form)) {
3064
3457
  sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3065
3458
  }
3066
3459
  for (const key of ["listViews", "formViews"]) {
3067
3460
  const container = view[key];
3068
- if (!isRec9(container)) continue;
3461
+ if (!isRec10(container)) continue;
3069
3462
  const kind = key === "listViews" ? "listView" : "formView";
3070
3463
  for (const [subKey, sub] of Object.entries(container)) {
3071
- if (!isRec9(sub)) continue;
3464
+ if (!isRec10(sub)) continue;
3072
3465
  sites.push({
3073
3466
  view: sub,
3074
3467
  path: `${basePath}.${key}.${subKey}`,
@@ -3086,18 +3479,18 @@ function formViewSites(view, basePath) {
3086
3479
  // src/validate-translation-references.ts
3087
3480
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
3088
3481
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
3089
- function isRec10(v) {
3482
+ function isRec11(v) {
3090
3483
  return !!v && typeof v === "object" && !Array.isArray(v);
3091
3484
  }
3092
3485
  function asArray15(v) {
3093
3486
  if (Array.isArray(v)) return v;
3094
- if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
3487
+ if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
3095
3488
  return [];
3096
3489
  }
3097
- function strName12(v) {
3490
+ function strName13(v) {
3098
3491
  return typeof v === "string" && v.length > 0 ? v : void 0;
3099
3492
  }
3100
- function distance5(a, b) {
3493
+ function distance6(a, b) {
3101
3494
  const m = a.length;
3102
3495
  const n = b.length;
3103
3496
  if (m === 0) return n;
@@ -3113,7 +3506,7 @@ function distance5(a, b) {
3113
3506
  }
3114
3507
  return prev[n];
3115
3508
  }
3116
- function suggest6(target, known) {
3509
+ function suggest7(target, known) {
3117
3510
  const names = [...known];
3118
3511
  const segmentMatch = names.find(
3119
3512
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -3122,7 +3515,7 @@ function suggest6(target, known) {
3122
3515
  let best;
3123
3516
  let bestScore = Infinity;
3124
3517
  for (const candidate of names) {
3125
- const d = distance5(target, candidate);
3518
+ const d = distance6(target, candidate);
3126
3519
  if (d < bestScore) {
3127
3520
  bestScore = d;
3128
3521
  best = candidate;
@@ -3155,33 +3548,33 @@ function collectViewRecord(view, factsFor) {
3155
3548
  const addSections = (container, binding) => {
3156
3549
  if (!binding) return;
3157
3550
  for (const section of asArray15(container.sections)) {
3158
- const sectionName = strName12(section.name);
3551
+ const sectionName = strName13(section.name);
3159
3552
  if (sectionName) factsFor(binding).sections.add(sectionName);
3160
3553
  }
3161
3554
  };
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));
3555
+ const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
3556
+ if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3557
+ addView(recordObject ?? listBinding, strName13(view.name));
3165
3558
  const named = namedViewKeys(view);
3166
3559
  for (const family of ["listViews", "formViews"]) {
3167
3560
  const container = view[family];
3168
- if (!isRec10(container)) continue;
3561
+ if (!isRec11(container)) continue;
3169
3562
  const registryKeys = family === "listViews" ? named.list : named.form;
3170
3563
  let at = 0;
3171
3564
  for (const sub of Object.values(container)) {
3172
3565
  if (!sub || typeof sub !== "object") continue;
3173
3566
  const registryKey = registryKeys[at++];
3174
- if (!isRec10(sub)) continue;
3567
+ if (!isRec11(sub)) continue;
3175
3568
  const binding = bindingOf(sub) ?? listBinding;
3176
3569
  addView(binding, registryKey);
3177
3570
  addSections(sub, binding);
3178
3571
  }
3179
3572
  }
3180
- if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3573
+ if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3181
3574
  addSections(view, recordObject ?? listBinding);
3182
3575
  }
3183
3576
  function defaultListViewKey(object, container) {
3184
- if (!object || !isRec10(container.list)) return void 0;
3577
+ if (!object || !isRec11(container.list)) return void 0;
3185
3578
  const item = (0, import_spec.expandViewContainer)(object, container).find(
3186
3579
  (i) => i.viewKind === "list" && i.isDefault
3187
3580
  );
@@ -3193,7 +3586,7 @@ function namedViewKeys(container) {
3193
3586
  const object = "probe";
3194
3587
  const prefix = `${object}.`;
3195
3588
  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;
3589
+ const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3197
3590
  const listCount = countEntries(container.listViews);
3198
3591
  const formCount = countEntries(container.formViews);
3199
3592
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -3211,14 +3604,14 @@ function readOptions(field) {
3211
3604
  values.add(opt);
3212
3605
  continue;
3213
3606
  }
3214
- if (!isRec10(opt)) continue;
3215
- const value = strName12(opt.value);
3607
+ if (!isRec11(opt)) continue;
3608
+ const value = strName13(opt.value);
3216
3609
  if (!value) continue;
3217
3610
  values.add(value);
3218
- const label2 = strName12(opt.label);
3611
+ const label2 = strName13(opt.label);
3219
3612
  if (label2) byLabel.set(label2.toLowerCase(), value);
3220
3613
  }
3221
- } else if (isRec10(raw)) {
3614
+ } else if (isRec11(raw)) {
3222
3615
  for (const [value, label2] of Object.entries(raw)) {
3223
3616
  values.add(value);
3224
3617
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -3239,23 +3632,23 @@ function buildUniverse(stack) {
3239
3632
  return facts;
3240
3633
  };
3241
3634
  for (const obj of asArray15(stack.objects)) {
3242
- const objectName = strName12(obj.name);
3635
+ const objectName = strName13(obj.name);
3243
3636
  if (!objectName) continue;
3244
3637
  const facts = factsFor(objectName);
3245
3638
  for (const field of asArray15(obj.fields)) {
3246
- const fieldName = strName12(field.name);
3639
+ const fieldName = strName13(field.name);
3247
3640
  if (fieldName) facts.fields.set(fieldName, field);
3248
3641
  }
3249
3642
  for (const action of asArray15(obj.actions)) {
3250
- const actionName = strName12(action.name);
3643
+ const actionName = strName13(action.name);
3251
3644
  if (actionName) facts.actions.set(actionName, action);
3252
3645
  }
3253
3646
  for (const view of asArray15(obj.views)) {
3254
- collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
3647
+ collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
3255
3648
  }
3256
3649
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
3257
3650
  for (const group of asArray15(obj.fieldGroups)) {
3258
- const key = strName12(group.key) ?? strName12(group.name);
3651
+ const key = strName13(group.key) ?? strName13(group.name);
3259
3652
  if (key) facts.sections.add(key);
3260
3653
  }
3261
3654
  }
@@ -3266,10 +3659,10 @@ function buildUniverse(stack) {
3266
3659
  for (let pi = 0; pi < pages.length; pi++) {
3267
3660
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
3268
3661
  if (!walked.objectName) continue;
3269
- const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3662
+ const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
3270
3663
  if (!props) continue;
3271
3664
  for (const section of asArray15(props.sections)) {
3272
- const sectionName = strName12(section.name);
3665
+ const sectionName = strName13(section.name);
3273
3666
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
3274
3667
  }
3275
3668
  }
@@ -3277,9 +3670,9 @@ function buildUniverse(stack) {
3277
3670
  const globalActions = /* @__PURE__ */ new Map();
3278
3671
  const actionOwners = /* @__PURE__ */ new Map();
3279
3672
  for (const action of asArray15(stack.actions)) {
3280
- const actionName = strName12(action.name);
3673
+ const actionName = strName13(action.name);
3281
3674
  if (!actionName) continue;
3282
- const owner = strName12(action.objectName) ?? strName12(action.object);
3675
+ const owner = strName13(action.objectName) ?? strName13(action.object);
3283
3676
  if (owner) {
3284
3677
  factsFor(owner).actions.set(actionName, action);
3285
3678
  actionOwners.set(actionName, owner);
@@ -3294,19 +3687,19 @@ function buildUniverse(stack) {
3294
3687
  }
3295
3688
  const apps = /* @__PURE__ */ new Map();
3296
3689
  for (const app of asArray15(stack.apps)) {
3297
- const appName = strName12(app.name);
3690
+ const appName = strName13(app.name);
3298
3691
  if (!appName) continue;
3299
3692
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
3300
3693
  const walkNav = (items) => {
3301
3694
  for (const item of asArray15(items)) {
3302
- const id = strName12(item.id);
3695
+ const id = strName13(item.id);
3303
3696
  if (id) navIds.add(id);
3304
3697
  if (item.children) walkNav(item.children);
3305
3698
  }
3306
3699
  };
3307
3700
  walkNav(app.navigation);
3308
3701
  for (const area of asArray15(app.areas)) {
3309
- const areaId = strName12(area.id);
3702
+ const areaId = strName13(area.id);
3310
3703
  if (areaId) navIds.add(areaId);
3311
3704
  walkNav(area.navigation);
3312
3705
  }
@@ -3314,20 +3707,20 @@ function buildUniverse(stack) {
3314
3707
  }
3315
3708
  const dashboards = /* @__PURE__ */ new Map();
3316
3709
  for (const dash of asArray15(stack.dashboards)) {
3317
- const dashName = strName12(dash.name);
3710
+ const dashName = strName13(dash.name);
3318
3711
  if (!dashName) continue;
3319
3712
  const widgets = /* @__PURE__ */ new Set();
3320
3713
  for (const widget of asArray15(dash.widgets)) {
3321
- const id = strName12(widget.id) ?? strName12(widget.name);
3714
+ const id = strName13(widget.id) ?? strName13(widget.name);
3322
3715
  if (id) widgets.add(id);
3323
3716
  }
3324
3717
  const actions = /* @__PURE__ */ new Set();
3325
3718
  const headerActions = [
3326
- ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3719
+ ...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
3327
3720
  ...asArray15(dash.actions)
3328
3721
  ];
3329
3722
  for (const action of headerActions) {
3330
- const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
3723
+ const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
3331
3724
  if (key) actions.add(key);
3332
3725
  }
3333
3726
  dashboards.set(dashName, { widgets, actions });
@@ -3339,7 +3732,7 @@ function localePath(bundleIndex, locale) {
3339
3732
  }
3340
3733
  function validateTranslationReferences(stack) {
3341
3734
  const findings = [];
3342
- if (!isRec10(stack)) return findings;
3735
+ if (!isRec11(stack)) return findings;
3343
3736
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3344
3737
  if (bundles.length === 0) return findings;
3345
3738
  const universe = buildUniverse(stack);
@@ -3348,13 +3741,13 @@ function validateTranslationReferences(stack) {
3348
3741
  };
3349
3742
  for (let bi = 0; bi < bundles.length; bi++) {
3350
3743
  const bundle = bundles[bi];
3351
- if (!isRec10(bundle)) continue;
3744
+ if (!isRec11(bundle)) continue;
3352
3745
  for (const [locale, rawData] of Object.entries(bundle)) {
3353
- if (!isRec10(rawData)) continue;
3746
+ if (!isRec11(rawData)) continue;
3354
3747
  const base = localePath(bi, locale);
3355
3748
  const inLocale = `locale "${locale}"`;
3356
3749
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
3357
- if (!isRec10(rawNode)) continue;
3750
+ if (!isRec11(rawNode)) continue;
3358
3751
  const objPath = `${base}.objects.${objectName}`;
3359
3752
  const facts = universe.objects.get(objectName);
3360
3753
  if (!facts) {
@@ -3362,7 +3755,7 @@ function validateTranslationReferences(stack) {
3362
3755
  orphan(
3363
3756
  `${inLocale} \xB7 object "${objectName}"`,
3364
3757
  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()),
3758
+ (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
3759
  `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
3760
  );
3368
3761
  continue;
@@ -3375,12 +3768,12 @@ function validateTranslationReferences(stack) {
3375
3768
  orphan(
3376
3769
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
3377
3770
  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()),
3771
+ `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
3772
  `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
3773
  );
3381
3774
  continue;
3382
3775
  }
3383
- if (!isRec10(rawField)) continue;
3776
+ if (!isRec11(rawField)) continue;
3384
3777
  checkOptionKeys(findings, {
3385
3778
  optionMap: rawField.options,
3386
3779
  field,
@@ -3395,7 +3788,7 @@ function validateTranslationReferences(stack) {
3395
3788
  orphan(
3396
3789
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
3397
3790
  `${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),
3791
+ `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
3792
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
3400
3793
  );
3401
3794
  }
@@ -3404,7 +3797,7 @@ function validateTranslationReferences(stack) {
3404
3797
  orphan(
3405
3798
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
3406
3799
  `${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),
3800
+ `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
3801
  `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
3802
  );
3410
3803
  }
@@ -3415,7 +3808,7 @@ function validateTranslationReferences(stack) {
3415
3808
  orphan(
3416
3809
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
3417
3810
  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()),
3811
+ `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
3812
  `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
3813
  );
3421
3814
  continue;
@@ -3437,7 +3830,7 @@ function validateTranslationReferences(stack) {
3437
3830
  orphan(
3438
3831
  `${inLocale} \xB7 global action "${actionName}"`,
3439
3832
  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()),
3833
+ 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
3834
  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
3835
  );
3443
3836
  continue;
@@ -3457,18 +3850,18 @@ function validateTranslationReferences(stack) {
3457
3850
  orphan(
3458
3851
  `${inLocale} \xB7 app "${appName}"`,
3459
3852
  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()),
3853
+ `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
3854
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
3462
3855
  );
3463
3856
  continue;
3464
3857
  }
3465
- if (!isRec10(rawApp)) continue;
3858
+ if (!isRec11(rawApp)) continue;
3466
3859
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
3467
3860
  if (navIds.has(navId)) continue;
3468
3861
  orphan(
3469
3862
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
3470
3863
  `${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),
3864
+ `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
3865
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
3473
3866
  );
3474
3867
  }
@@ -3480,18 +3873,18 @@ function validateTranslationReferences(stack) {
3480
3873
  orphan(
3481
3874
  `${inLocale} \xB7 dashboard "${dashName}"`,
3482
3875
  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()),
3876
+ `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
3877
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
3485
3878
  );
3486
3879
  continue;
3487
3880
  }
3488
- if (!isRec10(rawDash)) continue;
3881
+ if (!isRec11(rawDash)) continue;
3489
3882
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
3490
3883
  if (dash.widgets.has(widgetId)) continue;
3491
3884
  orphan(
3492
3885
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
3493
3886
  `${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),
3887
+ `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
3888
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
3496
3889
  );
3497
3890
  }
@@ -3500,7 +3893,7 @@ function validateTranslationReferences(stack) {
3500
3893
  orphan(
3501
3894
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
3502
3895
  `${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),
3896
+ `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
3897
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
3505
3898
  );
3506
3899
  }
@@ -3510,7 +3903,7 @@ function validateTranslationReferences(stack) {
3510
3903
  return findings;
3511
3904
  }
3512
3905
  function asRecord(v) {
3513
- return isRec10(v) ? v : {};
3906
+ return isRec11(v) ? v : {};
3514
3907
  }
3515
3908
  function checkOptionKeys(findings, ctx) {
3516
3909
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3522,7 +3915,7 @@ function checkOptionKeys(findings, ctx) {
3522
3915
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3523
3916
  where: ctx.where,
3524
3917
  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.`,
3918
+ 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
3919
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3527
3920
  });
3528
3921
  return;
@@ -3535,17 +3928,17 @@ function checkOptionKeys(findings, ctx) {
3535
3928
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3536
3929
  where: ctx.where,
3537
3930
  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),
3931
+ 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
3932
  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
3933
  });
3541
3934
  }
3542
3935
  }
3543
3936
  function checkActionParams(findings, ctx) {
3544
- const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3937
+ const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
3545
3938
  if (rawParams.length === 0) return;
3546
3939
  const declared = /* @__PURE__ */ new Set();
3547
3940
  for (const param of asArray15(ctx.action.params)) {
3548
- const name = strName12(param.name) ?? strName12(param.field);
3941
+ const name = strName13(param.name) ?? strName13(param.field);
3549
3942
  if (name) declared.add(name);
3550
3943
  }
3551
3944
  for (const paramName of rawParams) {
@@ -3555,40 +3948,40 @@ function checkActionParams(findings, ctx) {
3555
3948
  rule: TRANSLATION_TARGET_UNKNOWN,
3556
3949
  where: `${ctx.where} \xB7 param "${paramName}"`,
3557
3950
  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),
3951
+ 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
3952
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
3560
3953
  });
3561
3954
  }
3562
3955
  }
3563
3956
 
3564
3957
  // src/collection-entries.ts
3565
- function isRec11(v) {
3958
+ function isRec12(v) {
3566
3959
  return !!v && typeof v === "object" && !Array.isArray(v);
3567
3960
  }
3568
3961
  function collectionEntries(v, base) {
3569
3962
  if (Array.isArray(v)) {
3570
3963
  const out = [];
3571
3964
  for (let i = 0; i < v.length; i++) {
3572
- if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3965
+ if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3573
3966
  }
3574
3967
  return out;
3575
3968
  }
3576
- if (isRec11(v)) {
3577
- return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3969
+ if (isRec12(v)) {
3970
+ return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3578
3971
  }
3579
3972
  return [];
3580
3973
  }
3581
3974
 
3582
3975
  // src/validate-translatable-sections.ts
3583
3976
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3584
- function isRec12(v) {
3977
+ function isRec13(v) {
3585
3978
  return !!v && typeof v === "object" && !Array.isArray(v);
3586
3979
  }
3587
- function strName13(v) {
3980
+ function strName14(v) {
3588
3981
  return typeof v === "string" && v.length > 0 ? v : void 0;
3589
3982
  }
3590
3983
  function viewLabel(view) {
3591
- const name = strName13(view.name);
3984
+ const name = strName14(view.name);
3592
3985
  return name ? `view "${name}"` : "";
3593
3986
  }
3594
3987
  function joinWhere(...parts) {
@@ -3596,7 +3989,7 @@ function joinWhere(...parts) {
3596
3989
  }
3597
3990
  function collectViewSites(view, basePath, label2, sites) {
3598
3991
  const recordObject = viewObjectName(view);
3599
- const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3992
+ const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3600
3993
  for (const site of viewContainerSites(view, basePath)) {
3601
3994
  sites.push({
3602
3995
  path: `${site.path}.sections`,
@@ -3610,11 +4003,11 @@ function translatedObjectNames(stack) {
3610
4003
  const out = /* @__PURE__ */ new Set();
3611
4004
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3612
4005
  for (const bundle of bundles) {
3613
- if (!isRec12(bundle)) continue;
4006
+ if (!isRec13(bundle)) continue;
3614
4007
  for (const data of Object.values(bundle)) {
3615
- if (!isRec12(data) || !isRec12(data.objects)) continue;
4008
+ if (!isRec13(data) || !isRec13(data.objects)) continue;
3616
4009
  for (const [objectName, node] of Object.entries(data.objects)) {
3617
- if (isRec12(node)) out.add(objectName);
4010
+ if (isRec13(node)) out.add(objectName);
3618
4011
  }
3619
4012
  }
3620
4013
  }
@@ -3626,22 +4019,22 @@ function suggestedName(label2) {
3626
4019
  }
3627
4020
  function validateTranslatableSections(stack) {
3628
4021
  const findings = [];
3629
- if (!isRec12(stack)) return findings;
4022
+ if (!isRec13(stack)) return findings;
3630
4023
  const translated = translatedObjectNames(stack);
3631
4024
  if (translated.size === 0) return findings;
3632
4025
  const sites = [];
3633
4026
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3634
- const objectName = strName13(obj.name);
4027
+ const objectName = strName14(obj.name);
3635
4028
  if (!objectName) continue;
3636
4029
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3637
4030
  collectViewSites(
3638
- { ...view, object: strName13(view.object) ?? objectName },
4031
+ { ...view, object: strName14(view.object) ?? objectName },
3639
4032
  path,
3640
4033
  viewLabel(view),
3641
4034
  sites
3642
4035
  );
3643
4036
  }
3644
- if (isRec12(obj.listViews)) {
4037
+ if (isRec13(obj.listViews)) {
3645
4038
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3646
4039
  }
3647
4040
  }
@@ -3649,13 +4042,13 @@ function validateTranslatableSections(stack) {
3649
4042
  collectViewSites(view, path, viewLabel(view), sites);
3650
4043
  }
3651
4044
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3652
- const pageName = strName13(page.name);
4045
+ const pageName = strName14(page.name);
3653
4046
  const pageLabel = pageName ? `page "${pageName}"` : "";
3654
4047
  for (const walked of walkPageComponents(page, pagePath)) {
3655
4048
  if (!walked.objectName) continue;
3656
- const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
4049
+ const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
3657
4050
  if (!props) continue;
3658
- const type = strName13(walked.component.type) ?? "component";
4051
+ const type = strName14(walked.component.type) ?? "component";
3659
4052
  sites.push({
3660
4053
  path: `${walked.path}.properties.sections`,
3661
4054
  surface: joinWhere(pageLabel, type),
@@ -3670,9 +4063,9 @@ function validateTranslatableSections(stack) {
3670
4063
  if (!Array.isArray(site.sections)) continue;
3671
4064
  for (let i = 0; i < site.sections.length; i++) {
3672
4065
  const section = site.sections[i];
3673
- if (!isRec12(section)) continue;
3674
- if (strName13(section.name)) continue;
3675
- const heading = strName13(section.label);
4066
+ if (!isRec13(section)) continue;
4067
+ if (strName14(section.name)) continue;
4068
+ const heading = strName14(section.label);
3676
4069
  if (!heading) continue;
3677
4070
  const slug = suggestedName(heading);
3678
4071
  findings.push({
@@ -3690,10 +4083,10 @@ function validateTranslatableSections(stack) {
3690
4083
 
3691
4084
  // src/flow-walk.ts
3692
4085
  var import_automation2 = require("@objectstack/spec/automation");
3693
- function isRec13(v) {
4086
+ function isRec14(v) {
3694
4087
  return !!v && typeof v === "object" && !Array.isArray(v);
3695
4088
  }
3696
- function strName14(v) {
4089
+ function strName15(v) {
3697
4090
  return typeof v === "string" && v.length > 0 ? v : void 0;
3698
4091
  }
3699
4092
  var REGION_SLOTS = new Map(
@@ -3702,10 +4095,10 @@ var REGION_SLOTS = new Map(
3702
4095
  var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
3703
4096
  var MAX_REGION_DEPTH = 16;
3704
4097
  function flowNodeLabel(node, index) {
3705
- return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
4098
+ return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
3706
4099
  }
3707
4100
  function stripRegions(config) {
3708
- if (!isRec13(config)) return void 0;
4101
+ if (!isRec14(config)) return void 0;
3709
4102
  let out;
3710
4103
  for (const key of Object.keys(config)) {
3711
4104
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3716,11 +4109,11 @@ function stripRegions(config) {
3716
4109
  }
3717
4110
  function walkFlowNodes(flow, flowPath) {
3718
4111
  const out = [];
3719
- if (!isRec13(flow)) return out;
4112
+ if (!isRec14(flow)) return out;
3720
4113
  const visitList = (nodes, basePath, trail, depth) => {
3721
4114
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3722
4115
  nodes.forEach((raw, index) => {
3723
- if (!isRec13(raw)) return;
4116
+ if (!isRec14(raw)) return;
3724
4117
  const path = `${basePath}[${index}]`;
3725
4118
  out.push({
3726
4119
  node: raw,
@@ -3729,9 +4122,9 @@ function walkFlowNodes(flow, flowPath) {
3729
4122
  regionTrail: trail,
3730
4123
  depth
3731
4124
  });
3732
- const type = strName14(raw.type);
4125
+ const type = strName15(raw.type);
3733
4126
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3734
- if (!slots || !isRec13(raw.config)) return;
4127
+ if (!slots || !isRec14(raw.config)) return;
3735
4128
  const config = raw.config;
3736
4129
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3737
4130
  for (const slot of slots) {
@@ -3739,8 +4132,8 @@ function walkFlowNodes(flow, flowPath) {
3739
4132
  if (slot === "branches") {
3740
4133
  if (!Array.isArray(value)) continue;
3741
4134
  value.forEach((branch, b) => {
3742
- if (!isRec13(branch)) return;
3743
- const branchName = strName14(branch.name) ?? `#${b}`;
4135
+ if (!isRec14(branch)) return;
4136
+ const branchName = strName15(branch.name) ?? `#${b}`;
3744
4137
  visitList(
3745
4138
  branch.nodes,
3746
4139
  `${path}.config.branches[${b}].nodes`,
@@ -3750,7 +4143,7 @@ function walkFlowNodes(flow, flowPath) {
3750
4143
  });
3751
4144
  continue;
3752
4145
  }
3753
- if (!isRec13(value)) continue;
4146
+ if (!isRec14(value)) continue;
3754
4147
  visitList(
3755
4148
  value.nodes,
3756
4149
  `${path}.config.${slot}.nodes`,
@@ -3991,7 +4384,7 @@ function asArray17(v) {
3991
4384
  }
3992
4385
  return [];
3993
4386
  }
3994
- function strName15(v) {
4387
+ function strName16(v) {
3995
4388
  return typeof v === "string" && v.length > 0 ? v : void 0;
3996
4389
  }
3997
4390
  function surfaceOf(v) {
@@ -4002,17 +4395,17 @@ function validateAiSurfaceAffinity(stack) {
4002
4395
  if (!stack || typeof stack !== "object") return findings;
4003
4396
  const skillsByName = /* @__PURE__ */ new Map();
4004
4397
  for (const skill of asArray17(stack.skills)) {
4005
- const n = strName15(skill.name);
4398
+ const n = strName16(skill.name);
4006
4399
  if (n) skillsByName.set(n, skill);
4007
4400
  }
4008
4401
  const agents = asArray17(stack.agents);
4009
4402
  for (let ai = 0; ai < agents.length; ai++) {
4010
4403
  const agent = agents[ai];
4011
- const agentName = strName15(agent.name) ?? `#${ai}`;
4404
+ const agentName = strName16(agent.name) ?? `#${ai}`;
4012
4405
  const agentSurface = surfaceOf(agent.surface);
4013
4406
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
4014
4407
  for (let si = 0; si < skillRefs.length; si++) {
4015
- const ref = strName15(skillRefs[si]);
4408
+ const ref = strName16(skillRefs[si]);
4016
4409
  if (!ref) continue;
4017
4410
  const skill = skillsByName.get(ref);
4018
4411
  if (!skill) continue;
@@ -4041,10 +4434,10 @@ function asArray18(v) {
4041
4434
  }
4042
4435
  return [];
4043
4436
  }
4044
- function strName16(v) {
4437
+ function strName17(v) {
4045
4438
  return typeof v === "string" && v.length > 0 ? v : void 0;
4046
4439
  }
4047
- function distance6(a, b) {
4440
+ function distance7(a, b) {
4048
4441
  const m = a.length;
4049
4442
  const n = b.length;
4050
4443
  if (m === 0) return n;
@@ -4060,14 +4453,14 @@ function distance6(a, b) {
4060
4453
  }
4061
4454
  return prev[n];
4062
4455
  }
4063
- function suggest7(target, known) {
4456
+ function suggest8(target, known) {
4064
4457
  for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
4065
4458
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
4066
4459
  }
4067
4460
  let best;
4068
4461
  let bestScore = Infinity;
4069
4462
  for (const candidate of known) {
4070
- const d = distance6(target, candidate);
4463
+ const d = distance7(target, candidate);
4071
4464
  if (d < bestScore) {
4072
4465
  bestScore = d;
4073
4466
  best = candidate;
@@ -4082,8 +4475,8 @@ function materialisesAsTool(action) {
4082
4475
  if (!ai || typeof ai !== "object") return false;
4083
4476
  const aiRec = ai;
4084
4477
  if (aiRec.exposed !== true) return false;
4085
- if (!strName16(aiRec.description)) return false;
4086
- const type = strName16(action.type);
4478
+ if (!strName17(aiRec.description)) return false;
4479
+ const type = strName17(action.type);
4087
4480
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
4088
4481
  if (type === "script") return Boolean(action.target || action.body);
4089
4482
  return Boolean(action.target);
@@ -4091,12 +4484,12 @@ function materialisesAsTool(action) {
4091
4484
  function collectToolUniverse(stack) {
4092
4485
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
4093
4486
  for (const tool of asArray18(stack.tools)) {
4094
- const n = strName16(tool.name);
4487
+ const n = strName17(tool.name);
4095
4488
  if (n) universe.add(n);
4096
4489
  }
4097
4490
  const addActionFamily = (actions) => {
4098
4491
  for (const action of asArray18(actions)) {
4099
- const n = strName16(action.name);
4492
+ const n = strName17(action.name);
4100
4493
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
4101
4494
  }
4102
4495
  };
@@ -4110,7 +4503,7 @@ function collectUnexposedActionNames(stack) {
4110
4503
  const names = /* @__PURE__ */ new Set();
4111
4504
  const scan = (actions) => {
4112
4505
  for (const action of asArray18(actions)) {
4113
- const n = strName16(action.name);
4506
+ const n = strName17(action.name);
4114
4507
  if (n && !materialisesAsTool(action)) names.add(n);
4115
4508
  }
4116
4509
  };
@@ -4136,10 +4529,10 @@ function validateAiToolReferences(stack) {
4136
4529
  const skills = asArray18(stack.skills);
4137
4530
  for (let si = 0; si < skills.length; si++) {
4138
4531
  const skill = skills[si];
4139
- const skillName = strName16(skill.name) ?? `#${si}`;
4532
+ const skillName = strName17(skill.name) ?? `#${si}`;
4140
4533
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
4141
4534
  for (let ti = 0; ti < refs.length; ti++) {
4142
- const ref = strName16(refs[ti]);
4535
+ const ref = strName17(refs[ti]);
4143
4536
  if (!ref || resolves(ref)) continue;
4144
4537
  const isPattern = ref.endsWith("*");
4145
4538
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -4148,7 +4541,7 @@ function validateAiToolReferences(stack) {
4148
4541
  rule: AI_SKILL_TOOL_UNRESOLVED,
4149
4542
  where: `skill "${skillName}" \xB7 tools`,
4150
4543
  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),
4544
+ 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
4545
  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
4546
  });
4154
4547
  }
@@ -4166,7 +4559,7 @@ function asArray19(v) {
4166
4559
  }
4167
4560
  return [];
4168
4561
  }
4169
- function strName17(v) {
4562
+ function strName18(v) {
4170
4563
  return typeof v === "string" && v.length > 0 ? v : void 0;
4171
4564
  }
4172
4565
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -4176,7 +4569,7 @@ function validateAiAgentAuthoring(stack) {
4176
4569
  const agents = asArray19(stack.agents);
4177
4570
  for (let ai = 0; ai < agents.length; ai++) {
4178
4571
  const agent = agents[ai];
4179
- const name = strName17(agent.name) ?? `#${ai}`;
4572
+ const name = strName18(agent.name) ?? `#${ai}`;
4180
4573
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
4181
4574
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
4182
4575
  findings.push({
@@ -4192,9 +4585,9 @@ function validateAiAgentAuthoring(stack) {
4192
4585
  const apps = asArray19(stack.apps);
4193
4586
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
4194
4587
  const app = apps[appIdx];
4195
- const defaultAgent = strName17(app.defaultAgent);
4588
+ const defaultAgent = strName18(app.defaultAgent);
4196
4589
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
4197
- const appName = strName17(app.name) ?? `#${appIdx}`;
4590
+ const appName = strName18(app.name) ?? `#${appIdx}`;
4198
4591
  findings.push({
4199
4592
  severity: "warning",
4200
4593
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -4210,6 +4603,38 @@ function validateAiAgentAuthoring(stack) {
4210
4603
  // src/validate-hook-body-writes.ts
4211
4604
  var import_node_module = require("module");
4212
4605
  var import_shared = require("@objectstack/spec/shared");
4606
+
4607
+ // src/checked-parse.ts
4608
+ function createSourceFileChecked(tsc, fileName, source, options) {
4609
+ const sourceFile = tsc.createSourceFile(
4610
+ fileName,
4611
+ source,
4612
+ options.target,
4613
+ options.setParentNodes,
4614
+ options.scriptKind
4615
+ );
4616
+ const diagnostics = sourceFile.parseDiagnostics;
4617
+ if (!diagnostics || diagnostics.length === 0) return { sourceFile };
4618
+ const first = diagnostics[0];
4619
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
4620
+ const offset = options.synthesizedLinesBefore ?? 0;
4621
+ return {
4622
+ sourceFile,
4623
+ failure: {
4624
+ message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
4625
+ line: Math.max(1, line + 1 - offset),
4626
+ column: character + 1,
4627
+ count: diagnostics.length
4628
+ }
4629
+ };
4630
+ }
4631
+ function describeParseFailure(failure) {
4632
+ const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
4633
+ return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
4634
+ }
4635
+ var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
4636
+
4637
+ // src/validate-hook-body-writes.ts
4213
4638
  var import_meta = {};
4214
4639
  var cachedTs = null;
4215
4640
  function loadTypeScript() {
@@ -4225,6 +4650,8 @@ function loadTypeScript() {
4225
4650
  return cachedTs;
4226
4651
  }
4227
4652
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
4653
+ var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
4654
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
4228
4655
  var HOOK_BODY_WRITE_PATTERNS = [
4229
4656
  {
4230
4657
  id: "input-property-assign",
@@ -4291,13 +4718,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
4291
4718
  "owner",
4292
4719
  "record_type"
4293
4720
  ]);
4294
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4721
+ function unprovisionedAnchorWriteConsequence() {
4722
+ 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).`;
4723
+ }
4724
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4295
4725
  function asArray20(v) {
4296
- if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4297
- if (isRec14(v)) {
4726
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4727
+ if (isRec15(v)) {
4298
4728
  return Object.entries(v).map(([name, def]) => ({
4299
4729
  name,
4300
- ...isRec14(def) ? def : {}
4730
+ ...isRec15(def) ? def : {}
4301
4731
  }));
4302
4732
  }
4303
4733
  return [];
@@ -4320,23 +4750,23 @@ function judgeableFieldsOf(index, objectName) {
4320
4750
  if (!declared || declared.size === 0) return void 0;
4321
4751
  return declared;
4322
4752
  }
4323
- function extractHookBodyWrites(source) {
4324
- return extractHookBodyWriteSet(source).writes;
4325
- }
4326
4753
  function extractHookBodyWriteSet(source) {
4327
4754
  if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
4328
4755
  return { writes: [], ctxRecordEscapes: false };
4329
4756
  }
4330
4757
  const tsc = loadTypeScript();
4331
- const sf = tsc.createSourceFile(
4758
+ const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
4759
+ tsc,
4332
4760
  "hook-body.ts",
4333
4761
  `async function __body(ctx) {
4334
4762
  ${source}
4335
4763
  }`,
4336
- tsc.ScriptTarget.Latest,
4337
- /* setParentNodes */
4338
- false,
4339
- tsc.ScriptKind.TS
4764
+ {
4765
+ target: tsc.ScriptTarget.Latest,
4766
+ setParentNodes: false,
4767
+ scriptKind: tsc.ScriptKind.TS,
4768
+ synthesizedLinesBefore: 1
4769
+ }
4340
4770
  );
4341
4771
  const writes = [];
4342
4772
  const recordRefs = [];
@@ -4433,7 +4863,8 @@ ${source}
4433
4863
  visit(sf);
4434
4864
  return {
4435
4865
  writes,
4436
- ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
4866
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
4867
+ ...parseFailure ? { parseFailure } : {}
4437
4868
  };
4438
4869
  }
4439
4870
  function validateHookBodyWrites(stack) {
@@ -4441,15 +4872,28 @@ function validateHookBodyWrites(stack) {
4441
4872
  const hooks = asArray20(stack.hooks);
4442
4873
  if (hooks.length === 0) return findings;
4443
4874
  let objectFields = null;
4875
+ let anchors = null;
4444
4876
  hooks.forEach((hook, hookIndex) => {
4445
4877
  const body = hook.body;
4446
- if (!isRec14(body) || body.language !== "js") return;
4878
+ if (!isRec15(body) || body.language !== "js") return;
4447
4879
  const source = body.source;
4448
4880
  if (typeof source !== "string" || source.trim() === "") return;
4449
- const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4881
+ const extracted = extractHookBodyWriteSet(source);
4882
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4883
+ if (extracted.parseFailure) {
4884
+ findings.push({
4885
+ severity: "warning",
4886
+ rule: HOOK_BODY_SOURCE_UNPARSEABLE,
4887
+ where: `hook "${hookName}" \u203A body`,
4888
+ path: `hooks[${hookIndex}].body.source`,
4889
+ message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
4890
+ hint: PARSE_FAILURE_HINT
4891
+ });
4892
+ }
4893
+ const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4450
4894
  if (writes.length === 0) return;
4451
4895
  objectFields ?? (objectFields = indexObjectFields2(stack));
4452
- const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4896
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4453
4897
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
4454
4898
  (o) => typeof o === "string" && o.trim() !== ""
4455
4899
  );
@@ -4463,7 +4907,20 @@ function validateHookBodyWrites(stack) {
4463
4907
  if (reported.has(dedupeKey)) continue;
4464
4908
  if (w.object === void 0) {
4465
4909
  if (!inputJudgeable) continue;
4466
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
4910
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4911
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
4912
+ reported.add(dedupeKey);
4913
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
4914
+ findings.push({
4915
+ severity: "warning",
4916
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4917
+ where,
4918
+ path,
4919
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
4920
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
4921
+ });
4922
+ continue;
4923
+ }
4467
4924
  if (targetSets.some((s) => s.has(w.field))) continue;
4468
4925
  reported.add(dedupeKey);
4469
4926
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -4479,7 +4936,20 @@ function validateHookBodyWrites(stack) {
4479
4936
  } else {
4480
4937
  const known = judgeableFieldsOf(objectFields, w.object);
4481
4938
  if (!known) continue;
4482
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
4939
+ if (known.has(w.field)) continue;
4940
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4941
+ if (!anchors.get(w.object)?.has(w.field)) continue;
4942
+ reported.add(dedupeKey);
4943
+ findings.push({
4944
+ severity: "warning",
4945
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4946
+ where,
4947
+ path,
4948
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
4949
+ hint: unprovisionedAnchorHint(w.object, w.field)
4950
+ });
4951
+ continue;
4952
+ }
4483
4953
  reported.add(dedupeKey);
4484
4954
  findings.push({
4485
4955
  severity: "warning",
@@ -4508,19 +4978,21 @@ function fixHint(field, declared) {
4508
4978
  var import_shared2 = require("@objectstack/spec/shared");
4509
4979
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
4510
4980
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
4981
+ var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
4982
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
4511
4983
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
4512
4984
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
4513
4985
  var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
4514
4986
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
4515
4987
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
4516
4988
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
4517
- var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4989
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4518
4990
  function asArray21(v) {
4519
- if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4520
- if (isRec15(v)) {
4991
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4992
+ if (isRec16(v)) {
4521
4993
  return Object.entries(v).map(([name, def]) => ({
4522
4994
  name,
4523
- ...isRec15(def) ? def : {}
4995
+ ...isRec16(def) ? def : {}
4524
4996
  }));
4525
4997
  }
4526
4998
  return [];
@@ -4538,7 +5010,7 @@ function collectActionBodies(stack) {
4538
5010
  const type = typeof action.type === "string" ? action.type : "script";
4539
5011
  if (type !== "script") return;
4540
5012
  const body = action.body;
4541
- if (!isRec15(body) || body.language !== "js") return;
5013
+ if (!isRec16(body) || body.language !== "js") return;
4542
5014
  const source = body.source;
4543
5015
  if (typeof source !== "string" || source.trim() === "") return;
4544
5016
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4557,17 +5029,28 @@ function collectActionBodies(stack) {
4557
5029
  }
4558
5030
  function validateActionBodyWrites(stack) {
4559
5031
  const findings = [];
4560
- if (!isRec15(stack)) return findings;
5032
+ if (!isRec16(stack)) return findings;
4561
5033
  const sites = collectActionBodies(stack);
4562
5034
  if (sites.length === 0) return findings;
4563
5035
  let objectFields = null;
5036
+ let anchors = null;
4564
5037
  for (const site of sites) {
4565
5038
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
4566
- const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
5039
+ const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
4567
5040
  const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
4568
5041
  const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
4569
- if (writes.length === 0 && recordWrites.length === 0) continue;
4570
5042
  const where = `action "${site.name}" \u203A body`;
5043
+ if (parseFailure) {
5044
+ findings.push({
5045
+ severity: "warning",
5046
+ rule: ACTION_BODY_SOURCE_UNPARSEABLE,
5047
+ where,
5048
+ path: site.path,
5049
+ message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
5050
+ hint: PARSE_FAILURE_HINT
5051
+ });
5052
+ }
5053
+ if (writes.length === 0 && recordWrites.length === 0) continue;
4571
5054
  if (recordWrites.length > 0 && !ctxRecordEscapes) {
4572
5055
  const reportedFields = /* @__PURE__ */ new Set();
4573
5056
  for (const w of recordWrites) {
@@ -4585,6 +5068,7 @@ function validateActionBodyWrites(stack) {
4585
5068
  }
4586
5069
  if (writes.length === 0) continue;
4587
5070
  objectFields ?? (objectFields = indexObjectFields2(stack));
5071
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4588
5072
  const reported = /* @__PURE__ */ new Set();
4589
5073
  for (const w of writes) {
4590
5074
  if (w.object === void 0) continue;
@@ -4592,7 +5076,20 @@ function validateActionBodyWrites(stack) {
4592
5076
  if (reported.has(dedupeKey)) continue;
4593
5077
  const known = judgeableFieldsOf(objectFields, w.object);
4594
5078
  if (!known) continue;
4595
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
5079
+ if (known.has(w.field)) continue;
5080
+ if (IMPLICIT_FIELDS2.has(w.field)) {
5081
+ if (!anchors.get(w.object)?.has(w.field)) continue;
5082
+ reported.add(dedupeKey);
5083
+ findings.push({
5084
+ severity: "warning",
5085
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
5086
+ where,
5087
+ path: site.path,
5088
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
5089
+ hint: unprovisionedAnchorHint(w.object, w.field)
5090
+ });
5091
+ continue;
5092
+ }
4596
5093
  reported.add(dedupeKey);
4597
5094
  findings.push({
4598
5095
  severity: "warning",
@@ -4614,14 +5111,15 @@ function fixHint2(field, declared) {
4614
5111
  // src/validate-flow-node-writes.ts
4615
5112
  var import_shared3 = require("@objectstack/spec/shared");
4616
5113
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
5114
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
4617
5115
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4618
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5116
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4619
5117
  function asArray22(v) {
4620
- if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4621
- if (isRec16(v)) {
5118
+ if (Array.isArray(v)) return v.filter((x) => isRec17(x));
5119
+ if (isRec17(v)) {
4622
5120
  return Object.entries(v).map(([name, def]) => ({
4623
5121
  name,
4624
- ...isRec16(def) ? def : {}
5122
+ ...isRec17(def) ? def : {}
4625
5123
  }));
4626
5124
  }
4627
5125
  return [];
@@ -4634,30 +5132,44 @@ function readLiteralObjectName(config) {
4634
5132
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4635
5133
  function validateFlowNodeWrites(stack) {
4636
5134
  const findings = [];
4637
- if (!isRec16(stack)) return findings;
5135
+ if (!isRec17(stack)) return findings;
4638
5136
  const flows = asArray22(stack.flows);
4639
5137
  if (flows.length === 0) return findings;
4640
5138
  let objectFields = null;
5139
+ let anchors = null;
4641
5140
  flows.forEach((flow, flowIndex) => {
4642
5141
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
4643
5142
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4644
5143
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4645
5144
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4646
- const config = isRec16(node.config) ? node.config : void 0;
5145
+ const config = isRec17(node.config) ? node.config : void 0;
4647
5146
  if (!config) return;
4648
5147
  const fields = config.fields;
4649
- if (!isRec16(fields)) return;
5148
+ if (!isRec17(fields)) return;
4650
5149
  const written = Object.keys(fields);
4651
5150
  if (written.length === 0) return;
4652
5151
  const objectName = readLiteralObjectName(config);
4653
5152
  if (!objectName) return;
4654
5153
  objectFields ?? (objectFields = indexObjectFields2(stack));
5154
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4655
5155
  const known = judgeableFieldsOf(objectFields, objectName);
4656
5156
  if (!known) return;
4657
5157
  const nodeName = flowNodeLabel(node, walkIndex);
4658
5158
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
4659
5159
  for (const fieldName of written) {
4660
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
5160
+ if (known.has(fieldName)) continue;
5161
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
5162
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
5163
+ findings.push({
5164
+ severity: "warning",
5165
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
5166
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
5167
+ path: `${nodePath}.config.fields.${fieldName}`,
5168
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
5169
+ hint: unprovisionedAnchorHint(objectName, fieldName)
5170
+ });
5171
+ continue;
5172
+ }
4661
5173
  if (fieldName.includes(".")) continue;
4662
5174
  findings.push({
4663
5175
  severity: "error",
@@ -4770,15 +5282,15 @@ function validateReadonlyFlowWrites(stack) {
4770
5282
 
4771
5283
  // src/validate-react-page-props.ts
4772
5284
  var import_node_module2 = require("module");
4773
- var import_ui2 = require("@objectstack/spec/ui");
4774
- var import_data8 = require("@objectstack/spec/data");
5285
+ var import_ui3 = require("@objectstack/spec/ui");
5286
+ var import_data10 = require("@objectstack/spec/data");
4775
5287
 
4776
5288
  // src/zod-issue-format.ts
4777
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5289
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4778
5290
  var valueAtPath = (root, path) => {
4779
5291
  let cur = root;
4780
5292
  for (const key of path) {
4781
- if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
5293
+ if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
4782
5294
  cur = cur[key];
4783
5295
  }
4784
5296
  return cur;
@@ -4826,7 +5338,7 @@ function loadTypeScript2() {
4826
5338
  }
4827
5339
  var asArray24 = (v) => Array.isArray(v) ? v : [];
4828
5340
  var BLOCKS = new Map(
4829
- import_ui2.REACT_BLOCKS.map((b) => [
5341
+ import_ui3.REACT_BLOCKS.map((b) => [
4830
5342
  b.tag,
4831
5343
  {
4832
5344
  requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
@@ -4911,6 +5423,7 @@ function filterAttrValue(tsc, sf, attr) {
4911
5423
  };
4912
5424
  return perPosition(init.expression);
4913
5425
  }
5426
+ var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
4914
5427
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
4915
5428
  var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
4916
5429
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
@@ -4918,7 +5431,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4918
5431
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4919
5432
  function checkChartDrillDown(raw, push2) {
4920
5433
  if (raw === void 0 || raw === NOT_STATIC) return;
4921
- if (!isRec18(raw)) {
5434
+ if (!isRec19(raw)) {
4922
5435
  push2(
4923
5436
  "error",
4924
5437
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4927,7 +5440,7 @@ function checkChartDrillDown(raw, push2) {
4927
5440
  );
4928
5441
  return;
4929
5442
  }
4930
- const parsed = import_ui2.ChartDrillDownSchema.safeParse(raw);
5443
+ const parsed = import_ui3.ChartDrillDownSchema.safeParse(raw);
4931
5444
  if (parsed.success) return;
4932
5445
  for (const issue of parsed.error.issues) {
4933
5446
  const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
@@ -4941,7 +5454,7 @@ function checkChartDrillDown(raw, push2) {
4941
5454
  }
4942
5455
  function checkChartAggregate(raw, push2) {
4943
5456
  if (raw === void 0 || raw === NOT_STATIC) return;
4944
- if (!isRec18(raw)) {
5457
+ if (!isRec19(raw)) {
4945
5458
  push2(
4946
5459
  "error",
4947
5460
  REACT_CHART_AGGREGATE_INVALID,
@@ -4959,7 +5472,7 @@ function checkChartAggregate(raw, push2) {
4959
5472
  "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
5473
  );
4961
5474
  }
4962
- const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
5475
+ const parsed = import_ui3.ChartAggregateSchema.safeParse(raw);
4963
5476
  if (parsed.success) return;
4964
5477
  for (const issue of parsed.error.issues) {
4965
5478
  if (groupByAbsent && issue.path[0] === "groupBy") continue;
@@ -4972,7 +5485,7 @@ function checkChartAggregate(raw, push2) {
4972
5485
  );
4973
5486
  }
4974
5487
  }
4975
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5488
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4976
5489
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4977
5490
  function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4978
5491
  const { values, where, path } = attrs;
@@ -4982,11 +5495,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
4982
5495
  const aggregate = values.get("aggregate");
4983
5496
  checkChartAggregate(aggregate, push2);
4984
5497
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4985
- if (!isRec18(aggregate)) return;
5498
+ if (!isRec19(aggregate)) return;
4986
5499
  const fn = strOf(aggregate.function);
4987
5500
  const field = strOf(aggregate.field);
4988
5501
  const groupBy = aggregate.groupBy;
4989
- const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
5502
+ const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
4990
5503
  const objectName = strOf(values.get("objectName"));
4991
5504
  const known = objectName ? objectFields.get(objectName) : void 0;
4992
5505
  if (objectName && known) {
@@ -5015,7 +5528,7 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
5015
5528
  fieldRef(field, "field");
5016
5529
  fieldRef(groupByField, "groupBy");
5017
5530
  }
5018
- const keys = (0, import_ui2.chartAggregateResultKeys)({ field, function: fn, groupBy });
5531
+ const keys = (0, import_ui3.chartAggregateResultKeys)({ field, function: fn, groupBy });
5019
5532
  const columns = [keys.category, keys.value].filter((k) => !!k);
5020
5533
  if (columns.length === 0) return;
5021
5534
  const axisRef = (name, prop) => {
@@ -5030,18 +5543,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
5030
5543
  );
5031
5544
  };
5032
5545
  const xAxisRaw = values.get("xAxis");
5033
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5546
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5034
5547
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
5035
5548
  axisRef(categoryAxis, categoryProp);
5036
5549
  const yAxisRaw = values.get("yAxis");
5037
5550
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
5038
5551
  for (const a of yAxisList) {
5039
- axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
5552
+ axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
5040
5553
  }
5041
5554
  const series = values.get("series");
5042
5555
  if (Array.isArray(series)) {
5043
5556
  for (const s of series) {
5044
- if (!isRec18(s)) continue;
5557
+ if (!isRec19(s)) continue;
5045
5558
  const dataKey = strOf(s.dataKey);
5046
5559
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
5047
5560
  }
@@ -5081,7 +5594,7 @@ var REACT_FIELD_SPECS = {
5081
5594
  };
5082
5595
  var PATH_SEP = " \u203A ";
5083
5596
  var SCHEMA_TYPE_BY_TAG = new Map(
5084
- import_ui2.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
5597
+ import_ui3.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
5085
5598
  );
5086
5599
  var FILTER_PROPS = new Set(
5087
5600
  Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
@@ -5097,7 +5610,7 @@ function subformFieldRefs(value, basePath) {
5097
5610
  if (!Array.isArray(value)) return { child, parent };
5098
5611
  for (let i = 0; i < value.length; i++) {
5099
5612
  const sub = value[i];
5100
- if (!isRec18(sub)) continue;
5613
+ if (!isRec19(sub)) continue;
5101
5614
  const at = (key) => `${basePath}[${i}].${key}`;
5102
5615
  child.push({
5103
5616
  objectName: strOf(sub.childObject),
@@ -5122,7 +5635,7 @@ function filterFieldRefs(node, basePath, out) {
5122
5635
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
5123
5636
  return;
5124
5637
  }
5125
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5638
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data10.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5126
5639
  out.push({ name: head, path: `${basePath}[0]` });
5127
5640
  }
5128
5641
  }
@@ -5142,20 +5655,20 @@ function reactFieldRefs(spec, values, basePath) {
5142
5655
  }
5143
5656
  for (const key of spec.nestedFields ?? []) {
5144
5657
  const v = readable(key);
5145
- if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5658
+ if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5146
5659
  }
5147
5660
  for (const key of spec.sections ?? []) {
5148
5661
  const v = readable(key);
5149
5662
  if (!Array.isArray(v)) continue;
5150
5663
  for (let i = 0; i < v.length; i++) {
5151
5664
  const section = v[i];
5152
- if (!isRec18(section)) continue;
5665
+ if (!isRec19(section)) continue;
5153
5666
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
5154
5667
  }
5155
5668
  }
5156
5669
  for (const key of spec.keyedByField ?? []) {
5157
5670
  const v = readable(key);
5158
- if (!isRec18(v)) continue;
5671
+ if (!isRec19(v)) continue;
5159
5672
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
5160
5673
  }
5161
5674
  for (const key of spec.filterArrays ?? []) {
@@ -5212,7 +5725,7 @@ function recordContextFinding(tag, schemaType, where, path) {
5212
5725
  where,
5213
5726
  path,
5214
5727
  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}`
5728
+ hint: `On a react page bind the record yourself: ${import_ui3.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
5216
5729
  };
5217
5730
  }
5218
5731
  function localComponentNames(tsc, sf) {
@@ -5239,11 +5752,20 @@ function validateReactPageProps(stack) {
5239
5752
  if (typeof source !== "string" || source.trim() === "") continue;
5240
5753
  const name = String(page.name ?? `#${p}`);
5241
5754
  const tsc = loadTypeScript2();
5242
- let sf;
5243
- try {
5244
- sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
5245
- } catch {
5246
- continue;
5755
+ const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
5756
+ target: tsc.ScriptTarget.Latest,
5757
+ setParentNodes: true,
5758
+ scriptKind: tsc.ScriptKind.TSX
5759
+ });
5760
+ if (failure) {
5761
+ findings.push({
5762
+ severity: "warning",
5763
+ rule: REACT_PAGE_SOURCE_UNPARSEABLE,
5764
+ where: `page "${name}"`,
5765
+ path: `pages[${p}].source`,
5766
+ message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
5767
+ hint: PARSE_FAILURE_HINT
5768
+ });
5247
5769
  }
5248
5770
  const locals = localComponentNames(tsc, sf);
5249
5771
  const visit = (node) => {
@@ -5251,7 +5773,7 @@ function validateReactPageProps(stack) {
5251
5773
  const tag = node.tagName.getText(sf);
5252
5774
  const where = `page "${name}" \u203A <${tag}>`;
5253
5775
  const path = `pages[${p}].source`;
5254
- const recordType = import_ui2.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
5776
+ const recordType = import_ui3.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
5255
5777
  if (recordType && !locals.has(tag)) {
5256
5778
  findings.push(recordContextFinding(tag, recordType, where, path));
5257
5779
  tsc.forEachChild(node, visit);
@@ -5278,7 +5800,7 @@ function validateReactPageProps(stack) {
5278
5800
  }
5279
5801
  if (tag === "Block") {
5280
5802
  const blockType = strOf(values.get("type"));
5281
- if (blockType && (0, import_ui2.isRecordContextBlockType)(blockType)) {
5803
+ if (blockType && (0, import_ui3.isRecordContextBlockType)(blockType)) {
5282
5804
  findings.push(recordContextFinding(tag, blockType, where, path));
5283
5805
  tsc.forEachChild(node, visit);
5284
5806
  return;
@@ -5322,7 +5844,12 @@ function validateReactPageProps(stack) {
5322
5844
  searchTargets,
5323
5845
  where,
5324
5846
  `${path} \u203A searchableFields`,
5325
- "searchableFields"
5847
+ "searchableFields",
5848
+ // A `<ListView>` prop is a view-level narrowing — the checker's
5849
+ // default, spelled out here because the #8404 provenance index
5850
+ // follows it positionally.
5851
+ "narrowing",
5852
+ unprovisionedAnchors
5326
5853
  )
5327
5854
  );
5328
5855
  }
@@ -5341,9 +5868,26 @@ function validateReactPageProps(stack) {
5341
5868
  }
5342
5869
 
5343
5870
  // src/reference-integrity-suite.ts
5871
+ var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
5344
5872
  var REFERENCE_INTEGRITY_RULES = [
5345
5873
  { name: "validateObjectReferences", run: validateObjectReferences },
5346
- { name: "validateSearchableFields", run: validateSearchableFields },
5874
+ // [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
5875
+ // both judge a LIST VIEW's field references, and a standalone list view is
5876
+ // written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
5877
+ // an MCP/AI author has. Their walks read the flattened overlay shape that
5878
+ // door carries (see each rule's `views[]` self rung), and they resolve only
5879
+ // against `stack.objects`, which the per-write snapshot DOES carry — so the
5880
+ // crossing has no missing-collection false-positive channel. Measured over
5881
+ // the shipped view corpus before crossing (0 refusals; population in the
5882
+ // #9313 PR).
5883
+ { name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
5884
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
5885
+ // name written in metadata, resolved against the object's declared fields. It
5886
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
5887
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
5888
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
5889
+ // the refusal is the whole view, on every load, traced to nothing.
5890
+ { name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
5347
5891
  { name: "validateActionNameRefs", run: validateActionNameRefs },
5348
5892
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5349
5893
  { name: "validateChartBindings", run: validateChartBindings },
@@ -5449,23 +5993,25 @@ var REFERENCE_INTEGRITY_RULES = [
5449
5993
  // — only a page that is actually `kind:'react'` loads the compiler.
5450
5994
  { name: "validateReactPageProps", run: validateReactPageProps }
5451
5995
  ];
5452
- function validateReferenceIntegrity(stack) {
5996
+ function validateReferenceIntegrity(stack, options) {
5453
5997
  const findings = [];
5998
+ const writeType = options?.runtimeWriteType;
5454
5999
  for (const rule of REFERENCE_INTEGRITY_RULES) {
6000
+ if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
5455
6001
  findings.push(...rule.run(stack));
5456
6002
  }
5457
6003
  return findings;
5458
6004
  }
5459
6005
 
5460
6006
  // src/validate-component-props.ts
5461
- var import_ui3 = require("@objectstack/spec/ui");
6007
+ var import_ui4 = require("@objectstack/spec/ui");
5462
6008
  var import_spec2 = require("@objectstack/spec");
5463
6009
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5464
6010
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
5465
- function isRec19(v) {
6011
+ function isRec20(v) {
5466
6012
  return !!v && typeof v === "object" && !Array.isArray(v);
5467
6013
  }
5468
- function strName18(v) {
6014
+ function strName19(v) {
5469
6015
  return typeof v === "string" && v.length > 0 ? v : void 0;
5470
6016
  }
5471
6017
  function asArray25(v) {
@@ -5475,12 +6021,12 @@ function asArray25(v) {
5475
6021
  }
5476
6022
  return [];
5477
6023
  }
5478
- var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
6024
+ var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
5479
6025
  var DATASOURCE_SUPPLIED_PROP = "object";
5480
6026
  function suppliedByDataSource(issue, component) {
5481
6027
  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;
6028
+ const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
6029
+ return strName19(dataSource?.object) !== void 0;
5484
6030
  }
5485
6031
  function unrecognizedKeysFromUnionArm(issue) {
5486
6032
  if (issue.code !== "invalid_union") return void 0;
@@ -5497,18 +6043,18 @@ function unrecognizedKeysFromUnionArm(issue) {
5497
6043
  }
5498
6044
  function validateComponentProps(stack) {
5499
6045
  const findings = [];
5500
- if (!isRec19(stack)) return findings;
6046
+ if (!isRec20(stack)) return findings;
5501
6047
  const pages = asArray25(stack.pages);
5502
6048
  for (let pi = 0; pi < pages.length; pi++) {
5503
6049
  const page = pages[pi];
5504
- if (!isRec19(page)) continue;
5505
- const pageName = strName18(page.name) ?? `#${pi}`;
6050
+ if (!isRec20(page)) continue;
6051
+ const pageName = strName19(page.name) ?? `#${pi}`;
5506
6052
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5507
- const type = strName18(component.type);
6053
+ const type = strName19(component.type);
5508
6054
  if (!type) continue;
5509
6055
  const schema = PROPS_SCHEMAS[type];
5510
6056
  if (!schema) continue;
5511
- const props = isRec19(component.properties) ? component.properties : void 0;
6057
+ const props = isRec20(component.properties) ? component.properties : void 0;
5512
6058
  if (!props) continue;
5513
6059
  const where = `page "${pageName}" \xB7 ${type}`;
5514
6060
  const base = `${path}.properties`;
@@ -6504,7 +7050,7 @@ function validateApprovalApprovers(stack) {
6504
7050
  }
6505
7051
 
6506
7052
  // src/validate-record-title.ts
6507
- var import_data9 = require("@objectstack/spec/data");
7053
+ var import_data11 = require("@objectstack/spec/data");
6508
7054
  var TITLE_FORMAT_RETIRED = "title-format-retired";
6509
7055
  var TITLE_UNRESOLVABLE = "title-unresolvable";
6510
7056
  function asArray33(v) {
@@ -6532,7 +7078,7 @@ function validateRecordTitle(stack) {
6532
7078
  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
7079
  });
6534
7080
  }
6535
- const completeness = (0, import_data9.objectTitleCompleteness)(obj);
7081
+ const completeness = (0, import_data11.objectTitleCompleteness)(obj);
6536
7082
  if (completeness.status === "none") {
6537
7083
  findings.push({
6538
7084
  severity: "warning",
@@ -6679,10 +7225,10 @@ function asArray35(v) {
6679
7225
  }
6680
7226
  return [];
6681
7227
  }
6682
- function isRec20(v) {
7228
+ function isRec21(v) {
6683
7229
  return !!v && typeof v === "object" && !Array.isArray(v);
6684
7230
  }
6685
- function strName19(v) {
7231
+ function strName20(v) {
6686
7232
  return typeof v === "string" && v.length > 0 ? v : void 0;
6687
7233
  }
6688
7234
  function fieldNameOf(entry) {
@@ -6703,7 +7249,7 @@ function validateFormLayout(stack) {
6703
7249
  objectFields.set(name, new Set(fields));
6704
7250
  }
6705
7251
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6706
- const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
7252
+ const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
6707
7253
  const containerObject = viewObjectName(view);
6708
7254
  for (const site of formViewSites(view, viewPath)) {
6709
7255
  const objName = viewObjectName(site.view) ?? containerObject;
@@ -6713,7 +7259,7 @@ function validateFormLayout(stack) {
6713
7259
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6714
7260
  for (let s = 0; s < sections.length; s++) {
6715
7261
  const sec = sections[s];
6716
- const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
7262
+ const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6717
7263
  for (let f = 0; f < secFields.length; f++) {
6718
7264
  const entry = secFields[f];
6719
7265
  const fname = fieldNameOf(entry);
@@ -6728,7 +7274,7 @@ function validateFormLayout(stack) {
6728
7274
  hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6729
7275
  });
6730
7276
  }
6731
- const colSpan = isRec20(entry) ? entry.colSpan : void 0;
7277
+ const colSpan = isRec21(entry) ? entry.colSpan : void 0;
6732
7278
  if (colSpan != null) {
6733
7279
  findings.push({
6734
7280
  severity: "warning",
@@ -6886,12 +7432,12 @@ function bareRhsOnlyIdentifiers(ast) {
6886
7432
  for (const name of elsewhere) rhs.delete(name);
6887
7433
  return rhs;
6888
7434
  }
6889
- function isRec21(v) {
7435
+ function isRec22(v) {
6890
7436
  return !!v && typeof v === "object" && !Array.isArray(v);
6891
7437
  }
6892
7438
  function schemaIdOf(view) {
6893
7439
  const data = view.data;
6894
- if (!isRec21(data)) return void 0;
7440
+ if (!isRec22(data)) return void 0;
6895
7441
  if (data.provider !== "schema") return void 0;
6896
7442
  return typeof data.schemaId === "string" ? data.schemaId : void 0;
6897
7443
  }
@@ -7263,7 +7809,7 @@ function predicateSource2(v) {
7263
7809
  }
7264
7810
  return void 0;
7265
7811
  }
7266
- function isRec22(v) {
7812
+ function isRec23(v) {
7267
7813
  return !!v && typeof v === "object" && !Array.isArray(v);
7268
7814
  }
7269
7815
  function checkPredicate(source, scope, where, path, findings) {
@@ -7334,7 +7880,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
7334
7880
  if (!Array.isArray(entries) || depth > 12) return;
7335
7881
  for (let i = 0; i < entries.length; i++) {
7336
7882
  const entry = entries[i];
7337
- if (!isRec22(entry)) continue;
7883
+ if (!isRec23(entry)) continue;
7338
7884
  const path = `${base}[${i}]`;
7339
7885
  for (const key of PREDICATE_KEYS) {
7340
7886
  const source = predicateSource2(entry[key]);
@@ -7368,7 +7914,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
7368
7914
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7369
7915
  for (let s = 0; s < sections.length; s++) {
7370
7916
  const section = sections[s];
7371
- if (!isRec22(section)) continue;
7917
+ if (!isRec23(section)) continue;
7372
7918
  const sectionPath = `${site.path}.${bucket}[${s}]`;
7373
7919
  for (const key of PREDICATE_KEYS) {
7374
7920
  const source = predicateSource2(section[key]);
@@ -7652,6 +8198,7 @@ function validateSecurityPosture(stack, opts) {
7652
8198
  }
7653
8199
  }
7654
8200
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
8201
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
7655
8202
  const nowMs = opts?.nowMs ?? Date.now();
7656
8203
  for (const [i, seed] of asArray36(stack.data).entries()) {
7657
8204
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -7675,7 +8222,7 @@ function validateSecurityPosture(stack, opts) {
7675
8222
  }
7676
8223
  }
7677
8224
  const delegatedFrom = rec.delegated_from;
7678
- if (delegatedFrom != null && delegatedFrom !== "") {
8225
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
7679
8226
  const reason = rec.reason;
7680
8227
  if (typeof reason !== "string" || reason.trim().length === 0) {
7681
8228
  findings.push({
@@ -7841,6 +8388,8 @@ function validateOrgAxisRedLines(stack) {
7841
8388
  var import_formula6 = require("@objectstack/formula");
7842
8389
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
7843
8390
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
8391
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
8392
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
7844
8393
  function asArray38(v) {
7845
8394
  if (Array.isArray(v)) return v;
7846
8395
  if (v && typeof v === "object") {
@@ -7865,10 +8414,67 @@ function sourceOf(condition) {
7865
8414
  return str2(input?.source);
7866
8415
  }
7867
8416
  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).";
8417
+ function effectiveSharingModelOf(obj) {
8418
+ const m = obj.sharingModel;
8419
+ if (m === "private") return "private";
8420
+ if (m === "public_read") return "read";
8421
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
8422
+ if (m == null) {
8423
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
8424
+ return isSystem ? "public" : "private";
8425
+ }
8426
+ return "private";
8427
+ }
8428
+ function masterOf(obj) {
8429
+ for (const f of asArray38(obj.fields)) {
8430
+ if (f.type === "master_detail") {
8431
+ const ref = f.reference;
8432
+ if (typeof ref === "string" && ref) return ref;
8433
+ }
8434
+ }
8435
+ return void 0;
8436
+ }
8437
+ function anchorFindings(rule, index, objectsByName) {
8438
+ const object = str2(rule.object);
8439
+ if (!object) return [];
8440
+ const target = objectsByName.get(object);
8441
+ if (!target) return [];
8442
+ const name = str2(rule.name) || String(index);
8443
+ const where = `sharing rule "${name}" on object "${object}"`;
8444
+ const path = `sharingRules[${index}].object`;
8445
+ const owd = target.sharingModel;
8446
+ if (owd === "controlled_by_parent") {
8447
+ const master = masterOf(target);
8448
+ return [{
8449
+ severity: "error",
8450
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
8451
+ where,
8452
+ path,
8453
+ 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.`,
8454
+ 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.`
8455
+ }];
8456
+ }
8457
+ if (effectiveSharingModelOf(target) !== "public") return [];
8458
+ 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`;
8459
+ return [{
8460
+ severity: "error",
8461
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
8462
+ where,
8463
+ path,
8464
+ 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.`,
8465
+ 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.`
8466
+ }];
8467
+ }
7868
8468
  function validateSharingRuleEnforceability(stack) {
7869
8469
  const findings = [];
7870
8470
  const cfg = stack ?? {};
8471
+ const objectsByName = /* @__PURE__ */ new Map();
8472
+ for (const obj of asArray38(cfg.objects)) {
8473
+ const name = str2(obj.name);
8474
+ if (name) objectsByName.set(name, obj);
8475
+ }
7871
8476
  asArray38(cfg.sharingRules).forEach((rule, index) => {
8477
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
7872
8478
  const input = toCompilerInput(rule.condition);
7873
8479
  if (input === null) return;
7874
8480
  const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
@@ -7994,11 +8600,11 @@ var import_meta4 = {};
7994
8600
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
7995
8601
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
7996
8602
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
7997
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8603
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7998
8604
  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 }));
8605
+ if (Array.isArray(v)) return v.filter(isRec24);
8606
+ if (isRec24(v)) {
8607
+ return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
8002
8608
  }
8003
8609
  return [];
8004
8610
  }
@@ -8015,7 +8621,7 @@ function loadAjv() {
8015
8621
  `@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
8622
  );
8017
8623
  }
8018
- const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
8624
+ const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
8019
8625
  cachedAjv = ctor;
8020
8626
  return ctor;
8021
8627
  }
@@ -8030,7 +8636,7 @@ function loadAddFormats() {
8030
8636
  `@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
8637
  );
8032
8638
  }
8033
- const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
8639
+ const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
8034
8640
  cachedAddFormats = plugin;
8035
8641
  return plugin;
8036
8642
  }
@@ -8060,13 +8666,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
8060
8666
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
8061
8667
  for (const branch of ["then", "otherwise"]) {
8062
8668
  const nested = rule[branch];
8063
- if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8669
+ if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8064
8670
  }
8065
8671
  return out;
8066
8672
  }
8067
8673
  function walkObjectValidationRules(stack) {
8068
8674
  const walked = [];
8069
- if (!isRec23(stack)) return walked;
8675
+ if (!isRec24(stack)) return walked;
8070
8676
  for (const obj of asArray40(stack.objects)) {
8071
8677
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8072
8678
  const validations = obj.validations;
@@ -8101,7 +8707,7 @@ function validateRuleCompilability(stack) {
8101
8707
  });
8102
8708
  }
8103
8709
  }
8104
- if (rule.type === "json_schema" && isRec23(rule.schema)) {
8710
+ if (rule.type === "json_schema" && isRec24(rule.schema)) {
8105
8711
  try {
8106
8712
  createRuntimeAjv().compile(rule.schema);
8107
8713
  } catch (err) {
@@ -8121,7 +8727,7 @@ function validateRuleCompilability(stack) {
8121
8727
 
8122
8728
  // src/validate-rule-schema-formats.ts
8123
8729
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
8124
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8730
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8125
8731
  var SUBSCHEMA_KEYS = [
8126
8732
  "additionalItems",
8127
8733
  "additionalProperties",
@@ -8145,7 +8751,7 @@ var SUBSCHEMA_MAP_KEYS = [
8145
8751
  var MAX_SCHEMA_WALK_DEPTH = 32;
8146
8752
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
8147
8753
  function collectFormatUses(schema, pointer, out, depth) {
8148
- if (!isRec24(schema)) return;
8754
+ if (!isRec25(schema)) return;
8149
8755
  if (typeof schema.format === "string") {
8150
8756
  out.push({ pointer: `${pointer}/format`, name: schema.format });
8151
8757
  }
@@ -8164,7 +8770,7 @@ function collectFormatUses(schema, pointer, out, depth) {
8164
8770
  }
8165
8771
  for (const key of SUBSCHEMA_MAP_KEYS) {
8166
8772
  const value = schema[key];
8167
- if (!isRec24(value)) continue;
8773
+ if (!isRec25(value)) continue;
8168
8774
  for (const [name, entry] of Object.entries(value)) {
8169
8775
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
8170
8776
  }
@@ -8172,13 +8778,13 @@ function collectFormatUses(schema, pointer, out, depth) {
8172
8778
  const items = schema.items;
8173
8779
  if (Array.isArray(items)) {
8174
8780
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
8175
- } else if (isRec24(items)) {
8781
+ } else if (isRec25(items)) {
8176
8782
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
8177
8783
  }
8178
8784
  const dependencies = schema.dependencies;
8179
- if (isRec24(dependencies)) {
8785
+ if (isRec25(dependencies)) {
8180
8786
  for (const [name, entry] of Object.entries(dependencies)) {
8181
- if (!isRec24(entry)) continue;
8787
+ if (!isRec25(entry)) continue;
8182
8788
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
8183
8789
  }
8184
8790
  }
@@ -8205,9 +8811,9 @@ function nearestRegisteredFormat(name, registered) {
8205
8811
  let best = null;
8206
8812
  let bestDistance = Number.POSITIVE_INFINITY;
8207
8813
  for (const candidate of [...registered].sort()) {
8208
- const distance7 = editDistance2(authored, candidate);
8209
- if (distance7 < bestDistance) {
8210
- bestDistance = distance7;
8814
+ const distance8 = editDistance2(authored, candidate);
8815
+ if (distance8 < bestDistance) {
8816
+ bestDistance = distance8;
8211
8817
  best = candidate;
8212
8818
  }
8213
8819
  }
@@ -8217,7 +8823,7 @@ function validateRuleSchemaFormats(stack) {
8217
8823
  const findings = [];
8218
8824
  const pending = [];
8219
8825
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
8220
- if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
8826
+ if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
8221
8827
  const uses = [];
8222
8828
  collectFormatUses(rule.schema, "", uses, 0);
8223
8829
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -8250,7 +8856,7 @@ function asArray41(v) {
8250
8856
  }
8251
8857
  return [];
8252
8858
  }
8253
- function strName20(v) {
8859
+ function strName21(v) {
8254
8860
  return typeof v === "string" && v.length > 0 ? v : void 0;
8255
8861
  }
8256
8862
  function strList3(v) {
@@ -8265,7 +8871,7 @@ function collectNamePlacedActions(stack) {
8265
8871
  for (const n of strList3(list3[key])) placed.add(n);
8266
8872
  }
8267
8873
  for (const def of asArray41(list3.bulkActionDefs)) {
8268
- const n = strName20(def?.name);
8874
+ const n = strName21(def?.name);
8269
8875
  if (n) placed.add(n);
8270
8876
  }
8271
8877
  };
@@ -8291,7 +8897,7 @@ function validateActionLocations(stack) {
8291
8897
  const check = (action, path) => {
8292
8898
  if (!action || typeof action !== "object") return;
8293
8899
  if ("locations" in action) return;
8294
- const name = strName20(action.name);
8900
+ const name = strName21(action.name);
8295
8901
  if (!name) return;
8296
8902
  if (namePlaced.has(name)) return;
8297
8903
  findings.push({
@@ -8317,7 +8923,7 @@ function validateActionLocations(stack) {
8317
8923
 
8318
8924
  // src/lint-flow-patterns.ts
8319
8925
  var import_automation5 = require("@objectstack/spec/automation");
8320
- var import_data10 = require("@objectstack/spec/data");
8926
+ var import_data12 = require("@objectstack/spec/data");
8321
8927
  function asArray42(v) {
8322
8928
  if (Array.isArray(v)) return v;
8323
8929
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -8592,7 +9198,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
8592
9198
  function filterCarriesNoCondition(filter) {
8593
9199
  if (filter === void 0 || filter === null) return true;
8594
9200
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
8595
- return (0, import_data10.reduceFilterVerdict)(filter) === "true";
9201
+ return (0, import_data12.reduceFilterVerdict)(filter) === "true";
8596
9202
  }
8597
9203
  function describeUnboundedFilter(filter) {
8598
9204
  if (filter === void 0 || filter === null) return "no `filter` key";
@@ -8959,7 +9565,7 @@ function lintLivenessProperties(stack) {
8959
9565
  }
8960
9566
 
8961
9567
  // src/lint-autonumber-formats.ts
8962
- var import_data11 = require("@objectstack/spec/data");
9568
+ var import_data13 = require("@objectstack/spec/data");
8963
9569
  var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
8964
9570
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
8965
9571
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
@@ -8985,8 +9591,8 @@ function lintAutonumberFormats(stack) {
8985
9591
  const name = typeof f.name === "string" ? f.name : "(unnamed field)";
8986
9592
  const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
8987
9593
  if (!fmt) continue;
8988
- const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
8989
- const refs = (0, import_data11.referencedFields)(tokens);
9594
+ const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
9595
+ const refs = (0, import_data13.referencedFields)(tokens);
8990
9596
  const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
8991
9597
  for (const t of tokens) {
8992
9598
  if (t.kind !== "literal") continue;
@@ -9141,6 +9747,17 @@ function lintViewRefs(stack) {
9141
9747
  }
9142
9748
 
9143
9749
  // src/data-model-rules.ts
9750
+ function objectWhere(obj) {
9751
+ return `object "${obj?.name}"`;
9752
+ }
9753
+ function indexWhere(obj, idx, j, cols) {
9754
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
9755
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
9756
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
9757
+ }
9758
+ function fieldWhere(obj, fieldName) {
9759
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
9760
+ }
9144
9761
  function fieldEntries2(fields) {
9145
9762
  if (!fields) return [];
9146
9763
  if (Array.isArray(fields)) {
@@ -9174,11 +9791,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9174
9791
  for (let j = 0; j < declaredIndexes.length; j++) {
9175
9792
  const idx = declaredIndexes[j];
9176
9793
  if (idx?.unique !== true) continue;
9177
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
9794
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
9795
+ const cols = colList.join(", ");
9178
9796
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9179
9797
  issues.push({
9180
9798
  severity: "warning",
9181
9799
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
9800
+ where: indexWhere(obj, idx, j, colList),
9182
9801
  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
9802
  path: `objects[${i}].indexes[${j}]`,
9184
9803
  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 +9846,11 @@ function lintUniqueDeclarations(objects) {
9227
9846
  issues.push({
9228
9847
  severity: "warning",
9229
9848
  rule: UNIQUE_DOUBLE_DECLARATION,
9849
+ // More specific than `path` on purpose: this rule's finding is about ONE
9850
+ // column, but its `path` has always been the whole object (`objects[i]`)
9851
+ // because the defect straddles `fields.<name>.unique` and an entry of
9852
+ // `indexes`. `where` can name the column without picking one of the two.
9853
+ where: fieldWhere(obj, name),
9230
9854
  message,
9231
9855
  path: `objects[${i}]`,
9232
9856
  fix
@@ -9255,6 +9879,7 @@ function lintLegacyOrganizationComposites(objects) {
9255
9879
  issues.push({
9256
9880
  severity: "warning",
9257
9881
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
9882
+ where: indexWhere(obj, idx, j, cols),
9258
9883
  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
9884
  path: `objects[${i}].indexes[${j}]`,
9260
9885
  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 +9896,7 @@ var CLI_ONLY = ["cli"];
9271
9896
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9272
9897
  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
9898
  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.";
9899
+ 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
9900
  var EXPRESSION_INVALID = "expression-invalid";
9276
9901
  var AUTHORING_RULES = [
9277
9902
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -9332,8 +9957,14 @@ var AUTHORING_RULES = [
9332
9957
  input: "normalized",
9333
9958
  commands: ALL,
9334
9959
  source: "packages/lint/src/validate-functional-completeness.ts",
9335
- surfaces: CLI_ONLY,
9336
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9960
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
9961
+ // object rules cross together under the 2026-08-18 adjudication. The
9962
+ // false-positive budget the crossing owed was exempted on a measured
9963
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
9964
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
9965
+ // advisory-tier object rules deliberately do NOT ride.
9966
+ surfaces: CLI_AND_RUNTIME,
9967
+ runtimeTypes: ["object"],
9337
9968
  run: (stack) => validateFunctionalCompleteness(stack)
9338
9969
  },
9339
9970
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -9357,8 +9988,11 @@ var AUTHORING_RULES = [
9357
9988
  input: "normalized",
9358
9989
  commands: ALL,
9359
9990
  source: "packages/lint/src/validate-managed-api-methods.ts",
9360
- surfaces: CLI_ONLY,
9361
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9991
+ // Runtime publish gate (#4716): a managed object advertising a verb its
9992
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
9993
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
9994
+ surfaces: CLI_AND_RUNTIME,
9995
+ runtimeTypes: ["object"],
9362
9996
  run: (stack) => validateManagedApiMethods(stack)
9363
9997
  },
9364
9998
  // A view container in `views: []` that registers zero views: nothing appears
@@ -9380,19 +10014,38 @@ var AUTHORING_RULES = [
9380
10014
  },
9381
10015
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
9382
10016
  // chartConfig axis/series must resolve against the declared datasets.
10017
+ //
10018
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
10019
+ // widget bound to a dataset that resolves to nothing sailed through save AND
10020
+ // publish — this rule provably caught the body and was simply never invoked,
10021
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
10022
+ // hold a forward reference; publishing refuses it with the key path named.
10023
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
10024
+ // — without it every legitimate board reads as dangling, the 3-phantom
10025
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
10026
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
10027
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
10028
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
10029
+ // accepted knowingly rather than splitting the dataset limb into its own
10030
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
10031
+ // filter (registry machinery that weakens "delete a rule from the table and
10032
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
10033
+ // advisory channel and never block (#4463 P1).
9383
10034
  {
9384
10035
  name: "validateWidgetBindings",
9385
10036
  tier: "gating",
9386
10037
  input: "parsed",
9387
10038
  commands: ALL,
9388
10039
  source: "packages/lint/src/validate-widget-bindings.ts",
9389
- surfaces: CLI_ONLY,
9390
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10040
+ surfaces: CLI_AND_RUNTIME,
10041
+ runtimeTypes: ["dashboard"],
9391
10042
  run: (stack) => validateWidgetBindings(stack)
9392
10043
  },
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.
10044
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
10045
+ // button that renders and refuses (or does nothing) on click: a `script`
10046
+ // target must name a defined action, a `modal` target must name a declared
10047
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
10048
+ // Unresolved `url` routes stay advisory.
9396
10049
  {
9397
10050
  name: "validateDashboardActionRefs",
9398
10051
  tier: "gating",
@@ -9417,6 +10070,28 @@ var AUTHORING_RULES = [
9417
10070
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9418
10071
  run: (stack) => validateFilterTokens(stack)
9419
10072
  },
10073
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
10074
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
10075
+ // no layer: the engine refuses it on a declared temporal field at query time
10076
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
10077
+ // literal string. This is the authoring-time refusal the ruling shipped
10078
+ // alongside the engine door, judging the filter literal in isolation —
10079
+ // ordering positions only, all three authored filter shapes. Like
10080
+ // `validateEmptyCombinators` it needs NO resolution context, so
10081
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
10082
+ // for every filter-carrying type the gate already maps: the write path is
10083
+ // the one door an AI author uses, and dashboards/views are where the preset
10084
+ // vocabulary is near enough to reach for.
10085
+ {
10086
+ name: "validatePresetComparands",
10087
+ tier: "gating",
10088
+ input: "parsed",
10089
+ commands: ALL,
10090
+ source: "packages/lint/src/validate-preset-comparands.ts",
10091
+ surfaces: CLI_AND_RUNTIME,
10092
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
10093
+ run: (stack) => validatePresetComparands(stack)
10094
+ },
9420
10095
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9421
10096
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9422
10097
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -9460,9 +10135,31 @@ var AUTHORING_RULES = [
9460
10135
  // collection in the snapshot and return nothing, and the two that would
9461
10136
  // load `typescript` need a hook/action/react body the snapshot never
9462
10137
  // carries — which is what `runtime-lazy-deps.test.ts` pins.
10138
+ //
10139
+ // [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
10140
+ // taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
10141
+ // dispatch the suite, and the suite's own per-member `runtimeTypes`
10142
+ // (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
10143
+ // that snapshot — the gate passes the written type through
10144
+ // `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
10145
+ // members that judge a list view's field references and resolve only
10146
+ // against the `objects` collection the snapshot carries
10147
+ // (`validateSearchableFields`, `validateSortableFields`). The whole suite
10148
+ // is NOT the right granularity for this door, measured not assumed, and
10149
+ // the crossing fails differently per body shape — both ways wrong:
10150
+ // `validateActionNameRefs` (error-tier) resolves `views[].list` /
10151
+ // `views[].listViews.*` action names against `stack.actions`, a collection
10152
+ // no per-write snapshot carries, so on a CONTAINER view write it would
10153
+ // refuse every stack-level action the body names (measured:
10154
+ // `action-name-undefined` on the snapshot shape, clean on the full stack)
10155
+ // — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
10156
+ // type the gate has. On a FLATTENED overlay it has no rung at all, so the
10157
+ // crossing would be a silent no-op that reads as coverage — the very shape
10158
+ // #9313 was filed about. Flow snapshots are unchanged: every member keeps
10159
+ // the default `flow` declaration.
9463
10160
  surfaces: CLI_AND_RUNTIME,
9464
- runtimeTypes: ["flow"],
9465
- run: (stack) => validateReferenceIntegrity(stack)
10161
+ runtimeTypes: ["flow", "view"],
10162
+ run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
9466
10163
  },
9467
10164
  // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
9468
10165
  // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
@@ -9635,7 +10332,7 @@ var AUTHORING_RULES = [
9635
10332
  commands: ALL,
9636
10333
  source: "packages/lint/src/validate-record-title.ts",
9637
10334
  surfaces: CLI_ONLY,
9638
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10335
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9639
10336
  run: (stack) => validateRecordTitle(stack)
9640
10337
  },
9641
10338
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -9648,7 +10345,7 @@ var AUTHORING_RULES = [
9648
10345
  commands: ALL,
9649
10346
  source: "packages/lint/src/validate-semantic-roles.ts",
9650
10347
  surfaces: CLI_ONLY,
9651
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10348
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9652
10349
  run: (stack) => validateSemanticRoles(stack)
9653
10350
  },
9654
10351
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -9859,7 +10556,7 @@ var AUTHORING_RULES = [
9859
10556
  commands: ALL,
9860
10557
  source: "packages/lint/src/lint-liveness-properties.ts",
9861
10558
  surfaces: CLI_ONLY,
9862
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10559
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9863
10560
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
9864
10561
  severity: "warning",
9865
10562
  rule: f.rule,
@@ -9878,8 +10575,12 @@ var AUTHORING_RULES = [
9878
10575
  input: "parsed",
9879
10576
  commands: ALL,
9880
10577
  source: "packages/lint/src/lint-autonumber-formats.ts",
9881
- surfaces: CLI_ONLY,
9882
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10578
+ // Runtime publish gate (#4716): an autonumber referencing a field the
10579
+ // object does not carry is broken from the first record; only the error
10580
+ // arm blocks — the optional-field arm is `warning` and rides the
10581
+ // advisory channel like every other non-error finding (#4463 P1).
10582
+ surfaces: CLI_AND_RUNTIME,
10583
+ runtimeTypes: ["object"],
9883
10584
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
9884
10585
  severity: f.severity,
9885
10586
  rule: f.rule,
@@ -9920,12 +10621,12 @@ var AUTHORING_RULES = [
9920
10621
  commands: ["validate", "build"],
9921
10622
  source: "packages/lint/src/data-model-rules.ts",
9922
10623
  surfaces: CLI_ONLY,
9923
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10624
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9924
10625
  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
10626
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9926
10627
  severity: f.severity === "suggestion" ? "info" : f.severity,
9927
10628
  rule: f.rule,
9928
- where: f.path,
10629
+ where: f.where,
9929
10630
  path: f.path,
9930
10631
  message: f.message,
9931
10632
  hint: f.fix ?? ""
@@ -9941,12 +10642,12 @@ var AUTHORING_RULES = [
9941
10642
  commands: ["validate", "build"],
9942
10643
  source: "packages/lint/src/data-model-rules.ts",
9943
10644
  surfaces: CLI_ONLY,
9944
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10645
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9945
10646
  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
10647
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9947
10648
  severity: f.severity === "suggestion" ? "info" : f.severity,
9948
10649
  rule: f.rule,
9949
- where: f.path,
10650
+ where: f.where,
9950
10651
  path: f.path,
9951
10652
  message: f.message,
9952
10653
  hint: f.fix ?? ""
@@ -9963,12 +10664,12 @@ var AUTHORING_RULES = [
9963
10664
  commands: ["validate", "build"],
9964
10665
  source: "packages/lint/src/data-model-rules.ts",
9965
10666
  surfaces: CLI_ONLY,
9966
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10667
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9967
10668
  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
10669
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9969
10670
  severity: f.severity === "suggestion" ? "info" : f.severity,
9970
10671
  rule: f.rule,
9971
- where: f.path,
10672
+ where: f.where,
9972
10673
  path: f.path,
9973
10674
  message: f.message,
9974
10675
  hint: f.fix ?? ""
@@ -10096,16 +10797,26 @@ var AUTHORING_RULES = [
10096
10797
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10097
10798
  run: (stack) => validateOrgAxisRedLines(stack)
10098
10799
  },
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.
10800
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
10801
+ // sharing rule where the predicate is EXACT rather than inferred.
10802
+ //
10803
+ // - `condition` has a single runtime consumer
10804
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
10805
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
10806
+ // does not lower means the rule is SKIPPED at boot.
10807
+ // - `object` decides whether the grant is refused outright: reconcile hands
10808
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
10809
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
10810
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
10811
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
10812
+ // bypass set, federated anchors) are not, and are excluded by name.
10813
+ //
10814
+ // Either way the grant is declared and does not exist. The lint calls the
10815
+ // same compiler and mirrors the same verdict function, from the same inputs
10816
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
10817
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
10818
+ // surface is enforced, and these were the fields where that sentence was not
10819
+ // yet true.
10109
10820
  {
10110
10821
  name: "validateSharingRuleEnforceability",
10111
10822
  tier: "gating",
@@ -10113,7 +10824,7 @@ var AUTHORING_RULES = [
10113
10824
  commands: ALL,
10114
10825
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10115
10826
  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.",
10827
+ 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
10828
  run: (stack) => validateSharingRuleEnforceability(stack)
10118
10829
  },
10119
10830
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10152,8 +10863,15 @@ var AUTHORING_RULES = [
10152
10863
  input: "parsed",
10153
10864
  commands: ALL,
10154
10865
  source: "packages/lint/src/validate-rule-compilability.ts",
10155
- surfaces: CLI_ONLY,
10156
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10866
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
10867
+ // judged snapshot actually carries a `json_schema` validation — so an
10868
+ // ordinary object write (no `json_schema` anywhere) still loads no
10869
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
10870
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
10871
+ // carrying such a rule) is the measured, adjudicated price of refusing a
10872
+ // validation rule that would otherwise ship compiled-by-nothing.
10873
+ surfaces: CLI_AND_RUNTIME,
10874
+ runtimeTypes: ["object"],
10157
10875
  run: (stack) => validateRuleCompilability(stack)
10158
10876
  },
10159
10877
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10174,8 +10892,12 @@ var AUTHORING_RULES = [
10174
10892
  input: "parsed",
10175
10893
  commands: ALL,
10176
10894
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10177
- surfaces: CLI_ONLY,
10178
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10895
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
10896
+ // the two judgements over one artifact stay on one side of the wall
10897
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
10898
+ // format set is only enumerated once a schema actually names a format.
10899
+ surfaces: CLI_AND_RUNTIME,
10900
+ runtimeTypes: ["object"],
10179
10901
  run: (stack) => validateRuleSchemaFormats(stack)
10180
10902
  }
10181
10903
  ];
@@ -10217,7 +10939,20 @@ var TYPE_TO_STACK_KEY = {
10217
10939
  permission: "permissions",
10218
10940
  book: "books"
10219
10941
  };
10220
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
10942
+ function narrowObjectsToPackageClosure(objects, scope) {
10943
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
10944
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
10945
+ return objects.filter((entry) => {
10946
+ if (!entry || typeof entry !== "object") return true;
10947
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
10948
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
10949
+ if (reachable.has(owner)) return true;
10950
+ return isSystemObject(entry);
10951
+ });
10952
+ }
10953
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
10954
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
10955
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
10221
10956
  function runtimeAuthoringRulesFor(type) {
10222
10957
  return AUTHORING_RULES.filter(
10223
10958
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10243,7 +10978,8 @@ function buildRuntimeWriteSnapshots(args) {
10243
10978
  const itemName = typeof item.name === "string" ? item.name : void 0;
10244
10979
  const baseline = {};
10245
10980
  for (const key of CONTEXT_STACK_KEYS) {
10246
- const collection = args.context?.[key] ?? [];
10981
+ const raw = args.context?.[key] ?? [];
10982
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
10247
10983
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10248
10984
  }
10249
10985
  const candidate = {
@@ -10252,6 +10988,20 @@ function buildRuntimeWriteSnapshots(args) {
10252
10988
  };
10253
10989
  return { baseline, candidate };
10254
10990
  }
10991
+ var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
10992
+ var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
10993
+ var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
10994
+ function nameKeyFindingPath(path, candidate) {
10995
+ const m = TOP_LEVEL_INDEX.exec(path);
10996
+ if (!m) return path;
10997
+ const [, stackKey, index, rest] = m;
10998
+ if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
10999
+ const collection = candidate[stackKey];
11000
+ const entry = collection?.[Number(index)];
11001
+ const name = entry && typeof entry === "object" ? entry.name : void 0;
11002
+ if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
11003
+ return `${stackKey}.${name}${rest}`;
11004
+ }
10255
11005
  function runRules(rules, stack, ctx) {
10256
11006
  const findings = [];
10257
11007
  for (const rule of rules) {
@@ -10277,12 +11027,16 @@ function runRuntimeAuthoringRules(args) {
10277
11027
  const snapshots = buildRuntimeWriteSnapshots({
10278
11028
  type: args.type,
10279
11029
  item: args.item,
10280
- ...args.context !== void 0 ? { context: args.context } : {}
11030
+ ...args.context !== void 0 ? { context: args.context } : {},
11031
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10281
11032
  });
10282
11033
  if (!snapshots) return empty;
10283
- const ctx = { sduiManifest: args.sduiManifest };
11034
+ const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
10284
11035
  const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10285
- const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
11036
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
11037
+ const path = nameKeyFindingPath(f.path, snapshots.candidate);
11038
+ return path === f.path ? f : { ...f, path };
11039
+ });
10286
11040
  return {
10287
11041
  errors: added.filter((f) => f.severity === "error"),
10288
11042
  advisories: added.filter((f) => f.severity !== "error"),
@@ -10292,6 +11046,7 @@ function runRuntimeAuthoringRules(args) {
10292
11046
  // Annotate the CommonJS export names for ESM import in node:
10293
11047
  0 && (module.exports = {
10294
11048
  buildRuntimeWriteSnapshots,
11049
+ narrowObjectsToPackageClosure,
10295
11050
  runRuntimeAuthoringRules,
10296
11051
  runtimeAuthoringRulesFor,
10297
11052
  runtimeGatedTypes,