@objectstack/lint 17.0.0-rc.6 → 17.0.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
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/runtime.ts
21
21
  var runtime_exports = {};
22
22
  __export(runtime_exports, {
23
+ buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
23
24
  runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
24
25
  runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
25
26
  runtimeGatedTypes: () => runtimeGatedTypes,
@@ -41,6 +42,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
41
42
  function injectedColumnsFor(objectDef) {
42
43
  return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
43
44
  }
45
+ function unprovisionedInjectedColumnsFor(objectDef) {
46
+ return new Set((0, import_data.unprovisionedInjectedColumns)(objectDef));
47
+ }
48
+ function objectDefsOf(stack) {
49
+ if (!stack || typeof stack !== "object") return [];
50
+ const objects = stack.objects;
51
+ if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
52
+ if (objects && typeof objects === "object") {
53
+ return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
54
+ }
55
+ return [];
56
+ }
57
+ function indexUnprovisionedAnchors(stack) {
58
+ const index = /* @__PURE__ */ new Map();
59
+ for (const obj of objectDefsOf(stack)) {
60
+ const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
61
+ if (!name) continue;
62
+ const anchors = unprovisionedInjectedColumnsFor(obj);
63
+ if (anchors.size > 0) index.set(name, anchors);
64
+ }
65
+ return index;
66
+ }
67
+ function unprovisionedAnchorCause(objectName, field) {
68
+ return `'${field}' is an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column`;
69
+ }
70
+ function unprovisionedAnchorHint(objectName, field) {
71
+ return `If the remote table really carries '${field}', declare it in ${objectName}'s own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`;
72
+ }
44
73
 
45
74
  // src/validate-null-guards.ts
46
75
  var import_formula = require("@objectstack/formula");
@@ -274,6 +303,37 @@ function buildFieldIndex(objects) {
274
303
  }
275
304
  return idx;
276
305
  }
306
+ var BOUND_RECORD_ROOTS = ["record", "previous"];
307
+ function isCelNode(v) {
308
+ return !!v && typeof v === "object" && typeof v.op === "string";
309
+ }
310
+ function collectBoundRecordReads(source) {
311
+ const out = /* @__PURE__ */ new Map();
312
+ const ast = (0, import_formula2.parseCelToAst)(source);
313
+ if (!ast) return out;
314
+ const pending = [ast];
315
+ while (pending.length > 0) {
316
+ const celNode = pending.pop();
317
+ if (!isCelNode(celNode)) continue;
318
+ if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
319
+ const [celRecv, seg] = celNode.args;
320
+ if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
321
+ if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
322
+ }
323
+ }
324
+ const celArgs = celNode.args;
325
+ if (isCelNode(celArgs)) pending.push(celArgs);
326
+ else if (Array.isArray(celArgs)) {
327
+ for (const a of celArgs) {
328
+ if (isCelNode(a)) pending.push(a);
329
+ else if (Array.isArray(a)) {
330
+ for (const b of a) if (isCelNode(b)) pending.push(b);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ return out;
336
+ }
277
337
  function buildFieldTypeIndex(objects) {
278
338
  const idx = /* @__PURE__ */ new Map();
279
339
  for (const obj of objects) {
@@ -373,6 +433,29 @@ function validateStackExpressions(stack) {
373
433
  const fieldIndex = buildFieldIndex(objects);
374
434
  const fieldTypeIndex = buildFieldTypeIndex(objects);
375
435
  const nullableIndex = buildNullableFieldIndex(objects);
436
+ const unprovisionedIndex = /* @__PURE__ */ new Map();
437
+ for (const obj of objects) {
438
+ const name = typeof obj.name === "string" ? obj.name : void 0;
439
+ if (!name) continue;
440
+ const anchors = unprovisionedInjectedColumnsFor(obj);
441
+ if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
442
+ }
443
+ const warnUnprovisionedAnchors = (where, raw, objectName) => {
444
+ if (!objectName) return;
445
+ const anchors = unprovisionedIndex.get(objectName);
446
+ if (!anchors) return;
447
+ const source = celSourceOf(raw);
448
+ if (!source) return;
449
+ for (const [field, operand] of collectBoundRecordReads(source)) {
450
+ if (!anchors.has(field)) continue;
451
+ issues.push({
452
+ where,
453
+ message: `\`${operand}\` reads '${field}', an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column. The predicate can never match a real value \u2014 on SQLite it silently degrades to constant-false (HTTP 200, zero rows, no error). If the remote table really carries this column, declare '${field}' in the object's own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`,
454
+ source,
455
+ severity: "warning"
456
+ });
457
+ }
458
+ };
376
459
  const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
377
460
  if (!objectName) return;
378
461
  const nullableFields = nullableIndex.get(objectName);
@@ -399,6 +482,7 @@ function validateStackExpressions(stack) {
399
482
  );
400
483
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
401
484
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
485
+ warnUnprovisionedAnchors(where, raw, objectName);
402
486
  };
403
487
  const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
404
488
  const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
@@ -545,6 +629,7 @@ function validateStackExpressions(stack) {
545
629
  const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
546
630
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
547
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);
548
633
  }
549
634
  }
550
635
  }
@@ -750,6 +835,43 @@ function validateFunctionalCompleteness(stack) {
750
835
  return out;
751
836
  }
752
837
 
838
+ // src/validate-managed-api-methods.ts
839
+ var import_data2 = require("@objectstack/spec/data");
840
+ var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
841
+ var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
842
+ function entriesOf2(v) {
843
+ if (Array.isArray(v)) {
844
+ return v.flatMap(
845
+ (def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
846
+ );
847
+ }
848
+ if (isRec2(v)) {
849
+ return Object.entries(v).flatMap(
850
+ ([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
851
+ );
852
+ }
853
+ return [];
854
+ }
855
+ function validateManagedApiMethods(stack) {
856
+ const out = [];
857
+ if (!isRec2(stack)) return out;
858
+ for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
859
+ const conflicts = (0, import_data2.checkManagedApiMethodAffordances)(obj.def);
860
+ if (conflicts.length === 0) continue;
861
+ const verbs = conflicts.map((c) => c.verb).join(", ");
862
+ const flags = [...new Set(conflicts.map((c) => c.needs))];
863
+ out.push({
864
+ severity: "error",
865
+ rule: MANAGED_API_METHOD_UNAFFORDABLE,
866
+ where: `object "${obj.name}"`,
867
+ path: `objects[${oi}].enable.apiMethods`,
868
+ message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + (0, import_data2.describeManagedApiMethodConflicts)(conflicts) + ` The registry STRIPS [${verbs}] at registration, so this declaration and the API you actually get already disagree \u2014 today the only trace is a line in the boot log.`,
869
+ hint: `Either add \`userActions: { ${flags.map((f) => `${f}: true`).join(", ")} }\` to the object \u2014 only if the write is genuinely one a user context may perform, and only once the guard enforcing it exists (ADR-0092 D4: affordance never ships ahead of the guard) \u2014 or remove [${verbs}] from \`enable.apiMethods\`, which is what the runtime does for you today.`
870
+ });
871
+ }
872
+ return out;
873
+ }
874
+
753
875
  // src/validate-view-containers.ts
754
876
  var VIEW_CONTAINER_SHAPE = "view-container-shape";
755
877
  var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
@@ -767,10 +889,32 @@ function containerViewCount(rec) {
767
889
  function validateViewContainers(stack) {
768
890
  const out = [];
769
891
  if (!stack || typeof stack !== "object") return out;
892
+ const viewItems = stack.viewItems;
893
+ if (viewItems != null && asEntries(viewItems).length > 0) {
894
+ out.push({
895
+ severity: "error",
896
+ rule: VIEW_CONTAINER_SHAPE,
897
+ where: "viewItems",
898
+ path: "viewItems",
899
+ message: "`viewItems` is the machine-assembled channel for non-container view artifacts in runtime-assembled manifests (package export, environment artifacts) \u2014 it is not an authoring surface.",
900
+ hint: "Author views as defineView containers in `views:`; author a standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`), not in stack source."
901
+ });
902
+ }
770
903
  for (const { key, value } of asEntries(stack.views)) {
771
904
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
772
905
  const rec = value;
773
- if (rec.viewKind != null) continue;
906
+ if (rec.viewKind != null) {
907
+ const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
908
+ out.push({
909
+ severity: "error",
910
+ rule: VIEW_CONTAINER_SHAPE,
911
+ where: `views${key}${label3}`,
912
+ path: `views${key}`,
913
+ message: "A ViewItem record is not a view container: the stack `views:` collection carries containers only \u2014 `viewKind` belongs to a single VIEW, not to the container. The registration loop refuses this entry (#5320).",
914
+ hint: "Wrap it in a defineView container: defineView({ list: { type, data, columns, ... }, listViews: { ... } }) \u2014 or author the standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`). Machine-assembled manifests carry it under `viewItems:`."
915
+ });
916
+ continue;
917
+ }
774
918
  if (containerViewCount(rec) > 0) continue;
775
919
  const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
776
920
  const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
@@ -788,7 +932,7 @@ function validateViewContainers(stack) {
788
932
  }
789
933
 
790
934
  // src/validate-widget-bindings.ts
791
- var import_data2 = require("@objectstack/spec/data");
935
+ var import_data3 = require("@objectstack/spec/data");
792
936
  var import_ui = require("@objectstack/spec/ui");
793
937
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
794
938
  var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
@@ -800,6 +944,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
800
944
  var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
801
945
  var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
802
946
  var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
947
+ var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
803
948
  var LEGACY_ANALYTICS_KEYS = [
804
949
  "categoryField",
805
950
  "valueField",
@@ -919,6 +1064,7 @@ function validateWidgetBindings(stack) {
919
1064
  }
920
1065
  objectFieldTypes.set(o.name, fm);
921
1066
  }
1067
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
922
1068
  const datasetList = asArray3(stack.datasets);
923
1069
  for (let i = 0; i < datasetList.length; i++) {
924
1070
  const ds = datasetList[i];
@@ -931,7 +1077,7 @@ function validateWidgetBindings(stack) {
931
1077
  const aggregate = typeof m.aggregate === "string" ? m.aggregate : void 0;
932
1078
  if (!field || !aggregate) continue;
933
1079
  const ftype = fieldTypes.get(field);
934
- if (ftype && (0, import_data2.isIncoherentAggregate)(aggregate, ftype)) {
1080
+ if (ftype && (0, import_data3.isIncoherentAggregate)(aggregate, ftype)) {
935
1081
  findings.push({
936
1082
  severity: "warning",
937
1083
  rule: MEASURE_AGGREGATE_INCOHERENT,
@@ -1005,13 +1151,24 @@ function validateWidgetBindings(stack) {
1005
1151
  if (dashFilterDefs.length > 0) {
1006
1152
  const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
1007
1153
  const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
1008
- if (objectFields) {
1154
+ const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
1155
+ if (objectFields && datasetObject) {
1009
1156
  for (const def of dashFilterDefs) {
1010
1157
  const eff = effectiveFilterField(w, def);
1011
1158
  if (!eff) continue;
1012
1159
  const field = eff.field;
1013
1160
  if (field.includes(".")) continue;
1014
- if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
1161
+ if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
1162
+ if (anchors?.has(field)) {
1163
+ push2({
1164
+ severity: "warning",
1165
+ rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
1166
+ message: (eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but ` : `inherits dashboard filter \`${def.name}(${field})\`, but `) + `${unprovisionedAnchorCause(datasetObject, field)}. The filter is ANDed into this widget's analytics query (#2501), so it can never match a real value \u2014 on SQLite it silently degrades to constant-false and the widget renders empty (HTTP 200, zero rows, no error).`,
1167
+ hint: `${unprovisionedAnchorHint(datasetObject, field)} A widget can also opt out with filterBindings: { ${def.name}: false }. Suppress with suppressWarnings: ['${DASHBOARD_FILTER_FIELD_UNPROVISIONED}'] if the remote schema resolves it some other way.`
1168
+ });
1169
+ }
1170
+ continue;
1171
+ }
1015
1172
  push2({
1016
1173
  severity: "error",
1017
1174
  rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
@@ -1248,7 +1405,7 @@ function validateDashboardActionRefs(stack) {
1248
1405
  }
1249
1406
 
1250
1407
  // src/validate-filter-tokens.ts
1251
- var import_data3 = require("@objectstack/spec/data");
1408
+ var import_data4 = require("@objectstack/spec/data");
1252
1409
 
1253
1410
  // src/filter-walk.ts
1254
1411
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
@@ -1308,7 +1465,7 @@ function walkAuthoredFilters(stack, surfaces, visit) {
1308
1465
 
1309
1466
  // src/validate-filter-tokens.ts
1310
1467
  var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1311
- var KNOWN_LIST = import_data3.CONTEXT_TOKENS.join("}, {");
1468
+ var KNOWN_LIST = import_data4.CONTEXT_TOKENS.join("}, {");
1312
1469
  var TOKEN_FILTER_SURFACES = [
1313
1470
  { key: "dashboards", kind: "dashboard" },
1314
1471
  { key: "objects", kind: "object" },
@@ -1321,7 +1478,7 @@ var TOKEN_FILTER_SURFACES = [
1321
1478
  function walkFilterValues(node, path, where, out, seen) {
1322
1479
  if (node === null || node === void 0) return;
1323
1480
  if (typeof node === "string") {
1324
- const cls = (0, import_data3.classifyFilterToken)(node);
1481
+ const cls = (0, import_data4.classifyFilterToken)(node);
1325
1482
  if (cls?.kind === "unknown") {
1326
1483
  const suggestion = cls.suggestion;
1327
1484
  out.push({
@@ -1356,7 +1513,7 @@ function validateFilterTokens(stack) {
1356
1513
  }
1357
1514
 
1358
1515
  // src/validate-empty-combinators.ts
1359
- var import_data4 = require("@objectstack/spec/data");
1516
+ var import_data5 = require("@objectstack/spec/data");
1360
1517
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
1361
1518
  var FILTER_EMPTY_NODE = "filter-empty-node";
1362
1519
  var EMPTY_COMBINATOR_SURFACES = [
@@ -1375,12 +1532,12 @@ function isFilterNode(value) {
1375
1532
  return proto === Object.prototype || proto === null;
1376
1533
  }
1377
1534
  var VERDICT_OF = {
1378
- $and: (0, import_data4.reduceFilterVerdict)({ $and: [] }),
1379
- $or: (0, import_data4.reduceFilterVerdict)({ $or: [] }),
1380
- $not: (0, import_data4.reduceFilterVerdict)({ $not: {} }),
1381
- node: (0, import_data4.reduceFilterVerdict)({}),
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)({}),
1382
1539
  /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
1383
- orWithEmptyBranch: (0, import_data4.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
1540
+ orWithEmptyBranch: (0, import_data5.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
1384
1541
  };
1385
1542
  function rows(verdict) {
1386
1543
  if (verdict === "true") return "matches EVERY row";
@@ -1665,7 +1822,7 @@ function validateObjectReferences(stack) {
1665
1822
  }
1666
1823
 
1667
1824
  // src/validate-searchable-fields.ts
1668
- var import_data5 = require("@objectstack/spec/data");
1825
+ var import_data6 = require("@objectstack/spec/data");
1669
1826
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1670
1827
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1671
1828
  function asArray7(v) {
@@ -1675,7 +1832,7 @@ function asArray7(v) {
1675
1832
  }
1676
1833
  return [];
1677
1834
  }
1678
- function isRec2(v) {
1835
+ function isRec3(v) {
1679
1836
  return !!v && typeof v === "object" && !Array.isArray(v);
1680
1837
  }
1681
1838
  function strName3(v) {
@@ -1713,7 +1870,7 @@ function resolveAllowedSet(target) {
1713
1870
  fields = { ...fields };
1714
1871
  for (const f of systemDeclared) fields[f] = {};
1715
1872
  }
1716
- const { allowed, source } = (0, import_data5.resolveSearchFieldResolution)({
1873
+ const { allowed, source } = (0, import_data6.resolveSearchFieldResolution)({
1717
1874
  fields,
1718
1875
  searchableFields: target.searchableFields,
1719
1876
  displayField: target.displayField
@@ -1751,7 +1908,7 @@ function distance2(a, b) {
1751
1908
  }
1752
1909
  function indexObjectSearchTargets(stack) {
1753
1910
  const fieldsByObject = /* @__PURE__ */ new Map();
1754
- if (!isRec2(stack)) return fieldsByObject;
1911
+ if (!isRec3(stack)) return fieldsByObject;
1755
1912
  for (const obj of asArray7(stack.objects)) {
1756
1913
  const name = strName3(obj.name);
1757
1914
  if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
@@ -1783,7 +1940,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1783
1940
  });
1784
1941
  continue;
1785
1942
  }
1786
- if ((0, import_data5.isVirtualSearchField)(target.fields[name])) {
1943
+ if ((0, import_data6.isVirtualSearchField)(target.fields[name])) {
1787
1944
  const vtype = target.fields[name]?.type;
1788
1945
  findings.push({
1789
1946
  severity: "error",
@@ -1811,7 +1968,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1811
1968
  }
1812
1969
  const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
1813
1970
  let why;
1814
- if (import_data5.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1971
+ if (import_data6.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1815
1972
  why = "a system/audit column, which the auto-default set never includes";
1816
1973
  } else if (meta?.hidden) {
1817
1974
  why = "hidden";
@@ -1825,7 +1982,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1825
1982
  rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1826
1983
  where,
1827
1984
  path: `${path}[${i}]`,
1828
- message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data5.SEARCHABLE_TEXTUAL_TYPES, ...import_data5.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).`,
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).`,
1829
1986
  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.`
1830
1987
  });
1831
1988
  }
@@ -1833,7 +1990,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1833
1990
  }
1834
1991
  function validateSearchableFields(stack) {
1835
1992
  const findings = [];
1836
- if (!isRec2(stack)) return findings;
1993
+ if (!isRec3(stack)) return findings;
1837
1994
  const objects = asArray7(stack.objects);
1838
1995
  const fieldsByObject = indexObjectSearchTargets(stack);
1839
1996
  const check = (declared, objectName, where, path, subject, role) => {
@@ -1843,7 +2000,7 @@ function validateSearchableFields(stack) {
1843
2000
  };
1844
2001
  for (let oi = 0; oi < objects.length; oi++) {
1845
2002
  const obj = objects[oi];
1846
- if (!isRec2(obj)) continue;
2003
+ if (!isRec3(obj)) continue;
1847
2004
  const objName = strName3(obj.name);
1848
2005
  const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1849
2006
  check(
@@ -1854,9 +2011,9 @@ function validateSearchableFields(stack) {
1854
2011
  "searchableFields",
1855
2012
  "canonical"
1856
2013
  );
1857
- if (isRec2(obj.listViews)) {
2014
+ if (isRec3(obj.listViews)) {
1858
2015
  for (const [key, lv] of Object.entries(obj.listViews)) {
1859
- if (!isRec2(lv)) continue;
2016
+ if (!isRec3(lv)) continue;
1860
2017
  check(
1861
2018
  lv.searchableFields,
1862
2019
  // A built-in list view belongs to its object; an inline `data.object`
@@ -1873,10 +2030,10 @@ function validateSearchableFields(stack) {
1873
2030
  const views = asArray7(stack.views);
1874
2031
  for (let vi = 0; vi < views.length; vi++) {
1875
2032
  const view = views[vi];
1876
- if (!isRec2(view)) continue;
2033
+ if (!isRec3(view)) continue;
1877
2034
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
1878
2035
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
1879
- if (isRec2(view.list)) {
2036
+ if (isRec3(view.list)) {
1880
2037
  check(
1881
2038
  view.list.searchableFields,
1882
2039
  listViewObject(view.list) ?? viewObject,
@@ -1886,9 +2043,9 @@ function validateSearchableFields(stack) {
1886
2043
  "narrowing"
1887
2044
  );
1888
2045
  }
1889
- if (isRec2(view.listViews)) {
2046
+ if (isRec3(view.listViews)) {
1890
2047
  for (const [key, lv] of Object.entries(view.listViews)) {
1891
- if (!isRec2(lv)) continue;
2048
+ if (!isRec3(lv)) continue;
1892
2049
  check(
1893
2050
  lv.searchableFields,
1894
2051
  listViewObject(lv) ?? viewObject,
@@ -1904,11 +2061,11 @@ function validateSearchableFields(stack) {
1904
2061
  }
1905
2062
  function listViewObject(listView) {
1906
2063
  const data = listView.data;
1907
- return isRec2(data) ? strName3(data.object) : void 0;
2064
+ return isRec3(data) ? strName3(data.object) : void 0;
1908
2065
  }
1909
2066
 
1910
2067
  // src/page-walk.ts
1911
- function isRec3(v) {
2068
+ function isRec4(v) {
1912
2069
  return !!v && typeof v === "object" && !Array.isArray(v);
1913
2070
  }
1914
2071
  function strName4(v) {
@@ -1921,19 +2078,19 @@ function isSourceAuthoredPage(page) {
1921
2078
  }
1922
2079
  function walkPageComponents(page, pagePath) {
1923
2080
  const out = [];
1924
- if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
2081
+ if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
1925
2082
  const pageObject = strName4(page.object);
1926
2083
  const visit = (node, path, inheritedObject) => {
1927
- if (!isRec3(node)) return;
1928
- const props = isRec3(node.properties) ? node.properties : void 0;
1929
- const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
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;
1930
2087
  const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
1931
2088
  out.push({ component: node, path, objectName });
1932
2089
  if (!props) return;
1933
2090
  if (Array.isArray(props.items)) {
1934
2091
  for (let i = 0; i < props.items.length; i++) {
1935
2092
  const item = props.items[i];
1936
- if (!isRec3(item) || !Array.isArray(item.children)) continue;
2093
+ if (!isRec4(item) || !Array.isArray(item.children)) continue;
1937
2094
  for (let c = 0; c < item.children.length; c++) {
1938
2095
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1939
2096
  }
@@ -1955,12 +2112,12 @@ function walkPageComponents(page, pagePath) {
1955
2112
  const regions = Array.isArray(page.regions) ? page.regions : [];
1956
2113
  for (let r = 0; r < regions.length; r++) {
1957
2114
  const region = regions[r];
1958
- if (!isRec3(region) || !Array.isArray(region.components)) continue;
2115
+ if (!isRec4(region) || !Array.isArray(region.components)) continue;
1959
2116
  for (let c = 0; c < region.components.length; c++) {
1960
2117
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1961
2118
  }
1962
2119
  }
1963
- const slots = isRec3(page.slots) ? page.slots : void 0;
2120
+ const slots = isRec4(page.slots) ? page.slots : void 0;
1964
2121
  if (slots) {
1965
2122
  for (const [slot, value] of Object.entries(slots)) {
1966
2123
  const list3 = Array.isArray(value) ? value : [value];
@@ -2168,6 +2325,7 @@ function validateActionNameRefs(stack) {
2168
2325
 
2169
2326
  // src/validate-page-field-bindings.ts
2170
2327
  var PAGE_FIELD_UNKNOWN = "page-field-unknown";
2328
+ var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
2171
2329
  function asArray9(v) {
2172
2330
  if (Array.isArray(v)) return v;
2173
2331
  if (v && typeof v === "object") {
@@ -2178,7 +2336,7 @@ function asArray9(v) {
2178
2336
  function strName6(v) {
2179
2337
  return typeof v === "string" && v.length > 0 ? v : void 0;
2180
2338
  }
2181
- function isRec4(v) {
2339
+ function isRec5(v) {
2182
2340
  return !!v && typeof v === "object" && !Array.isArray(v);
2183
2341
  }
2184
2342
  function fieldRefsFrom(value, basePath) {
@@ -2189,7 +2347,7 @@ function fieldRefsFrom(value, basePath) {
2189
2347
  out.push({ name: bare, path });
2190
2348
  return;
2191
2349
  }
2192
- if (!isRec4(v)) return;
2350
+ if (!isRec5(v)) return;
2193
2351
  const named = strName6(v.field) ?? strName6(v.name);
2194
2352
  if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
2195
2353
  };
@@ -2247,15 +2405,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2247
2405
  const sections = Array.isArray(props[key]) ? props[key] : [];
2248
2406
  for (let si = 0; si < sections.length; si++) {
2249
2407
  const section = sections[si];
2250
- if (!isRec4(section)) continue;
2408
+ if (!isRec5(section)) continue;
2251
2409
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2252
2410
  }
2253
2411
  }
2254
2412
  return refs;
2255
2413
  }
2256
2414
  function relatedListFieldRefs(props, basePath, sep = ".") {
2257
- const add = isRec4(props.add) ? props.add : void 0;
2258
- const picker = add && isRec4(add.picker) ? add.picker : void 0;
2415
+ const add = isRec5(props.add) ? props.add : void 0;
2416
+ const picker = add && isRec5(add.picker) ? add.picker : void 0;
2259
2417
  const at = (key) => `${basePath}${sep}${key}`;
2260
2418
  return {
2261
2419
  relatedObject: strName6(props.objectName),
@@ -2276,7 +2434,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2276
2434
  }
2277
2435
  function indexObjectFields(stack) {
2278
2436
  const objectFields = /* @__PURE__ */ new Map();
2279
- if (!isRec4(stack)) return objectFields;
2437
+ if (!isRec5(stack)) return objectFields;
2280
2438
  for (const obj of asArray9(stack.objects)) {
2281
2439
  const name = strName6(obj.name);
2282
2440
  if (!name) continue;
@@ -2289,14 +2447,27 @@ function indexObjectFields(stack) {
2289
2447
  }
2290
2448
  return objectFields;
2291
2449
  }
2292
- function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
2450
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
2293
2451
  const findings = [];
2294
2452
  if (!objectName) return findings;
2295
2453
  const known = objectFields.get(objectName);
2296
2454
  if (!known) return findings;
2455
+ const anchors = unprovisionedAnchors?.get(objectName);
2297
2456
  for (const ref of refs) {
2298
2457
  if (ref.name.includes(".")) continue;
2299
- if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
2458
+ if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
2459
+ if (anchors?.has(ref.name)) {
2460
+ findings.push({
2461
+ severity: "warning",
2462
+ rule: PAGE_FIELD_UNPROVISIONED,
2463
+ where,
2464
+ path: ref.path,
2465
+ message: `field "${ref.name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, ref.name)}` + (consequence2 === "queried" ? ' \u2014 it is used in a QUERY, so the predicate can never match a real value: on SQLite it silently degrades to constant-false and the surface renders an empty result that looks exactly like "there is no data".' : " \u2014 the component renders it, blank, on every record."),
2466
+ hint: unprovisionedAnchorHint(objectName, ref.name)
2467
+ });
2468
+ }
2469
+ continue;
2470
+ }
2300
2471
  findings.push({
2301
2472
  severity: consequence2 === "queried" ? "error" : "warning",
2302
2473
  rule: PAGE_FIELD_UNKNOWN,
@@ -2312,17 +2483,20 @@ function validatePageFieldBindings(stack) {
2312
2483
  const findings = [];
2313
2484
  if (!stack || typeof stack !== "object") return findings;
2314
2485
  const objectFields = indexObjectFields(stack);
2486
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2315
2487
  const pages = asArray9(stack.pages);
2316
2488
  for (let pi = 0; pi < pages.length; pi++) {
2317
2489
  const page = pages[pi];
2318
2490
  if (!page || typeof page !== "object") continue;
2319
2491
  const pageName = strName6(page.name) ?? `#${pi}`;
2320
2492
  const checkRefs = (refs, objectName, where) => {
2321
- findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
2493
+ findings.push(
2494
+ ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2495
+ );
2322
2496
  };
2323
2497
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2324
2498
  const type = strName6(component.type);
2325
- const props = isRec4(component.properties) ? component.properties : void 0;
2499
+ const props = isRec5(component.properties) ? component.properties : void 0;
2326
2500
  if (!type || !props) continue;
2327
2501
  const where = `page "${pageName}" \xB7 ${type}`;
2328
2502
  const base = `${path}.properties`;
@@ -2337,7 +2511,7 @@ function validatePageFieldBindings(stack) {
2337
2511
  if (!refs) continue;
2338
2512
  checkRefs(refs, objectName, where);
2339
2513
  }
2340
- const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
2514
+ const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2341
2515
  if (cfg) {
2342
2516
  const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2343
2517
  const base = `pages[${pi}].interfaceConfig`;
@@ -2346,7 +2520,7 @@ function validatePageFieldBindings(stack) {
2346
2520
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2347
2521
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2348
2522
  ];
2349
- const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
2523
+ const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2350
2524
  if (userFilters) {
2351
2525
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2352
2526
  }
@@ -2374,7 +2548,7 @@ function strName7(v) {
2374
2548
  function strList2(v) {
2375
2549
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2376
2550
  }
2377
- function isRec5(v) {
2551
+ function isRec6(v) {
2378
2552
  return !!v && typeof v === "object" && !Array.isArray(v);
2379
2553
  }
2380
2554
  function distance4(a, b) {
@@ -2503,10 +2677,10 @@ function validateChartBindings(stack) {
2503
2677
  const reports = asArray10(stack.reports);
2504
2678
  for (let ri = 0; ri < reports.length; ri++) {
2505
2679
  const report = reports[ri];
2506
- if (!isRec5(report)) continue;
2680
+ if (!isRec6(report)) continue;
2507
2681
  const reportName = strName7(report.name) ?? `#${ri}`;
2508
2682
  const checkReportChart = (chart, dataset, values, where, path) => {
2509
- if (!isRec5(chart)) return;
2683
+ if (!isRec6(chart)) return;
2510
2684
  check({
2511
2685
  dataset,
2512
2686
  // `values` is the report's measure SELECTION, not a chart ref; feeding
@@ -2530,7 +2704,7 @@ function validateChartBindings(stack) {
2530
2704
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2531
2705
  for (let bi = 0; bi < blocks.length; bi++) {
2532
2706
  const block = blocks[bi];
2533
- if (!isRec5(block)) continue;
2707
+ if (!isRec6(block)) continue;
2534
2708
  checkReportChart(
2535
2709
  block.chart,
2536
2710
  strName7(block.dataset),
@@ -2541,9 +2715,9 @@ function validateChartBindings(stack) {
2541
2715
  }
2542
2716
  }
2543
2717
  const checkListChart = (container, where, path) => {
2544
- if (!isRec5(container)) return;
2718
+ if (!isRec6(container)) return;
2545
2719
  const chart = container.chart;
2546
- if (!isRec5(chart)) return;
2720
+ if (!isRec6(chart)) return;
2547
2721
  check({
2548
2722
  dataset: strName7(chart.dataset),
2549
2723
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
@@ -2555,10 +2729,10 @@ function validateChartBindings(stack) {
2555
2729
  const views = asArray10(stack.views);
2556
2730
  for (let vi = 0; vi < views.length; vi++) {
2557
2731
  const view = views[vi];
2558
- if (!isRec5(view)) continue;
2732
+ if (!isRec6(view)) continue;
2559
2733
  const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
2560
2734
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2561
- if (isRec5(view.listViews)) {
2735
+ if (isRec6(view.listViews)) {
2562
2736
  for (const [key, lv] of Object.entries(view.listViews)) {
2563
2737
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2564
2738
  }
@@ -2567,7 +2741,7 @@ function validateChartBindings(stack) {
2567
2741
  const objects = asArray10(stack.objects);
2568
2742
  for (let oi = 0; oi < objects.length; oi++) {
2569
2743
  const obj = objects[oi];
2570
- if (!isRec5(obj) || !isRec5(obj.listViews)) continue;
2744
+ if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2571
2745
  const objName = strName7(obj.name) ?? `#${oi}`;
2572
2746
  for (const [key, lv] of Object.entries(obj.listViews)) {
2573
2747
  checkListChart(
@@ -2580,10 +2754,10 @@ function validateChartBindings(stack) {
2580
2754
  const pages = asArray10(stack.pages);
2581
2755
  for (let pi = 0; pi < pages.length; pi++) {
2582
2756
  const page = pages[pi];
2583
- if (!isRec5(page)) continue;
2757
+ if (!isRec6(page)) continue;
2584
2758
  const pageName = strName7(page.name) ?? `#${pi}`;
2585
2759
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2586
- const props = isRec5(component.properties) ? component.properties : void 0;
2760
+ const props = isRec6(component.properties) ? component.properties : void 0;
2587
2761
  if (!props || !strName7(props.dataset)) continue;
2588
2762
  const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
2589
2763
  const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
@@ -2733,10 +2907,10 @@ function validateNavAccess(stack) {
2733
2907
 
2734
2908
  // src/validate-nav-target-refs.ts
2735
2909
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2736
- var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2910
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2737
2911
  function asArray13(v) {
2738
- if (Array.isArray(v)) return v.filter(isRec6);
2739
- if (isRec6(v)) return Object.entries(v).map(([name, def]) => isRec6(def) ? { name, ...def } : { name });
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 });
2740
2914
  return [];
2741
2915
  }
2742
2916
  function strName9(v) {
@@ -2758,7 +2932,7 @@ function namesOf(collection) {
2758
2932
  }
2759
2933
  function validateNavTargetRefs(stack) {
2760
2934
  const findings = [];
2761
- if (!isRec6(stack)) return findings;
2935
+ if (!isRec7(stack)) return findings;
2762
2936
  const apps = asArray13(stack.apps);
2763
2937
  if (apps.length === 0) return findings;
2764
2938
  const declared = /* @__PURE__ */ new Map();
@@ -2770,7 +2944,7 @@ function validateNavTargetRefs(stack) {
2770
2944
  const walk = (items, basePath) => {
2771
2945
  if (!Array.isArray(items)) return;
2772
2946
  for (const [ni, raw] of items.entries()) {
2773
- if (!isRec6(raw)) continue;
2947
+ if (!isRec7(raw)) continue;
2774
2948
  const nav = raw;
2775
2949
  const navPath = `${basePath}[${ni}]`;
2776
2950
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
@@ -2801,32 +2975,100 @@ function validateNavTargetRefs(stack) {
2801
2975
  return findings;
2802
2976
  }
2803
2977
 
2978
+ // src/validate-nav-object-servability.ts
2979
+ var import_data7 = require("@objectstack/spec/data");
2980
+ var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2981
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2982
+ 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 });
2985
+ return [];
2986
+ }
2987
+ function strName10(v) {
2988
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2989
+ }
2990
+ var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2991
+ function validateNavObjectServability(stack) {
2992
+ const findings = [];
2993
+ if (!isRec8(stack)) return findings;
2994
+ const apps = asArray14(stack.apps);
2995
+ if (apps.length === 0) return findings;
2996
+ const ownEnable = /* @__PURE__ */ new Map();
2997
+ const objects = asArray14(stack.objects);
2998
+ for (const [oi, obj] of objects.entries()) {
2999
+ const n = strName10(obj.name);
3000
+ if (!n) continue;
3001
+ ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
3002
+ }
3003
+ if (ownEnable.size === 0) return findings;
3004
+ for (const [ai, app] of apps.entries()) {
3005
+ const appName = strName10(app.name) ?? `#${ai}`;
3006
+ const walk = (items, basePath) => {
3007
+ if (!Array.isArray(items)) return;
3008
+ for (const [ni, raw] of items.entries()) {
3009
+ if (!isRec8(raw)) continue;
3010
+ const nav = raw;
3011
+ const navPath = `${basePath}[${ni}]`;
3012
+ if (nav.type === "object") {
3013
+ const target = strName10(nav.objectName);
3014
+ 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 : {};
3017
+ const apiDisabled = enable.apiEnabled === false;
3018
+ 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
+ const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
3020
+ const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
3021
+ findings.push({
3022
+ severity: "error",
3023
+ rule: NAV_OBJECT_UNSERVABLE,
3024
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3025
+ // The nav entry is where the dead row is authored; the `enable`
3026
+ // key that condemns it is named in the message, because the fix
3027
+ // may belong at either end.
3028
+ path: `${navPath}.objectName`,
3029
+ message: `Navigation targets object "${target}", which cannot serve a list: ${condition} (\`${offendingKey}\`), so the list request answers ${answer} for EVERY user \u2014 platform administrators included, since that gate reads only the object's \`enable\` block and never the caller. The entry cannot be rescued with \`requiredPermissions\`: they are independent conditions. The server prunes this entry from the served \`/meta\` payload (#7912), so publishing it ships a menu row that silently is not there.`,
3030
+ hint: `Remove the nav entry, or make "${target}" listable by setting \`enable.apiEnabled: true\` and granting \`list\` in \`enable.apiMethods\`. \u26D4 Do NOT open the API on an object that is disabled on purpose \u2014 several platform objects hold credential material and are API-disabled deliberately; for those the entry is the mistake, not the \`enable\` block.`
3031
+ });
3032
+ }
3033
+ }
3034
+ if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
3035
+ }
3036
+ };
3037
+ walk(app.navigation, `apps[${ai}].navigation`);
3038
+ for (const [ari, area] of asArray14(app.areas).entries()) {
3039
+ walk(area.items, `apps[${ai}].areas[${ari}].items`);
3040
+ walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
3041
+ }
3042
+ }
3043
+ return findings;
3044
+ }
3045
+
2804
3046
  // src/validate-translation-references.ts
2805
3047
  var import_spec = require("@objectstack/spec");
2806
3048
  var import_system4 = require("@objectstack/spec/system");
2807
3049
 
2808
3050
  // src/view-walk.ts
2809
- function isRec7(v) {
3051
+ function isRec9(v) {
2810
3052
  return !!v && typeof v === "object" && !Array.isArray(v);
2811
3053
  }
2812
- function strName10(v) {
3054
+ function strName11(v) {
2813
3055
  return typeof v === "string" && v.length > 0 ? v : void 0;
2814
3056
  }
2815
3057
  function viewObjectName(view) {
2816
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
3058
+ return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
2817
3059
  }
2818
3060
  function viewContainerSites(view, basePath) {
2819
- if (!isRec7(view)) return [];
3061
+ if (!isRec9(view)) return [];
2820
3062
  const sites = [{ view, path: basePath, surface: "", kind: "self" }];
2821
- if (isRec7(view.form)) {
3063
+ if (isRec9(view.form)) {
2822
3064
  sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
2823
3065
  }
2824
3066
  for (const key of ["listViews", "formViews"]) {
2825
3067
  const container = view[key];
2826
- if (!isRec7(container)) continue;
3068
+ if (!isRec9(container)) continue;
2827
3069
  const kind = key === "listViews" ? "listView" : "formView";
2828
3070
  for (const [subKey, sub] of Object.entries(container)) {
2829
- if (!isRec7(sub)) continue;
3071
+ if (!isRec9(sub)) continue;
2830
3072
  sites.push({
2831
3073
  view: sub,
2832
3074
  path: `${basePath}.${key}.${subKey}`,
@@ -2844,15 +3086,15 @@ function formViewSites(view, basePath) {
2844
3086
  // src/validate-translation-references.ts
2845
3087
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
2846
3088
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
2847
- function isRec8(v) {
3089
+ function isRec10(v) {
2848
3090
  return !!v && typeof v === "object" && !Array.isArray(v);
2849
3091
  }
2850
- function asArray14(v) {
3092
+ function asArray15(v) {
2851
3093
  if (Array.isArray(v)) return v;
2852
- if (isRec8(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec8(def) ? def : {} }));
3094
+ if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
2853
3095
  return [];
2854
3096
  }
2855
- function strName11(v) {
3097
+ function strName12(v) {
2856
3098
  return typeof v === "string" && v.length > 0 ? v : void 0;
2857
3099
  }
2858
3100
  function distance5(a, b) {
@@ -2912,34 +3154,34 @@ function collectViewRecord(view, factsFor) {
2912
3154
  };
2913
3155
  const addSections = (container, binding) => {
2914
3156
  if (!binding) return;
2915
- for (const section of asArray14(container.sections)) {
2916
- const sectionName = strName11(section.name);
3157
+ for (const section of asArray15(container.sections)) {
3158
+ const sectionName = strName12(section.name);
2917
3159
  if (sectionName) factsFor(binding).sections.add(sectionName);
2918
3160
  }
2919
3161
  };
2920
- const listBinding = isRec8(view.list) ? bindingOf(view.list) : void 0;
2921
- if (isRec8(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
2922
- addView(recordObject ?? listBinding, strName11(view.name));
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));
2923
3165
  const named = namedViewKeys(view);
2924
3166
  for (const family of ["listViews", "formViews"]) {
2925
3167
  const container = view[family];
2926
- if (!isRec8(container)) continue;
3168
+ if (!isRec10(container)) continue;
2927
3169
  const registryKeys = family === "listViews" ? named.list : named.form;
2928
3170
  let at = 0;
2929
3171
  for (const sub of Object.values(container)) {
2930
3172
  if (!sub || typeof sub !== "object") continue;
2931
3173
  const registryKey = registryKeys[at++];
2932
- if (!isRec8(sub)) continue;
3174
+ if (!isRec10(sub)) continue;
2933
3175
  const binding = bindingOf(sub) ?? listBinding;
2934
3176
  addView(binding, registryKey);
2935
3177
  addSections(sub, binding);
2936
3178
  }
2937
3179
  }
2938
- if (isRec8(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3180
+ if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2939
3181
  addSections(view, recordObject ?? listBinding);
2940
3182
  }
2941
3183
  function defaultListViewKey(object, container) {
2942
- if (!object || !isRec8(container.list)) return void 0;
3184
+ if (!object || !isRec10(container.list)) return void 0;
2943
3185
  const item = (0, import_spec.expandViewContainer)(object, container).find(
2944
3186
  (i) => i.viewKind === "list" && i.isDefault
2945
3187
  );
@@ -2951,7 +3193,7 @@ function namedViewKeys(container) {
2951
3193
  const object = "probe";
2952
3194
  const prefix = `${object}.`;
2953
3195
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
2954
- const countEntries = (v) => isRec8(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3196
+ const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
2955
3197
  const listCount = countEntries(container.listViews);
2956
3198
  const formCount = countEntries(container.formViews);
2957
3199
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -2969,14 +3211,14 @@ function readOptions(field) {
2969
3211
  values.add(opt);
2970
3212
  continue;
2971
3213
  }
2972
- if (!isRec8(opt)) continue;
2973
- const value = strName11(opt.value);
3214
+ if (!isRec10(opt)) continue;
3215
+ const value = strName12(opt.value);
2974
3216
  if (!value) continue;
2975
3217
  values.add(value);
2976
- const label2 = strName11(opt.label);
3218
+ const label2 = strName12(opt.label);
2977
3219
  if (label2) byLabel.set(label2.toLowerCase(), value);
2978
3220
  }
2979
- } else if (isRec8(raw)) {
3221
+ } else if (isRec10(raw)) {
2980
3222
  for (const [value, label2] of Object.entries(raw)) {
2981
3223
  values.add(value);
2982
3224
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -2996,48 +3238,48 @@ function buildUniverse(stack) {
2996
3238
  }
2997
3239
  return facts;
2998
3240
  };
2999
- for (const obj of asArray14(stack.objects)) {
3000
- const objectName = strName11(obj.name);
3241
+ for (const obj of asArray15(stack.objects)) {
3242
+ const objectName = strName12(obj.name);
3001
3243
  if (!objectName) continue;
3002
3244
  const facts = factsFor(objectName);
3003
- for (const field of asArray14(obj.fields)) {
3004
- const fieldName = strName11(field.name);
3245
+ for (const field of asArray15(obj.fields)) {
3246
+ const fieldName = strName12(field.name);
3005
3247
  if (fieldName) facts.fields.set(fieldName, field);
3006
3248
  }
3007
- for (const action of asArray14(obj.actions)) {
3008
- const actionName = strName11(action.name);
3249
+ for (const action of asArray15(obj.actions)) {
3250
+ const actionName = strName12(action.name);
3009
3251
  if (actionName) facts.actions.set(actionName, action);
3010
3252
  }
3011
- for (const view of asArray14(obj.views)) {
3012
- collectViewRecord({ ...view, object: strName11(view.object) ?? objectName }, factsFor);
3253
+ for (const view of asArray15(obj.views)) {
3254
+ collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
3013
3255
  }
3014
3256
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
3015
- for (const group of asArray14(obj.fieldGroups)) {
3016
- const key = strName11(group.key) ?? strName11(group.name);
3257
+ for (const group of asArray15(obj.fieldGroups)) {
3258
+ const key = strName12(group.key) ?? strName12(group.name);
3017
3259
  if (key) facts.sections.add(key);
3018
3260
  }
3019
3261
  }
3020
- for (const view of asArray14(stack.views)) {
3262
+ for (const view of asArray15(stack.views)) {
3021
3263
  collectViewRecord(view, factsFor);
3022
3264
  }
3023
- const pages = asArray14(stack.pages);
3265
+ const pages = asArray15(stack.pages);
3024
3266
  for (let pi = 0; pi < pages.length; pi++) {
3025
3267
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
3026
3268
  if (!walked.objectName) continue;
3027
- const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
3269
+ const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3028
3270
  if (!props) continue;
3029
- for (const section of asArray14(props.sections)) {
3030
- const sectionName = strName11(section.name);
3271
+ for (const section of asArray15(props.sections)) {
3272
+ const sectionName = strName12(section.name);
3031
3273
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
3032
3274
  }
3033
3275
  }
3034
3276
  }
3035
3277
  const globalActions = /* @__PURE__ */ new Map();
3036
3278
  const actionOwners = /* @__PURE__ */ new Map();
3037
- for (const action of asArray14(stack.actions)) {
3038
- const actionName = strName11(action.name);
3279
+ for (const action of asArray15(stack.actions)) {
3280
+ const actionName = strName12(action.name);
3039
3281
  if (!actionName) continue;
3040
- const owner = strName11(action.objectName) ?? strName11(action.object);
3282
+ const owner = strName12(action.objectName) ?? strName12(action.object);
3041
3283
  if (owner) {
3042
3284
  factsFor(owner).actions.set(actionName, action);
3043
3285
  actionOwners.set(actionName, owner);
@@ -3051,41 +3293,41 @@ function buildUniverse(stack) {
3051
3293
  }
3052
3294
  }
3053
3295
  const apps = /* @__PURE__ */ new Map();
3054
- for (const app of asArray14(stack.apps)) {
3055
- const appName = strName11(app.name);
3296
+ for (const app of asArray15(stack.apps)) {
3297
+ const appName = strName12(app.name);
3056
3298
  if (!appName) continue;
3057
3299
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
3058
3300
  const walkNav = (items) => {
3059
- for (const item of asArray14(items)) {
3060
- const id = strName11(item.id);
3301
+ for (const item of asArray15(items)) {
3302
+ const id = strName12(item.id);
3061
3303
  if (id) navIds.add(id);
3062
3304
  if (item.children) walkNav(item.children);
3063
3305
  }
3064
3306
  };
3065
3307
  walkNav(app.navigation);
3066
- for (const area of asArray14(app.areas)) {
3067
- const areaId = strName11(area.id);
3308
+ for (const area of asArray15(app.areas)) {
3309
+ const areaId = strName12(area.id);
3068
3310
  if (areaId) navIds.add(areaId);
3069
3311
  walkNav(area.navigation);
3070
3312
  }
3071
3313
  apps.set(appName, navIds);
3072
3314
  }
3073
3315
  const dashboards = /* @__PURE__ */ new Map();
3074
- for (const dash of asArray14(stack.dashboards)) {
3075
- const dashName = strName11(dash.name);
3316
+ for (const dash of asArray15(stack.dashboards)) {
3317
+ const dashName = strName12(dash.name);
3076
3318
  if (!dashName) continue;
3077
3319
  const widgets = /* @__PURE__ */ new Set();
3078
- for (const widget of asArray14(dash.widgets)) {
3079
- const id = strName11(widget.id) ?? strName11(widget.name);
3320
+ for (const widget of asArray15(dash.widgets)) {
3321
+ const id = strName12(widget.id) ?? strName12(widget.name);
3080
3322
  if (id) widgets.add(id);
3081
3323
  }
3082
3324
  const actions = /* @__PURE__ */ new Set();
3083
3325
  const headerActions = [
3084
- ...asArray14(isRec8(dash.header) ? dash.header.actions : void 0),
3085
- ...asArray14(dash.actions)
3326
+ ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3327
+ ...asArray15(dash.actions)
3086
3328
  ];
3087
3329
  for (const action of headerActions) {
3088
- const key = strName11(action.actionUrl) ?? strName11(action.url) ?? strName11(action.name);
3330
+ const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
3089
3331
  if (key) actions.add(key);
3090
3332
  }
3091
3333
  dashboards.set(dashName, { widgets, actions });
@@ -3097,7 +3339,7 @@ function localePath(bundleIndex, locale) {
3097
3339
  }
3098
3340
  function validateTranslationReferences(stack) {
3099
3341
  const findings = [];
3100
- if (!isRec8(stack)) return findings;
3342
+ if (!isRec10(stack)) return findings;
3101
3343
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3102
3344
  if (bundles.length === 0) return findings;
3103
3345
  const universe = buildUniverse(stack);
@@ -3106,13 +3348,13 @@ function validateTranslationReferences(stack) {
3106
3348
  };
3107
3349
  for (let bi = 0; bi < bundles.length; bi++) {
3108
3350
  const bundle = bundles[bi];
3109
- if (!isRec8(bundle)) continue;
3351
+ if (!isRec10(bundle)) continue;
3110
3352
  for (const [locale, rawData] of Object.entries(bundle)) {
3111
- if (!isRec8(rawData)) continue;
3353
+ if (!isRec10(rawData)) continue;
3112
3354
  const base = localePath(bi, locale);
3113
3355
  const inLocale = `locale "${locale}"`;
3114
3356
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
3115
- if (!isRec8(rawNode)) continue;
3357
+ if (!isRec10(rawNode)) continue;
3116
3358
  const objPath = `${base}.objects.${objectName}`;
3117
3359
  const facts = universe.objects.get(objectName);
3118
3360
  if (!facts) {
@@ -3138,7 +3380,7 @@ function validateTranslationReferences(stack) {
3138
3380
  );
3139
3381
  continue;
3140
3382
  }
3141
- if (!isRec8(rawField)) continue;
3383
+ if (!isRec10(rawField)) continue;
3142
3384
  checkOptionKeys(findings, {
3143
3385
  optionMap: rawField.options,
3144
3386
  field,
@@ -3220,7 +3462,7 @@ function validateTranslationReferences(stack) {
3220
3462
  );
3221
3463
  continue;
3222
3464
  }
3223
- if (!isRec8(rawApp)) continue;
3465
+ if (!isRec10(rawApp)) continue;
3224
3466
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
3225
3467
  if (navIds.has(navId)) continue;
3226
3468
  orphan(
@@ -3243,7 +3485,7 @@ function validateTranslationReferences(stack) {
3243
3485
  );
3244
3486
  continue;
3245
3487
  }
3246
- if (!isRec8(rawDash)) continue;
3488
+ if (!isRec10(rawDash)) continue;
3247
3489
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
3248
3490
  if (dash.widgets.has(widgetId)) continue;
3249
3491
  orphan(
@@ -3268,7 +3510,7 @@ function validateTranslationReferences(stack) {
3268
3510
  return findings;
3269
3511
  }
3270
3512
  function asRecord(v) {
3271
- return isRec8(v) ? v : {};
3513
+ return isRec10(v) ? v : {};
3272
3514
  }
3273
3515
  function checkOptionKeys(findings, ctx) {
3274
3516
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3280,7 +3522,7 @@ function checkOptionKeys(findings, ctx) {
3280
3522
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3281
3523
  where: ctx.where,
3282
3524
  path: ctx.path,
3283
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName11(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
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.`,
3284
3526
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3285
3527
  });
3286
3528
  return;
@@ -3299,11 +3541,11 @@ function checkOptionKeys(findings, ctx) {
3299
3541
  }
3300
3542
  }
3301
3543
  function checkActionParams(findings, ctx) {
3302
- const rawParams = Object.keys(asRecord(isRec8(ctx.rawAction) ? ctx.rawAction.params : void 0));
3544
+ const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3303
3545
  if (rawParams.length === 0) return;
3304
3546
  const declared = /* @__PURE__ */ new Set();
3305
- for (const param of asArray14(ctx.action.params)) {
3306
- const name = strName11(param.name) ?? strName11(param.field);
3547
+ for (const param of asArray15(ctx.action.params)) {
3548
+ const name = strName12(param.name) ?? strName12(param.field);
3307
3549
  if (name) declared.add(name);
3308
3550
  }
3309
3551
  for (const paramName of rawParams) {
@@ -3320,33 +3562,33 @@ function checkActionParams(findings, ctx) {
3320
3562
  }
3321
3563
 
3322
3564
  // src/collection-entries.ts
3323
- function isRec9(v) {
3565
+ function isRec11(v) {
3324
3566
  return !!v && typeof v === "object" && !Array.isArray(v);
3325
3567
  }
3326
3568
  function collectionEntries(v, base) {
3327
3569
  if (Array.isArray(v)) {
3328
3570
  const out = [];
3329
3571
  for (let i = 0; i < v.length; i++) {
3330
- if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3572
+ if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3331
3573
  }
3332
3574
  return out;
3333
3575
  }
3334
- if (isRec9(v)) {
3335
- return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3576
+ if (isRec11(v)) {
3577
+ return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3336
3578
  }
3337
3579
  return [];
3338
3580
  }
3339
3581
 
3340
3582
  // src/validate-translatable-sections.ts
3341
3583
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3342
- function isRec10(v) {
3584
+ function isRec12(v) {
3343
3585
  return !!v && typeof v === "object" && !Array.isArray(v);
3344
3586
  }
3345
- function strName12(v) {
3587
+ function strName13(v) {
3346
3588
  return typeof v === "string" && v.length > 0 ? v : void 0;
3347
3589
  }
3348
3590
  function viewLabel(view) {
3349
- const name = strName12(view.name);
3591
+ const name = strName13(view.name);
3350
3592
  return name ? `view "${name}"` : "";
3351
3593
  }
3352
3594
  function joinWhere(...parts) {
@@ -3354,7 +3596,7 @@ function joinWhere(...parts) {
3354
3596
  }
3355
3597
  function collectViewSites(view, basePath, label2, sites) {
3356
3598
  const recordObject = viewObjectName(view);
3357
- const listBinding = isRec10(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3599
+ const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3358
3600
  for (const site of viewContainerSites(view, basePath)) {
3359
3601
  sites.push({
3360
3602
  path: `${site.path}.sections`,
@@ -3368,11 +3610,11 @@ function translatedObjectNames(stack) {
3368
3610
  const out = /* @__PURE__ */ new Set();
3369
3611
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3370
3612
  for (const bundle of bundles) {
3371
- if (!isRec10(bundle)) continue;
3613
+ if (!isRec12(bundle)) continue;
3372
3614
  for (const data of Object.values(bundle)) {
3373
- if (!isRec10(data) || !isRec10(data.objects)) continue;
3615
+ if (!isRec12(data) || !isRec12(data.objects)) continue;
3374
3616
  for (const [objectName, node] of Object.entries(data.objects)) {
3375
- if (isRec10(node)) out.add(objectName);
3617
+ if (isRec12(node)) out.add(objectName);
3376
3618
  }
3377
3619
  }
3378
3620
  }
@@ -3384,22 +3626,22 @@ function suggestedName(label2) {
3384
3626
  }
3385
3627
  function validateTranslatableSections(stack) {
3386
3628
  const findings = [];
3387
- if (!isRec10(stack)) return findings;
3629
+ if (!isRec12(stack)) return findings;
3388
3630
  const translated = translatedObjectNames(stack);
3389
3631
  if (translated.size === 0) return findings;
3390
3632
  const sites = [];
3391
3633
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3392
- const objectName = strName12(obj.name);
3634
+ const objectName = strName13(obj.name);
3393
3635
  if (!objectName) continue;
3394
3636
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3395
3637
  collectViewSites(
3396
- { ...view, object: strName12(view.object) ?? objectName },
3638
+ { ...view, object: strName13(view.object) ?? objectName },
3397
3639
  path,
3398
3640
  viewLabel(view),
3399
3641
  sites
3400
3642
  );
3401
3643
  }
3402
- if (isRec10(obj.listViews)) {
3644
+ if (isRec12(obj.listViews)) {
3403
3645
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3404
3646
  }
3405
3647
  }
@@ -3407,13 +3649,13 @@ function validateTranslatableSections(stack) {
3407
3649
  collectViewSites(view, path, viewLabel(view), sites);
3408
3650
  }
3409
3651
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3410
- const pageName = strName12(page.name);
3652
+ const pageName = strName13(page.name);
3411
3653
  const pageLabel = pageName ? `page "${pageName}"` : "";
3412
3654
  for (const walked of walkPageComponents(page, pagePath)) {
3413
3655
  if (!walked.objectName) continue;
3414
- const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3656
+ const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
3415
3657
  if (!props) continue;
3416
- const type = strName12(walked.component.type) ?? "component";
3658
+ const type = strName13(walked.component.type) ?? "component";
3417
3659
  sites.push({
3418
3660
  path: `${walked.path}.properties.sections`,
3419
3661
  surface: joinWhere(pageLabel, type),
@@ -3428,9 +3670,9 @@ function validateTranslatableSections(stack) {
3428
3670
  if (!Array.isArray(site.sections)) continue;
3429
3671
  for (let i = 0; i < site.sections.length; i++) {
3430
3672
  const section = site.sections[i];
3431
- if (!isRec10(section)) continue;
3432
- if (strName12(section.name)) continue;
3433
- const heading = strName12(section.label);
3673
+ if (!isRec12(section)) continue;
3674
+ if (strName13(section.name)) continue;
3675
+ const heading = strName13(section.label);
3434
3676
  if (!heading) continue;
3435
3677
  const slug = suggestedName(heading);
3436
3678
  findings.push({
@@ -3448,10 +3690,10 @@ function validateTranslatableSections(stack) {
3448
3690
 
3449
3691
  // src/flow-walk.ts
3450
3692
  var import_automation2 = require("@objectstack/spec/automation");
3451
- function isRec11(v) {
3693
+ function isRec13(v) {
3452
3694
  return !!v && typeof v === "object" && !Array.isArray(v);
3453
3695
  }
3454
- function strName13(v) {
3696
+ function strName14(v) {
3455
3697
  return typeof v === "string" && v.length > 0 ? v : void 0;
3456
3698
  }
3457
3699
  var REGION_SLOTS = new Map(
@@ -3460,10 +3702,10 @@ var REGION_SLOTS = new Map(
3460
3702
  var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
3461
3703
  var MAX_REGION_DEPTH = 16;
3462
3704
  function flowNodeLabel(node, index) {
3463
- return strName13(node.label) ?? strName13(node.id) ?? `#${index}`;
3705
+ return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
3464
3706
  }
3465
3707
  function stripRegions(config) {
3466
- if (!isRec11(config)) return void 0;
3708
+ if (!isRec13(config)) return void 0;
3467
3709
  let out;
3468
3710
  for (const key of Object.keys(config)) {
3469
3711
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3474,11 +3716,11 @@ function stripRegions(config) {
3474
3716
  }
3475
3717
  function walkFlowNodes(flow, flowPath) {
3476
3718
  const out = [];
3477
- if (!isRec11(flow)) return out;
3719
+ if (!isRec13(flow)) return out;
3478
3720
  const visitList = (nodes, basePath, trail, depth) => {
3479
3721
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3480
3722
  nodes.forEach((raw, index) => {
3481
- if (!isRec11(raw)) return;
3723
+ if (!isRec13(raw)) return;
3482
3724
  const path = `${basePath}[${index}]`;
3483
3725
  out.push({
3484
3726
  node: raw,
@@ -3487,9 +3729,9 @@ function walkFlowNodes(flow, flowPath) {
3487
3729
  regionTrail: trail,
3488
3730
  depth
3489
3731
  });
3490
- const type = strName13(raw.type);
3732
+ const type = strName14(raw.type);
3491
3733
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3492
- if (!slots || !isRec11(raw.config)) return;
3734
+ if (!slots || !isRec13(raw.config)) return;
3493
3735
  const config = raw.config;
3494
3736
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3495
3737
  for (const slot of slots) {
@@ -3497,8 +3739,8 @@ function walkFlowNodes(flow, flowPath) {
3497
3739
  if (slot === "branches") {
3498
3740
  if (!Array.isArray(value)) continue;
3499
3741
  value.forEach((branch, b) => {
3500
- if (!isRec11(branch)) return;
3501
- const branchName = strName13(branch.name) ?? `#${b}`;
3742
+ if (!isRec13(branch)) return;
3743
+ const branchName = strName14(branch.name) ?? `#${b}`;
3502
3744
  visitList(
3503
3745
  branch.nodes,
3504
3746
  `${path}.config.branches[${b}].nodes`,
@@ -3508,7 +3750,7 @@ function walkFlowNodes(flow, flowPath) {
3508
3750
  });
3509
3751
  continue;
3510
3752
  }
3511
- if (!isRec11(value)) continue;
3753
+ if (!isRec13(value)) continue;
3512
3754
  visitList(
3513
3755
  value.nodes,
3514
3756
  `${path}.config.${slot}.nodes`,
@@ -3528,7 +3770,8 @@ function joinTrail(trail, segment) {
3528
3770
  // src/validate-flow-template-paths.ts
3529
3771
  var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
3530
3772
  var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
3531
- function asArray15(v) {
3773
+ var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
3774
+ function asArray16(v) {
3532
3775
  if (Array.isArray(v)) return v;
3533
3776
  if (v && typeof v === "object") {
3534
3777
  return Object.entries(v).map(([name, def]) => ({
@@ -3557,7 +3800,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
3557
3800
  ]);
3558
3801
  function fieldTypesOf(obj) {
3559
3802
  const types = /* @__PURE__ */ new Map();
3560
- for (const f of asArray15(obj.fields)) {
3803
+ for (const f of asArray16(obj.fields)) {
3561
3804
  if (typeof f.name === "string") {
3562
3805
  types.set(f.name, typeof f.type === "string" ? f.type : "");
3563
3806
  }
@@ -3654,10 +3897,10 @@ function declaredExpandOf(flow) {
3654
3897
  }
3655
3898
  function validateFlowTemplatePaths(stack) {
3656
3899
  const findings = [];
3657
- const flows = asArray15(stack.flows);
3900
+ const flows = asArray16(stack.flows);
3658
3901
  if (flows.length === 0) return findings;
3659
3902
  const objectsByName = /* @__PURE__ */ new Map();
3660
- for (const obj of asArray15(stack.objects)) {
3903
+ for (const obj of asArray16(stack.objects)) {
3661
3904
  if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
3662
3905
  }
3663
3906
  flows.forEach((flow, flowIndex) => {
@@ -3670,6 +3913,7 @@ function validateFlowTemplatePaths(stack) {
3670
3913
  const obj = objectsByName.get(objectName);
3671
3914
  if (!obj) return;
3672
3915
  const fieldTypes = fieldTypesOf(obj);
3916
+ const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
3673
3917
  const expandSet = declaredExpandOf(flow);
3674
3918
  walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
3675
3919
  const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
@@ -3681,6 +3925,7 @@ function validateFlowTemplatePaths(stack) {
3681
3925
  if (leaves.length === 0) return;
3682
3926
  const seenUnknown = /* @__PURE__ */ new Set();
3683
3927
  const seenTraversal = /* @__PURE__ */ new Set();
3928
+ const seenUnprovisioned = /* @__PURE__ */ new Set();
3684
3929
  for (const leaf of leaves) {
3685
3930
  const inFilter = leaf.inFilter;
3686
3931
  for (const rest of recordRefsIn(leaf.text)) {
@@ -3688,6 +3933,19 @@ function validateFlowTemplatePaths(stack) {
3688
3933
  const hasSubPath = rest.length > 1;
3689
3934
  const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
3690
3935
  const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
3936
+ if (unprovisionedAnchors.has(head)) {
3937
+ if (!seenUnprovisioned.has(head)) {
3938
+ seenUnprovisioned.add(head);
3939
+ findings.push({
3940
+ severity: "warning",
3941
+ rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
3942
+ where,
3943
+ path: nodePath,
3944
+ message: (inFilter ? `${nodeType} filter references ` : "template references ") + `'{record.${rest.join(".")}}', and ${unprovisionedAnchorCause(objectName, head)} \u2014 ` + (inFilter ? `the token resolves to nothing on every run, which DROPS the condition from the query instead of narrowing it; the node then refuses to run at execution time (#3810).` : `the token resolves to an empty string on every run (silently).`),
3945
+ hint: unprovisionedAnchorHint(objectName, head)
3946
+ });
3947
+ }
3948
+ }
3691
3949
  if (!isKnown) {
3692
3950
  if (seenUnknown.has(head)) continue;
3693
3951
  seenUnknown.add(head);
@@ -3726,14 +3984,14 @@ function validateFlowTemplatePaths(stack) {
3726
3984
 
3727
3985
  // src/validate-ai-surface-affinity.ts
3728
3986
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
3729
- function asArray16(v) {
3987
+ function asArray17(v) {
3730
3988
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3731
3989
  if (v && typeof v === "object") {
3732
3990
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3733
3991
  }
3734
3992
  return [];
3735
3993
  }
3736
- function strName14(v) {
3994
+ function strName15(v) {
3737
3995
  return typeof v === "string" && v.length > 0 ? v : void 0;
3738
3996
  }
3739
3997
  function surfaceOf(v) {
@@ -3743,18 +4001,18 @@ function validateAiSurfaceAffinity(stack) {
3743
4001
  const findings = [];
3744
4002
  if (!stack || typeof stack !== "object") return findings;
3745
4003
  const skillsByName = /* @__PURE__ */ new Map();
3746
- for (const skill of asArray16(stack.skills)) {
3747
- const n = strName14(skill.name);
4004
+ for (const skill of asArray17(stack.skills)) {
4005
+ const n = strName15(skill.name);
3748
4006
  if (n) skillsByName.set(n, skill);
3749
4007
  }
3750
- const agents = asArray16(stack.agents);
4008
+ const agents = asArray17(stack.agents);
3751
4009
  for (let ai = 0; ai < agents.length; ai++) {
3752
4010
  const agent = agents[ai];
3753
- const agentName = strName14(agent.name) ?? `#${ai}`;
4011
+ const agentName = strName15(agent.name) ?? `#${ai}`;
3754
4012
  const agentSurface = surfaceOf(agent.surface);
3755
4013
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
3756
4014
  for (let si = 0; si < skillRefs.length; si++) {
3757
- const ref = strName14(skillRefs[si]);
4015
+ const ref = strName15(skillRefs[si]);
3758
4016
  if (!ref) continue;
3759
4017
  const skill = skillsByName.get(ref);
3760
4018
  if (!skill) continue;
@@ -3776,14 +4034,14 @@ function validateAiSurfaceAffinity(stack) {
3776
4034
  // src/validate-ai-tool-references.ts
3777
4035
  var import_system5 = require("@objectstack/spec/system");
3778
4036
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
3779
- function asArray17(v) {
4037
+ function asArray18(v) {
3780
4038
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3781
4039
  if (v && typeof v === "object") {
3782
4040
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3783
4041
  }
3784
4042
  return [];
3785
4043
  }
3786
- function strName15(v) {
4044
+ function strName16(v) {
3787
4045
  return typeof v === "string" && v.length > 0 ? v : void 0;
3788
4046
  }
3789
4047
  function distance6(a, b) {
@@ -3824,26 +4082,26 @@ function materialisesAsTool(action) {
3824
4082
  if (!ai || typeof ai !== "object") return false;
3825
4083
  const aiRec = ai;
3826
4084
  if (aiRec.exposed !== true) return false;
3827
- if (!strName15(aiRec.description)) return false;
3828
- const type = strName15(action.type);
4085
+ if (!strName16(aiRec.description)) return false;
4086
+ const type = strName16(action.type);
3829
4087
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
3830
4088
  if (type === "script") return Boolean(action.target || action.body);
3831
4089
  return Boolean(action.target);
3832
4090
  }
3833
4091
  function collectToolUniverse(stack) {
3834
4092
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
3835
- for (const tool of asArray17(stack.tools)) {
3836
- const n = strName15(tool.name);
4093
+ for (const tool of asArray18(stack.tools)) {
4094
+ const n = strName16(tool.name);
3837
4095
  if (n) universe.add(n);
3838
4096
  }
3839
4097
  const addActionFamily = (actions) => {
3840
- for (const action of asArray17(actions)) {
3841
- const n = strName15(action.name);
4098
+ for (const action of asArray18(actions)) {
4099
+ const n = strName16(action.name);
3842
4100
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
3843
4101
  }
3844
4102
  };
3845
4103
  addActionFamily(stack.actions);
3846
- for (const obj of asArray17(stack.objects)) {
4104
+ for (const obj of asArray18(stack.objects)) {
3847
4105
  addActionFamily(obj.actions);
3848
4106
  }
3849
4107
  return universe;
@@ -3851,13 +4109,13 @@ function collectToolUniverse(stack) {
3851
4109
  function collectUnexposedActionNames(stack) {
3852
4110
  const names = /* @__PURE__ */ new Set();
3853
4111
  const scan = (actions) => {
3854
- for (const action of asArray17(actions)) {
3855
- const n = strName15(action.name);
4112
+ for (const action of asArray18(actions)) {
4113
+ const n = strName16(action.name);
3856
4114
  if (n && !materialisesAsTool(action)) names.add(n);
3857
4115
  }
3858
4116
  };
3859
4117
  scan(stack.actions);
3860
- for (const obj of asArray17(stack.objects)) scan(obj.actions);
4118
+ for (const obj of asArray18(stack.objects)) scan(obj.actions);
3861
4119
  return names;
3862
4120
  }
3863
4121
  function validateAiToolReferences(stack) {
@@ -3875,13 +4133,13 @@ function validateAiToolReferences(stack) {
3875
4133
  }
3876
4134
  return universe.has(ref);
3877
4135
  };
3878
- const skills = asArray17(stack.skills);
4136
+ const skills = asArray18(stack.skills);
3879
4137
  for (let si = 0; si < skills.length; si++) {
3880
4138
  const skill = skills[si];
3881
- const skillName = strName15(skill.name) ?? `#${si}`;
4139
+ const skillName = strName16(skill.name) ?? `#${si}`;
3882
4140
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
3883
4141
  for (let ti = 0; ti < refs.length; ti++) {
3884
- const ref = strName15(refs[ti]);
4142
+ const ref = strName16(refs[ti]);
3885
4143
  if (!ref || resolves(ref)) continue;
3886
4144
  const isPattern = ref.endsWith("*");
3887
4145
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -3901,24 +4159,24 @@ function validateAiToolReferences(stack) {
3901
4159
  // src/validate-ai-agent-authoring.ts
3902
4160
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
3903
4161
  var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
3904
- function asArray18(v) {
4162
+ function asArray19(v) {
3905
4163
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3906
4164
  if (v && typeof v === "object") {
3907
4165
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3908
4166
  }
3909
4167
  return [];
3910
4168
  }
3911
- function strName16(v) {
4169
+ function strName17(v) {
3912
4170
  return typeof v === "string" && v.length > 0 ? v : void 0;
3913
4171
  }
3914
4172
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
3915
4173
  function validateAiAgentAuthoring(stack) {
3916
4174
  const findings = [];
3917
4175
  if (!stack || typeof stack !== "object") return findings;
3918
- const agents = asArray18(stack.agents);
4176
+ const agents = asArray19(stack.agents);
3919
4177
  for (let ai = 0; ai < agents.length; ai++) {
3920
4178
  const agent = agents[ai];
3921
- const name = strName16(agent.name) ?? `#${ai}`;
4179
+ const name = strName17(agent.name) ?? `#${ai}`;
3922
4180
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
3923
4181
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
3924
4182
  findings.push({
@@ -3931,12 +4189,12 @@ function validateAiAgentAuthoring(stack) {
3931
4189
  });
3932
4190
  }
3933
4191
  const roster = [...PLATFORM_AGENT_NAMES].join(", ");
3934
- const apps = asArray18(stack.apps);
4192
+ const apps = asArray19(stack.apps);
3935
4193
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
3936
4194
  const app = apps[appIdx];
3937
- const defaultAgent = strName16(app.defaultAgent);
4195
+ const defaultAgent = strName17(app.defaultAgent);
3938
4196
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
3939
- const appName = strName16(app.name) ?? `#${appIdx}`;
4197
+ const appName = strName17(app.name) ?? `#${appIdx}`;
3940
4198
  findings.push({
3941
4199
  severity: "warning",
3942
4200
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -4033,24 +4291,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
4033
4291
  "owner",
4034
4292
  "record_type"
4035
4293
  ]);
4036
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4037
- function asArray19(v) {
4038
- if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4039
- if (isRec12(v)) {
4294
+ var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4295
+ function asArray20(v) {
4296
+ if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4297
+ if (isRec14(v)) {
4040
4298
  return Object.entries(v).map(([name, def]) => ({
4041
4299
  name,
4042
- ...isRec12(def) ? def : {}
4300
+ ...isRec14(def) ? def : {}
4043
4301
  }));
4044
4302
  }
4045
4303
  return [];
4046
4304
  }
4047
4305
  function indexObjectFields2(stack) {
4048
4306
  const out = /* @__PURE__ */ new Map();
4049
- for (const obj of asArray19(stack.objects)) {
4307
+ for (const obj of asArray20(stack.objects)) {
4050
4308
  const name = typeof obj.name === "string" ? obj.name : void 0;
4051
4309
  if (!name) continue;
4052
4310
  const names = /* @__PURE__ */ new Set();
4053
- for (const f of asArray19(obj.fields)) {
4311
+ for (const f of asArray20(obj.fields)) {
4054
4312
  if (typeof f.name === "string" && f.name) names.add(f.name);
4055
4313
  }
4056
4314
  out.set(name, names);
@@ -4180,12 +4438,12 @@ ${source}
4180
4438
  }
4181
4439
  function validateHookBodyWrites(stack) {
4182
4440
  const findings = [];
4183
- const hooks = asArray19(stack.hooks);
4441
+ const hooks = asArray20(stack.hooks);
4184
4442
  if (hooks.length === 0) return findings;
4185
4443
  let objectFields = null;
4186
4444
  hooks.forEach((hook, hookIndex) => {
4187
4445
  const body = hook.body;
4188
- if (!isRec12(body) || body.language !== "js") return;
4446
+ if (!isRec14(body) || body.language !== "js") return;
4189
4447
  const source = body.source;
4190
4448
  if (typeof source !== "string" || source.trim() === "") return;
4191
4449
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -4256,13 +4514,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
4256
4514
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
4257
4515
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
4258
4516
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
4259
- var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4260
- function asArray20(v) {
4261
- if (Array.isArray(v)) return v.filter((x) => isRec13(x));
4262
- if (isRec13(v)) {
4517
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4518
+ function asArray21(v) {
4519
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4520
+ if (isRec15(v)) {
4263
4521
  return Object.entries(v).map(([name, def]) => ({
4264
4522
  name,
4265
- ...isRec13(def) ? def : {}
4523
+ ...isRec15(def) ? def : {}
4266
4524
  }));
4267
4525
  }
4268
4526
  return [];
@@ -4276,11 +4534,11 @@ function collectActionBodies(stack) {
4276
4534
  const sites = [];
4277
4535
  const seen = /* @__PURE__ */ new Set();
4278
4536
  const collect = (actions, pathPrefix, parentObject) => {
4279
- asArray20(actions).forEach((action, index) => {
4537
+ asArray21(actions).forEach((action, index) => {
4280
4538
  const type = typeof action.type === "string" ? action.type : "script";
4281
4539
  if (type !== "script") return;
4282
4540
  const body = action.body;
4283
- if (!isRec13(body) || body.language !== "js") return;
4541
+ if (!isRec15(body) || body.language !== "js") return;
4284
4542
  const source = body.source;
4285
4543
  if (typeof source !== "string" || source.trim() === "") return;
4286
4544
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4291,7 +4549,7 @@ function collectActionBodies(stack) {
4291
4549
  });
4292
4550
  };
4293
4551
  collect(stack.actions, "actions");
4294
- asArray20(stack.objects).forEach((obj, objIndex) => {
4552
+ asArray21(stack.objects).forEach((obj, objIndex) => {
4295
4553
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
4296
4554
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
4297
4555
  });
@@ -4299,7 +4557,7 @@ function collectActionBodies(stack) {
4299
4557
  }
4300
4558
  function validateActionBodyWrites(stack) {
4301
4559
  const findings = [];
4302
- if (!isRec13(stack)) return findings;
4560
+ if (!isRec15(stack)) return findings;
4303
4561
  const sites = collectActionBodies(stack);
4304
4562
  if (sites.length === 0) return findings;
4305
4563
  let objectFields = null;
@@ -4357,13 +4615,13 @@ function fixHint2(field, declared) {
4357
4615
  var import_shared3 = require("@objectstack/spec/shared");
4358
4616
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4359
4617
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4360
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4361
- function asArray21(v) {
4362
- if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4363
- if (isRec14(v)) {
4618
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4619
+ function asArray22(v) {
4620
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4621
+ if (isRec16(v)) {
4364
4622
  return Object.entries(v).map(([name, def]) => ({
4365
4623
  name,
4366
- ...isRec14(def) ? def : {}
4624
+ ...isRec16(def) ? def : {}
4367
4625
  }));
4368
4626
  }
4369
4627
  return [];
@@ -4376,8 +4634,8 @@ function readLiteralObjectName(config) {
4376
4634
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4377
4635
  function validateFlowNodeWrites(stack) {
4378
4636
  const findings = [];
4379
- if (!isRec14(stack)) return findings;
4380
- const flows = asArray21(stack.flows);
4637
+ if (!isRec16(stack)) return findings;
4638
+ const flows = asArray22(stack.flows);
4381
4639
  if (flows.length === 0) return findings;
4382
4640
  let objectFields = null;
4383
4641
  flows.forEach((flow, flowIndex) => {
@@ -4385,10 +4643,10 @@ function validateFlowNodeWrites(stack) {
4385
4643
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4386
4644
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4387
4645
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4388
- const config = isRec14(node.config) ? node.config : void 0;
4646
+ const config = isRec16(node.config) ? node.config : void 0;
4389
4647
  if (!config) return;
4390
4648
  const fields = config.fields;
4391
- if (!isRec14(fields)) return;
4649
+ if (!isRec16(fields)) return;
4392
4650
  const written = Object.keys(fields);
4393
4651
  if (written.length === 0) return;
4394
4652
  const objectName = readLiteralObjectName(config);
@@ -4422,7 +4680,7 @@ function fixHint3(field, declared) {
4422
4680
  // src/validate-readonly-flow-writes.ts
4423
4681
  var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
4424
4682
  var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
4425
- function asArray22(v) {
4683
+ function asArray23(v) {
4426
4684
  if (Array.isArray(v)) return v;
4427
4685
  if (v && typeof v === "object") {
4428
4686
  return Object.entries(v).map(([name, def]) => ({
@@ -4463,9 +4721,9 @@ function readLiteralObjectName2(config) {
4463
4721
  }
4464
4722
  function validateReadonlyFlowWrites(stack) {
4465
4723
  const findings = [];
4466
- const flows = asArray22(stack.flows);
4724
+ const flows = asArray23(stack.flows);
4467
4725
  if (flows.length === 0) return findings;
4468
- const roIndex = buildReadonlyIndex(asArray22(stack.objects));
4726
+ const roIndex = buildReadonlyIndex(asArray23(stack.objects));
4469
4727
  flows.forEach((flow, flowIndex) => {
4470
4728
  if (flow.runAs === "system") return;
4471
4729
  const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
@@ -4513,14 +4771,14 @@ function validateReadonlyFlowWrites(stack) {
4513
4771
  // src/validate-react-page-props.ts
4514
4772
  var import_node_module2 = require("module");
4515
4773
  var import_ui2 = require("@objectstack/spec/ui");
4516
- var import_data6 = require("@objectstack/spec/data");
4774
+ var import_data8 = require("@objectstack/spec/data");
4517
4775
 
4518
4776
  // src/zod-issue-format.ts
4519
- var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4777
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4520
4778
  var valueAtPath = (root, path) => {
4521
4779
  let cur = root;
4522
4780
  for (const key of path) {
4523
- if (!isRec15(cur) && !Array.isArray(cur)) return void 0;
4781
+ if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
4524
4782
  cur = cur[key];
4525
4783
  }
4526
4784
  return cur;
@@ -4566,7 +4824,7 @@ function loadTypeScript2() {
4566
4824
  }
4567
4825
  return cachedTs2;
4568
4826
  }
4569
- var asArray23 = (v) => Array.isArray(v) ? v : [];
4827
+ var asArray24 = (v) => Array.isArray(v) ? v : [];
4570
4828
  var BLOCKS = new Map(
4571
4829
  import_ui2.REACT_BLOCKS.map((b) => [
4572
4830
  b.tag,
@@ -4654,12 +4912,13 @@ function filterAttrValue(tsc, sf, attr) {
4654
4912
  return perPosition(init.expression);
4655
4913
  }
4656
4914
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
4915
+ var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
4657
4916
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
4658
4917
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4659
4918
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4660
4919
  function checkChartDrillDown(raw, push2) {
4661
4920
  if (raw === void 0 || raw === NOT_STATIC) return;
4662
- if (!isRec16(raw)) {
4921
+ if (!isRec18(raw)) {
4663
4922
  push2(
4664
4923
  "error",
4665
4924
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4682,7 +4941,7 @@ function checkChartDrillDown(raw, push2) {
4682
4941
  }
4683
4942
  function checkChartAggregate(raw, push2) {
4684
4943
  if (raw === void 0 || raw === NOT_STATIC) return;
4685
- if (!isRec16(raw)) {
4944
+ if (!isRec18(raw)) {
4686
4945
  push2(
4687
4946
  "error",
4688
4947
  REACT_CHART_AGGREGATE_INVALID,
@@ -4713,9 +4972,9 @@ function checkChartAggregate(raw, push2) {
4713
4972
  );
4714
4973
  }
4715
4974
  }
4716
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4975
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4717
4976
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4718
- function checkObjectChart(attrs, objectFields, findings) {
4977
+ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4719
4978
  const { values, where, path } = attrs;
4720
4979
  const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
4721
4980
  checkChartDrillDown(values.get("drillDown"), push2);
@@ -4723,18 +4982,29 @@ function checkObjectChart(attrs, objectFields, findings) {
4723
4982
  const aggregate = values.get("aggregate");
4724
4983
  checkChartAggregate(aggregate, push2);
4725
4984
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4726
- if (!isRec16(aggregate)) return;
4985
+ if (!isRec18(aggregate)) return;
4727
4986
  const fn = strOf(aggregate.function);
4728
4987
  const field = strOf(aggregate.field);
4729
4988
  const groupBy = aggregate.groupBy;
4730
- const groupByField = strOf(groupBy) ?? (isRec16(groupBy) ? strOf(groupBy.field) : void 0);
4989
+ const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
4731
4990
  const objectName = strOf(values.get("objectName"));
4732
4991
  const known = objectName ? objectFields.get(objectName) : void 0;
4733
4992
  if (objectName && known) {
4993
+ const anchors = unprovisionedAnchors.get(objectName);
4734
4994
  const fieldRef = (name, prop) => {
4735
4995
  if (!name) return;
4736
4996
  if (name.includes(".")) return;
4737
- if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
4997
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) {
4998
+ if (anchors?.has(name)) {
4999
+ push2(
5000
+ "warning",
5001
+ REACT_CHART_FIELD_UNPROVISIONED,
5002
+ `aggregate.${prop} "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 the aggregate query reads a column that is empty on every row, so the chart ${prop === "groupBy" ? "groups everything into one empty bucket" : "aggregates nothing"} instead of failing.`,
5003
+ unprovisionedAnchorHint(objectName, name)
5004
+ );
5005
+ }
5006
+ return;
5007
+ }
4738
5008
  push2(
4739
5009
  "error",
4740
5010
  REACT_CHART_FIELD_UNKNOWN,
@@ -4760,18 +5030,18 @@ function checkObjectChart(attrs, objectFields, findings) {
4760
5030
  );
4761
5031
  };
4762
5032
  const xAxisRaw = values.get("xAxis");
4763
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec16(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5033
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4764
5034
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
4765
5035
  axisRef(categoryAxis, categoryProp);
4766
5036
  const yAxisRaw = values.get("yAxis");
4767
5037
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
4768
5038
  for (const a of yAxisList) {
4769
- axisRef(strOf(a) ?? (isRec16(a) ? strOf(a.field) : void 0), "yAxis[].field");
5039
+ axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
4770
5040
  }
4771
5041
  const series = values.get("series");
4772
5042
  if (Array.isArray(series)) {
4773
5043
  for (const s of series) {
4774
- if (!isRec16(s)) continue;
5044
+ if (!isRec18(s)) continue;
4775
5045
  const dataKey = strOf(s.dataKey);
4776
5046
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
4777
5047
  }
@@ -4827,7 +5097,7 @@ function subformFieldRefs(value, basePath) {
4827
5097
  if (!Array.isArray(value)) return { child, parent };
4828
5098
  for (let i = 0; i < value.length; i++) {
4829
5099
  const sub = value[i];
4830
- if (!isRec16(sub)) continue;
5100
+ if (!isRec18(sub)) continue;
4831
5101
  const at = (key) => `${basePath}[${i}].${key}`;
4832
5102
  child.push({
4833
5103
  objectName: strOf(sub.childObject),
@@ -4852,7 +5122,7 @@ function filterFieldRefs(node, basePath, out) {
4852
5122
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
4853
5123
  return;
4854
5124
  }
4855
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data6.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5125
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
4856
5126
  out.push({ name: head, path: `${basePath}[0]` });
4857
5127
  }
4858
5128
  }
@@ -4872,20 +5142,20 @@ function reactFieldRefs(spec, values, basePath) {
4872
5142
  }
4873
5143
  for (const key of spec.nestedFields ?? []) {
4874
5144
  const v = readable(key);
4875
- if (isRec16(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5145
+ if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4876
5146
  }
4877
5147
  for (const key of spec.sections ?? []) {
4878
5148
  const v = readable(key);
4879
5149
  if (!Array.isArray(v)) continue;
4880
5150
  for (let i = 0; i < v.length; i++) {
4881
5151
  const section = v[i];
4882
- if (!isRec16(section)) continue;
5152
+ if (!isRec18(section)) continue;
4883
5153
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
4884
5154
  }
4885
5155
  }
4886
5156
  for (const key of spec.keyedByField ?? []) {
4887
5157
  const v = readable(key);
4888
- if (!isRec16(v)) continue;
5158
+ if (!isRec18(v)) continue;
4889
5159
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
4890
5160
  }
4891
5161
  for (const key of spec.filterArrays ?? []) {
@@ -4893,22 +5163,30 @@ function reactFieldRefs(spec, values, basePath) {
4893
5163
  }
4894
5164
  return { own, queried };
4895
5165
  }
4896
- function checkBlockFieldProps(tag, values, objectFields, where, path) {
5166
+ function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
4897
5167
  const objectName = strOf(values.get("objectName"));
4898
5168
  const out = [];
4899
5169
  const spec = REACT_FIELD_SPECS[tag];
4900
5170
  if (spec) {
4901
5171
  const { own, queried } = reactFieldRefs(spec, values, path);
4902
- out.push(...checkFieldRefs(own, objectName, objectFields, where));
4903
- out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
5172
+ out.push(
5173
+ ...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
5174
+ );
5175
+ out.push(
5176
+ ...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
5177
+ );
4904
5178
  }
4905
5179
  if (tag === "ObjectForm") {
4906
5180
  const raw = values.get("subforms");
4907
5181
  const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
4908
5182
  for (const sub of subs.child) {
4909
- out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
5183
+ out.push(
5184
+ ...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
5185
+ );
4910
5186
  }
4911
- out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
5187
+ out.push(
5188
+ ...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
5189
+ );
4912
5190
  }
4913
5191
  const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
4914
5192
  if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
@@ -4917,7 +5195,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
4917
5195
  componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
4918
5196
  objectName,
4919
5197
  objectFields,
4920
- where
5198
+ where,
5199
+ "skipped",
5200
+ unprovisionedAnchors
4921
5201
  )
4922
5202
  );
4923
5203
  }
@@ -4949,8 +5229,9 @@ function localComponentNames(tsc, sf) {
4949
5229
  function validateReactPageProps(stack) {
4950
5230
  const findings = [];
4951
5231
  const objectFields = indexObjectFields(stack);
5232
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
4952
5233
  const searchTargets = indexObjectSearchTargets(stack);
4953
- const pages = asArray23(stack.pages);
5234
+ const pages = asArray24(stack.pages);
4954
5235
  for (let p = 0; p < pages.length; p++) {
4955
5236
  const page = pages[p];
4956
5237
  if (!page || page.kind !== "react") continue;
@@ -5031,7 +5312,7 @@ function validateReactPageProps(stack) {
5031
5312
  }
5032
5313
  }
5033
5314
  if (tag === "ObjectChart" && !hasSpread) {
5034
- checkObjectChart({ values, where, path }, objectFields, findings);
5315
+ checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
5035
5316
  }
5036
5317
  if (tag === "ListView" && !hasSpread) {
5037
5318
  findings.push(
@@ -5047,7 +5328,7 @@ function validateReactPageProps(stack) {
5047
5328
  }
5048
5329
  if (!hasSpread) {
5049
5330
  findings.push(
5050
- ...checkBlockFieldProps(tag, values, objectFields, where, path)
5331
+ ...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
5051
5332
  );
5052
5333
  }
5053
5334
  }
@@ -5074,6 +5355,15 @@ var REFERENCE_INTEGRITY_RULES = [
5074
5355
  // `action` is deliberately absent (validateActionNameRefs owns it) and so is
5075
5356
  // `component` (an unregistered ref renders a named diagnostic, not silence).
5076
5357
  { name: "validateNavTargetRefs", run: validateNavTargetRefs },
5358
+ // [#7912] The THIRD question about a nav entry, after "does the target
5359
+ // resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
5360
+ // destination serve at all? An object's own `enable` block can make its list
5361
+ // answer 404/405 for every persona, and no gate authorable on the entry
5362
+ // expresses that — which is how #7544's dead row survived review for a year.
5363
+ // The server now prunes such an entry from the `/meta` payload; the
5364
+ // maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
5365
+ // prune is never silent to the author who wrote the row.
5366
+ { name: "validateNavObjectServability", run: validateNavObjectServability },
5077
5367
  { name: "validateTranslationReferences", run: validateTranslationReferences },
5078
5368
  // The same family from the other end (#5417). Its sibling above asks "does
5079
5369
  // this bundle key resolve?"; this one asks "is there a key at all?" — a form
@@ -5172,13 +5462,13 @@ var import_ui3 = require("@objectstack/spec/ui");
5172
5462
  var import_spec2 = require("@objectstack/spec");
5173
5463
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5174
5464
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
5175
- function isRec17(v) {
5465
+ function isRec19(v) {
5176
5466
  return !!v && typeof v === "object" && !Array.isArray(v);
5177
5467
  }
5178
- function strName17(v) {
5468
+ function strName18(v) {
5179
5469
  return typeof v === "string" && v.length > 0 ? v : void 0;
5180
5470
  }
5181
- function asArray24(v) {
5471
+ function asArray25(v) {
5182
5472
  if (Array.isArray(v)) return v;
5183
5473
  if (v && typeof v === "object") {
5184
5474
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5189,23 +5479,36 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
5189
5479
  var DATASOURCE_SUPPLIED_PROP = "object";
5190
5480
  function suppliedByDataSource(issue, component) {
5191
5481
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
5192
- const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
5193
- return strName17(dataSource?.object) !== void 0;
5482
+ const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
5483
+ return strName18(dataSource?.object) !== void 0;
5484
+ }
5485
+ function unrecognizedKeysFromUnionArm(issue) {
5486
+ if (issue.code !== "invalid_union") return void 0;
5487
+ const arms = issue.errors;
5488
+ if (!arms || arms.length === 0) return void 0;
5489
+ let found;
5490
+ for (const arm of arms) {
5491
+ const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
5492
+ if (keyIssues.length === 0) continue;
5493
+ if (keyIssues.length !== arm.length || found) return void 0;
5494
+ found = keyIssues[0];
5495
+ }
5496
+ return found;
5194
5497
  }
5195
5498
  function validateComponentProps(stack) {
5196
5499
  const findings = [];
5197
- if (!isRec17(stack)) return findings;
5198
- const pages = asArray24(stack.pages);
5500
+ if (!isRec19(stack)) return findings;
5501
+ const pages = asArray25(stack.pages);
5199
5502
  for (let pi = 0; pi < pages.length; pi++) {
5200
5503
  const page = pages[pi];
5201
- if (!isRec17(page)) continue;
5202
- const pageName = strName17(page.name) ?? `#${pi}`;
5504
+ if (!isRec19(page)) continue;
5505
+ const pageName = strName18(page.name) ?? `#${pi}`;
5203
5506
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5204
- const type = strName17(component.type);
5507
+ const type = strName18(component.type);
5205
5508
  if (!type) continue;
5206
5509
  const schema = PROPS_SCHEMAS[type];
5207
5510
  if (!schema) continue;
5208
- const props = isRec17(component.properties) ? component.properties : void 0;
5511
+ const props = isRec19(component.properties) ? component.properties : void 0;
5209
5512
  if (!props) continue;
5210
5513
  const where = `page "${pageName}" \xB7 ${type}`;
5211
5514
  const base = `${path}.properties`;
@@ -5224,6 +5527,20 @@ function validateComponentProps(stack) {
5224
5527
  for (const issue of parsed.error?.issues ?? []) {
5225
5528
  if (suppliedByDataSource(issue, component)) continue;
5226
5529
  const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
5530
+ const armIssue = unrecognizedKeysFromUnionArm(issue);
5531
+ if (armIssue) {
5532
+ for (const key of armIssue.keys ?? []) {
5533
+ findings.push({
5534
+ severity: "warning",
5535
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5536
+ where,
5537
+ path: `${at}.${key}`,
5538
+ message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
5539
+ hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
5540
+ });
5541
+ }
5542
+ continue;
5543
+ }
5227
5544
  if (issue.code === "unrecognized_keys") {
5228
5545
  for (const key of issue.keys ?? []) {
5229
5546
  findings.push({
@@ -5454,7 +5771,7 @@ function looksLikeTailwind(className) {
5454
5771
  return false;
5455
5772
  });
5456
5773
  }
5457
- function asArray25(v) {
5774
+ function asArray26(v) {
5458
5775
  if (Array.isArray(v)) return v;
5459
5776
  if (v && typeof v === "object") {
5460
5777
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5547,13 +5864,13 @@ function checkNode(node, pageName, path, findings) {
5547
5864
  }
5548
5865
  function validateResponsiveStyles(stack) {
5549
5866
  const findings = [];
5550
- const pages = asArray25(stack.pages);
5867
+ const pages = asArray26(stack.pages);
5551
5868
  for (let p = 0; p < pages.length; p++) {
5552
5869
  const page = pages[p];
5553
5870
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
5554
- const regions = asArray25(page.regions);
5871
+ const regions = asArray26(page.regions);
5555
5872
  for (let r = 0; r < regions.length; r++) {
5556
- const components = asArray25(regions[r].components);
5873
+ const components = asArray26(regions[r].components);
5557
5874
  for (let c = 0; c < components.length; c++) {
5558
5875
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
5559
5876
  }
@@ -5564,10 +5881,10 @@ function validateResponsiveStyles(stack) {
5564
5881
 
5565
5882
  // src/validate-jsx-pages.ts
5566
5883
  var import_sdui_parser = require("@objectstack/sdui-parser");
5567
- var asArray26 = (v) => Array.isArray(v) ? v : [];
5884
+ var asArray27 = (v) => Array.isArray(v) ? v : [];
5568
5885
  function validateJsxPages(stack, opts = {}) {
5569
5886
  const findings = [];
5570
- const pages = asArray26(stack.pages);
5887
+ const pages = asArray27(stack.pages);
5571
5888
  for (let p = 0; p < pages.length; p++) {
5572
5889
  const page = pages[p];
5573
5890
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -5615,10 +5932,10 @@ function loadSucraseTransform() {
5615
5932
  }
5616
5933
  return cachedTransform;
5617
5934
  }
5618
- var asArray27 = (v) => Array.isArray(v) ? v : [];
5935
+ var asArray28 = (v) => Array.isArray(v) ? v : [];
5619
5936
  function validateReactPages(stack) {
5620
5937
  const findings = [];
5621
- const pages = asArray27(stack.pages);
5938
+ const pages = asArray28(stack.pages);
5622
5939
  for (let p = 0; p < pages.length; p++) {
5623
5940
  const page = pages[p];
5624
5941
  if (!page || page.kind !== "react") continue;
@@ -5655,11 +5972,11 @@ function validateReactPages(stack) {
5655
5972
 
5656
5973
  // src/validate-page-source-styling.ts
5657
5974
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
5658
- var asArray28 = (v) => Array.isArray(v) ? v : [];
5975
+ var asArray29 = (v) => Array.isArray(v) ? v : [];
5659
5976
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
5660
5977
  function validatePageSourceStyling(stack) {
5661
5978
  const findings = [];
5662
- const pages = asArray28(stack.pages);
5979
+ const pages = asArray29(stack.pages);
5663
5980
  for (let p = 0; p < pages.length; p++) {
5664
5981
  const page = pages[p];
5665
5982
  if (!page) continue;
@@ -5687,7 +6004,7 @@ function validatePageSourceStyling(stack) {
5687
6004
  // src/validate-capability-references.ts
5688
6005
  var import_security = require("@objectstack/spec/security");
5689
6006
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
5690
- function asArray29(v) {
6007
+ function asArray30(v) {
5691
6008
  if (Array.isArray(v)) return v;
5692
6009
  if (v && typeof v === "object") {
5693
6010
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5712,13 +6029,13 @@ function validateCapabilityReferences(stack) {
5712
6029
  const findings = [];
5713
6030
  if (!stack || typeof stack !== "object") return findings;
5714
6031
  const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
5715
- for (const cap of asArray29(stack.capabilities)) {
6032
+ for (const cap of asArray30(stack.capabilities)) {
5716
6033
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
5717
6034
  }
5718
- for (const ps of asArray29(stack.permissions)) {
6035
+ for (const ps of asArray30(stack.permissions)) {
5719
6036
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
5720
6037
  }
5721
- for (const seed of asArray29(stack.data)) {
6038
+ for (const seed of asArray30(stack.data)) {
5722
6039
  if (seed.object !== "sys_capability") continue;
5723
6040
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
5724
6041
  const name = rec?.name;
@@ -5737,7 +6054,7 @@ function validateCapabilityReferences(stack) {
5737
6054
  hint
5738
6055
  });
5739
6056
  };
5740
- const objects = asArray29(stack.objects);
6057
+ const objects = asArray30(stack.objects);
5741
6058
  for (let i = 0; i < objects.length; i++) {
5742
6059
  const obj = objects[i];
5743
6060
  if (!obj || typeof obj !== "object") continue;
@@ -5746,27 +6063,27 @@ function validateCapabilityReferences(stack) {
5746
6063
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
5747
6064
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
5748
6065
  }
5749
- const fields = asArray29(obj.fields);
6066
+ const fields = asArray30(obj.fields);
5750
6067
  for (const f of fields) {
5751
6068
  const fname = typeof f.name === "string" ? f.name : "(field)";
5752
6069
  for (const cap of asCapArray(f.requiredPermissions)) {
5753
6070
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
5754
6071
  }
5755
6072
  }
5756
- for (const [ai, action] of asArray29(obj.actions).entries()) {
6073
+ for (const [ai, action] of asArray30(obj.actions).entries()) {
5757
6074
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
5758
6075
  for (const cap of asCapArray(action.requiredPermissions)) {
5759
6076
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
5760
6077
  }
5761
6078
  }
5762
6079
  }
5763
- for (const [i, action] of asArray29(stack.actions).entries()) {
6080
+ for (const [i, action] of asArray30(stack.actions).entries()) {
5764
6081
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
5765
6082
  for (const cap of asCapArray(action.requiredPermissions)) {
5766
6083
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
5767
6084
  }
5768
6085
  }
5769
- const apps = asArray29(stack.apps);
6086
+ const apps = asArray30(stack.apps);
5770
6087
  for (let i = 0; i < apps.length; i++) {
5771
6088
  const app = apps[i];
5772
6089
  if (!app || typeof app !== "object") continue;
@@ -5801,7 +6118,7 @@ var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-inval
5801
6118
  var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
5802
6119
  var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
5803
6120
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
5804
- function asArray30(v) {
6121
+ function asArray31(v) {
5805
6122
  if (Array.isArray(v)) return v;
5806
6123
  if (v && typeof v === "object") {
5807
6124
  return Object.entries(v).map(([name, def]) => ({
@@ -5829,10 +6146,10 @@ function startNodeOf(flow) {
5829
6146
  }
5830
6147
  function validateFlowTriggerReadiness(stack) {
5831
6148
  const findings = [];
5832
- const flows = asArray30(stack.flows);
6149
+ const flows = asArray31(stack.flows);
5833
6150
  if (flows.length === 0) return findings;
5834
6151
  const objectNames = new Set(
5835
- asArray30(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
6152
+ asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
5836
6153
  );
5837
6154
  flows.forEach((flow, flowIndex) => {
5838
6155
  const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
@@ -5997,7 +6314,7 @@ var TYPE_FIX = {
5997
6314
  business_unit: "department",
5998
6315
  bu: "department"
5999
6316
  };
6000
- function asArray31(v) {
6317
+ function asArray32(v) {
6001
6318
  if (Array.isArray(v)) return v;
6002
6319
  if (v && typeof v === "object") {
6003
6320
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6007,7 +6324,7 @@ function asArray31(v) {
6007
6324
  function validateApprovalApprovers(stack) {
6008
6325
  const findings = [];
6009
6326
  if (!stack || typeof stack !== "object") return findings;
6010
- const flows = asArray31(stack.flows);
6327
+ const flows = asArray32(stack.flows);
6011
6328
  const validTypes = new Set(import_automation4.ApproverType.options);
6012
6329
  for (let fi = 0; fi < flows.length; fi++) {
6013
6330
  const flow = flows[fi];
@@ -6187,10 +6504,10 @@ function validateApprovalApprovers(stack) {
6187
6504
  }
6188
6505
 
6189
6506
  // src/validate-record-title.ts
6190
- var import_data7 = require("@objectstack/spec/data");
6507
+ var import_data9 = require("@objectstack/spec/data");
6191
6508
  var TITLE_FORMAT_RETIRED = "title-format-retired";
6192
6509
  var TITLE_UNRESOLVABLE = "title-unresolvable";
6193
- function asArray32(v) {
6510
+ function asArray33(v) {
6194
6511
  if (Array.isArray(v)) return v;
6195
6512
  if (v && typeof v === "object") {
6196
6513
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6199,7 +6516,7 @@ function asArray32(v) {
6199
6516
  }
6200
6517
  function validateRecordTitle(stack) {
6201
6518
  const findings = [];
6202
- const objects = asArray32(stack.objects);
6519
+ const objects = asArray33(stack.objects);
6203
6520
  for (let i = 0; i < objects.length; i++) {
6204
6521
  const obj = objects[i];
6205
6522
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -6215,7 +6532,7 @@ function validateRecordTitle(stack) {
6215
6532
  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.`
6216
6533
  });
6217
6534
  }
6218
- const completeness = (0, import_data7.objectTitleCompleteness)(obj);
6535
+ const completeness = (0, import_data9.objectTitleCompleteness)(obj);
6219
6536
  if (completeness.status === "none") {
6220
6537
  findings.push({
6221
6538
  severity: "warning",
@@ -6235,7 +6552,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
6235
6552
  var FIELD_GROUP_EMPTY = "field-group-empty";
6236
6553
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
6237
6554
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
6238
- function asArray33(v) {
6555
+ var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
6556
+ function asArray34(v) {
6239
6557
  if (Array.isArray(v)) return v;
6240
6558
  if (v && typeof v === "object") {
6241
6559
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6244,7 +6562,7 @@ function asArray33(v) {
6244
6562
  }
6245
6563
  function validateSemanticRoles(stack) {
6246
6564
  const findings = [];
6247
- const objects = asArray33(stack.objects);
6565
+ const objects = asArray34(stack.objects);
6248
6566
  for (let i = 0; i < objects.length; i++) {
6249
6567
  const obj = objects[i];
6250
6568
  if (!obj || typeof obj !== "object") continue;
@@ -6253,6 +6571,15 @@ function validateSemanticRoles(stack) {
6253
6571
  const path = `objects[${i}]`;
6254
6572
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
6255
6573
  const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
6574
+ const unprovisioned = unprovisionedInjectedColumnsFor(obj);
6575
+ const unprovisionedPointer = (slot, entry) => ({
6576
+ severity: "warning",
6577
+ rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
6578
+ where,
6579
+ path: `${path}.${slot}`,
6580
+ message: `${objName}: ${slot} points at "${entry}", an injected system column with no storage behind it \u2014 this object is external (ADR-0015), so the platform registers the anchor but the remote schema owns the table and no column backs it. Every consumer renders it empty on every record.`,
6581
+ hint: `If the remote table really carries "${entry}", declare it in the object's own fields (mapped through the external binding's columnMap); otherwise point ${slot} at a real remote column.`
6582
+ });
6256
6583
  const declaredGroups = new Set(
6257
6584
  (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
6258
6585
  );
@@ -6294,10 +6621,16 @@ function validateSemanticRoles(stack) {
6294
6621
  message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
6295
6622
  hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
6296
6623
  });
6624
+ } else if (typeof stage === "string" && unprovisioned.has(stage)) {
6625
+ findings.push(unprovisionedPointer("stageField", stage));
6297
6626
  }
6298
6627
  const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
6299
6628
  for (const entry of highlights) {
6300
- if (typeof entry !== "string" || entry.length === 0 || fieldNames.has(entry)) continue;
6629
+ if (typeof entry !== "string" || entry.length === 0) continue;
6630
+ if (fieldNames.has(entry)) {
6631
+ if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
6632
+ continue;
6633
+ }
6301
6634
  findings.push({
6302
6635
  severity: "warning",
6303
6636
  rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -6339,17 +6672,17 @@ function validateSemanticRoles(stack) {
6339
6672
  // src/validate-form-layout.ts
6340
6673
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
6341
6674
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
6342
- function asArray34(v) {
6675
+ function asArray35(v) {
6343
6676
  if (Array.isArray(v)) return v;
6344
6677
  if (v && typeof v === "object") {
6345
6678
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6346
6679
  }
6347
6680
  return [];
6348
6681
  }
6349
- function isRec18(v) {
6682
+ function isRec20(v) {
6350
6683
  return !!v && typeof v === "object" && !Array.isArray(v);
6351
6684
  }
6352
- function strName18(v) {
6685
+ function strName19(v) {
6353
6686
  return typeof v === "string" && v.length > 0 ? v : void 0;
6354
6687
  }
6355
6688
  function fieldNameOf(entry) {
@@ -6363,14 +6696,14 @@ function fieldNameOf(entry) {
6363
6696
  function validateFormLayout(stack) {
6364
6697
  const findings = [];
6365
6698
  const objectFields = /* @__PURE__ */ new Map();
6366
- for (const obj of asArray34(stack.objects)) {
6699
+ for (const obj of asArray35(stack.objects)) {
6367
6700
  const name = typeof obj.name === "string" ? obj.name : void 0;
6368
6701
  if (!name) continue;
6369
6702
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
6370
6703
  objectFields.set(name, new Set(fields));
6371
6704
  }
6372
6705
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6373
- const viewName = strName18(view.name) ?? strName18(view.object) ?? viewPath;
6706
+ const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
6374
6707
  const containerObject = viewObjectName(view);
6375
6708
  for (const site of formViewSites(view, viewPath)) {
6376
6709
  const objName = viewObjectName(site.view) ?? containerObject;
@@ -6380,7 +6713,7 @@ function validateFormLayout(stack) {
6380
6713
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6381
6714
  for (let s = 0; s < sections.length; s++) {
6382
6715
  const sec = sections[s];
6383
- const secFields = isRec18(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6716
+ const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6384
6717
  for (let f = 0; f < secFields.length; f++) {
6385
6718
  const entry = secFields[f];
6386
6719
  const fname = fieldNameOf(entry);
@@ -6395,7 +6728,7 @@ function validateFormLayout(stack) {
6395
6728
  hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6396
6729
  });
6397
6730
  }
6398
- const colSpan = isRec18(entry) ? entry.colSpan : void 0;
6731
+ const colSpan = isRec20(entry) ? entry.colSpan : void 0;
6399
6732
  if (colSpan != null) {
6400
6733
  findings.push({
6401
6734
  severity: "warning",
@@ -6506,6 +6839,64 @@ function validateSeedStateMachine(stack) {
6506
6839
 
6507
6840
  // src/validate-visibility-predicates.ts
6508
6841
  var import_formula4 = require("@objectstack/formula");
6842
+
6843
+ // src/predicate-rhs-position.ts
6844
+ function isNode2(v) {
6845
+ return !!v && typeof v === "object" && typeof v.op === "string";
6846
+ }
6847
+ var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
6848
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
6849
+ function bareId(node) {
6850
+ if (!isNode2(node)) return null;
6851
+ return node.op === "id" && typeof node.args === "string" ? node.args : null;
6852
+ }
6853
+ function bareRhsOnlyIdentifiers(ast) {
6854
+ const rhs = /* @__PURE__ */ new Set();
6855
+ const elsewhere = /* @__PURE__ */ new Set();
6856
+ const walk = (node, suppressible) => {
6857
+ if (Array.isArray(node)) {
6858
+ for (const child of node) walk(child, suppressible);
6859
+ return;
6860
+ }
6861
+ if (!isNode2(node)) return;
6862
+ const args = node.args;
6863
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
6864
+ walk(args[1], suppressible);
6865
+ walk(args[2], false);
6866
+ return;
6867
+ }
6868
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
6869
+ const right = suppressible ? bareId(args[1]) : null;
6870
+ walk(args[0], suppressible);
6871
+ if (right !== null) {
6872
+ rhs.add(right);
6873
+ return;
6874
+ }
6875
+ walk(args[1], suppressible);
6876
+ return;
6877
+ }
6878
+ const name = bareId(node);
6879
+ if (name !== null) {
6880
+ elsewhere.add(name);
6881
+ return;
6882
+ }
6883
+ walk(args, suppressible);
6884
+ };
6885
+ walk(ast, true);
6886
+ for (const name of elsewhere) rhs.delete(name);
6887
+ return rhs;
6888
+ }
6889
+ function isRec21(v) {
6890
+ return !!v && typeof v === "object" && !Array.isArray(v);
6891
+ }
6892
+ function schemaIdOf(view) {
6893
+ const data = view.data;
6894
+ if (!isRec21(data)) return void 0;
6895
+ if (data.provider !== "schema") return void 0;
6896
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
6897
+ }
6898
+
6899
+ // src/validate-visibility-predicates.ts
6509
6900
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
6510
6901
  var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
6511
6902
  var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
@@ -6553,7 +6944,7 @@ function boundName(overrun) {
6553
6944
  return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
6554
6945
  }
6555
6946
  var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
6556
- function isNode2(v) {
6947
+ function isNode3(v) {
6557
6948
  return !!v && typeof v === "object" && typeof v.op === "string";
6558
6949
  }
6559
6950
  function namespaceRoots(node, out) {
@@ -6561,22 +6952,27 @@ function namespaceRoots(node, out) {
6561
6952
  for (const child of node) namespaceRoots(child, out);
6562
6953
  return;
6563
6954
  }
6564
- if (!isNode2(node)) return;
6955
+ if (!isNode3(node)) return;
6565
6956
  const args = node.args;
6566
6957
  if (Array.isArray(args)) {
6567
6958
  const receiver = node.op === "rcall" ? args[1] : args[0];
6568
- if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6959
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6569
6960
  out.add(receiver.args);
6570
6961
  }
6571
6962
  }
6572
6963
  namespaceRoots(args, out);
6573
6964
  }
6574
- function firstBareIdentifier(source) {
6965
+ function firstBareIdentifier(source, literalRhs) {
6575
6966
  const ast = (0, import_formula4.parseCelToAst)(source);
6576
6967
  if (!ast) return null;
6577
6968
  const rooted = /* @__PURE__ */ new Set();
6578
6969
  namespaceRoots(ast, rooted);
6579
- return (0, import_formula4.firstUndeclaredReference)(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
6970
+ const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
6971
+ return (0, import_formula4.firstUndeclaredReference)(source, [
6972
+ ...VIEW_PAGE_EXTRA_ROOTS,
6973
+ ...rooted,
6974
+ ...literalSlot
6975
+ ]);
6580
6976
  }
6581
6977
  var CANONICAL_ROOT_BY_LAYER = {
6582
6978
  runtime: "record",
@@ -6585,16 +6981,16 @@ var CANONICAL_ROOT_BY_LAYER = {
6585
6981
  var MISLAYER_BY_LAYER = {
6586
6982
  runtime: {
6587
6983
  forbiddenRoot: "data",
6588
- message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing-form root (a `*.form.ts` row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
6984
+ message: "visibility predicate is rooted at `data.` \u2014 that is the root a metadata-editing form binds (the row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
6589
6985
  hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
6590
6986
  },
6591
6987
  metadata: {
6592
6988
  forbiddenRoot: "record",
6593
- message: "visibility predicate is rooted at `record.` \u2014 that is the runtime record-surface root (a `*.view.ts` / `*.page.ts` live record), not a metadata-editing form. A `*.form.ts` predicate that binds `record.` never matches and the element renders unconditionally (ADR-0089).",
6989
+ message: "visibility predicate is rooted at `record.` \u2014 that is the root a runtime view/page surface binds (the live record), not the root a metadata-editing form binds. On a metadata-editing form \u2014 the row under edit \u2014 a `record.`-rooted predicate never matches and the element renders unconditionally (ADR-0089).",
6594
6990
  hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
6595
6991
  }
6596
6992
  };
6597
- function checkElement(el, where, path, layer, findings) {
6993
+ function checkElement(el, where, path, layer, findings, literalRhs = false) {
6598
6994
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
6599
6995
  const source = predicateSource(raw);
6600
6996
  const rule = MISLAYER_BY_LAYER[layer];
@@ -6632,7 +7028,7 @@ function checkElement(el, where, path, layer, findings) {
6632
7028
  });
6633
7029
  }
6634
7030
  if (source && !refusal) {
6635
- const bare = firstBareIdentifier(source);
7031
+ const bare = firstBareIdentifier(source, literalRhs);
6636
7032
  if (bare) {
6637
7033
  const root = CANONICAL_ROOT_BY_LAYER[layer];
6638
7034
  findings.push({
@@ -6641,7 +7037,7 @@ function checkElement(el, where, path, layer, findings) {
6641
7037
  where,
6642
7038
  path,
6643
7039
  message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
6644
- hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a `*.form.ts` metadata-editing form binds the row under edit as `data`).")
7040
+ hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a metadata-editing form binds the row under edit as `data`).")
6645
7041
  });
6646
7042
  }
6647
7043
  }
@@ -6650,24 +7046,27 @@ function isFieldObject(entry) {
6650
7046
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
6651
7047
  }
6652
7048
  function validateVisibilityPredicates(stack, opts = {}) {
6653
- const layer = opts.layer ?? "runtime";
7049
+ const declaredLayer = opts.layer ?? "runtime";
6654
7050
  const findings = [];
6655
7051
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6656
7052
  const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
6657
7053
  for (const site of formViewSites(view, viewPath)) {
6658
7054
  const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
7055
+ const schemaBound = schemaIdOf(site.view) !== void 0;
7056
+ const literalRhs = schemaBound;
7057
+ const layer = schemaBound ? "metadata" : declaredLayer;
6659
7058
  for (const bucket of ["sections", "groups"]) {
6660
7059
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6661
7060
  for (let s = 0; s < sections.length; s++) {
6662
7061
  const sec = sections[s];
6663
7062
  if (!sec || typeof sec !== "object") continue;
6664
7063
  const secPath = `${site.path}.${bucket}[${s}]`;
6665
- checkElement(sec, where, secPath, layer, findings);
7064
+ checkElement(sec, where, secPath, layer, findings, literalRhs);
6666
7065
  const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6667
7066
  for (let f = 0; f < secFields.length; f++) {
6668
7067
  const entry = secFields[f];
6669
7068
  if (isFieldObject(entry)) {
6670
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
7069
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
6671
7070
  }
6672
7071
  }
6673
7072
  }
@@ -6678,7 +7077,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
6678
7077
  const pageName = typeof page.name === "string" ? page.name : void 0;
6679
7078
  const where = `page "${pageName ?? pagePath}"`;
6680
7079
  for (const walked of walkPageComponents(page, pagePath)) {
6681
- checkElement(walked.component, where, walked.path, layer, findings);
7080
+ checkElement(walked.component, where, walked.path, declaredLayer, findings);
6682
7081
  }
6683
7082
  }
6684
7083
  return findings;
@@ -6690,9 +7089,9 @@ var import_kernel2 = require("@objectstack/spec/kernel");
6690
7089
  var import_spec4 = require("@objectstack/spec");
6691
7090
  var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
6692
7091
  var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
7092
+ var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
6693
7093
  var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
6694
7094
  var ROOT = "data";
6695
- var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
6696
7095
  function defOf(schema) {
6697
7096
  if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
6698
7097
  const s = schema;
@@ -6785,11 +7184,11 @@ function stepInto(scope, segment) {
6785
7184
  if (!declared.includes(segment)) return { kind: "undeclared", declared };
6786
7185
  return { kind: "declared", next: propertyOf(u, segment) };
6787
7186
  }
6788
- function isNode3(v) {
7187
+ function isNode4(v) {
6789
7188
  return !!v && typeof v === "object" && typeof v.op === "string";
6790
7189
  }
6791
7190
  function memberChain(node) {
6792
- if (!isNode3(node)) return null;
7191
+ if (!isNode4(node)) return null;
6793
7192
  if (node.op === "id" && typeof node.args === "string") return [node.args];
6794
7193
  if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
6795
7194
  const head = memberChain(node.args[0]);
@@ -6802,7 +7201,7 @@ function rootedPaths(node, out) {
6802
7201
  for (const child of node) rootedPaths(child, out);
6803
7202
  return;
6804
7203
  }
6805
- if (!isNode3(node)) return;
7204
+ if (!isNode4(node)) return;
6806
7205
  if (node.op === ".") {
6807
7206
  const chain = memberChain(node);
6808
7207
  if (chain && chain[0] === ROOT && chain.length > 1) {
@@ -6812,23 +7211,40 @@ function rootedPaths(node, out) {
6812
7211
  }
6813
7212
  rootedPaths(node.args, out);
6814
7213
  }
7214
+ var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
7215
+ function equalitySites(node, out) {
7216
+ if (Array.isArray(node)) {
7217
+ for (const child of node) equalitySites(child, out);
7218
+ return;
7219
+ }
7220
+ if (!isNode4(node)) return;
7221
+ const args = node.args;
7222
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
7223
+ equalitySites(args[1], out);
7224
+ return;
7225
+ }
7226
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
7227
+ out.push({ op: node.op, right: args[1] });
7228
+ }
7229
+ equalitySites(args, out);
7230
+ }
6815
7231
  function classifyIdentifiers(node, values, excluded) {
6816
7232
  if (Array.isArray(node)) {
6817
7233
  for (const child of node) classifyIdentifiers(child, values, excluded);
6818
7234
  return;
6819
7235
  }
6820
- if (!isNode3(node)) return;
7236
+ if (!isNode4(node)) return;
6821
7237
  const args = node.args;
6822
7238
  if (Array.isArray(args)) {
6823
7239
  const receiver = node.op === "rcall" ? args[1] : args[0];
6824
- if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
7240
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6825
7241
  excluded.add(receiver.args);
6826
7242
  }
6827
7243
  if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
6828
7244
  const macroArgs = args[2];
6829
7245
  if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
6830
7246
  const bound = macroArgs[0];
6831
- if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
7247
+ if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
6832
7248
  excluded.add(bound.args);
6833
7249
  }
6834
7250
  }
@@ -6847,15 +7263,9 @@ function predicateSource2(v) {
6847
7263
  }
6848
7264
  return void 0;
6849
7265
  }
6850
- function isRec19(v) {
7266
+ function isRec22(v) {
6851
7267
  return !!v && typeof v === "object" && !Array.isArray(v);
6852
7268
  }
6853
- function schemaIdOf(view) {
6854
- const data = view.data;
6855
- if (!isRec19(data)) return void 0;
6856
- if (data.provider !== "schema") return void 0;
6857
- return typeof data.schemaId === "string" ? data.schemaId : void 0;
6858
- }
6859
7269
  function checkPredicate(source, scope, where, path, findings) {
6860
7270
  const ast = (0, import_formula5.parseCelToAst)(source);
6861
7271
  if (!ast) return;
@@ -6885,19 +7295,38 @@ function checkPredicate(source, scope, where, path, findings) {
6885
7295
  }
6886
7296
  }
6887
7297
  const declaredHere = keysOf(scope);
6888
- if (!declaredHere) return;
6889
- const values = /* @__PURE__ */ new Set();
6890
- const excluded = /* @__PURE__ */ new Set();
6891
- classifyIdentifiers(ast, values, excluded);
6892
- for (const id of values) {
6893
- if (excluded.has(id) || !declaredHere.includes(id)) continue;
7298
+ const rhsOnly = bareRhsOnlyIdentifiers(ast);
7299
+ if (declaredHere) {
7300
+ const values = /* @__PURE__ */ new Set();
7301
+ const excluded = /* @__PURE__ */ new Set();
7302
+ classifyIdentifiers(ast, values, excluded);
7303
+ for (const id of values) {
7304
+ if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
7305
+ findings.push({
7306
+ severity: "error",
7307
+ rule: PREDICATE_PATH_UNROOTED,
7308
+ where,
7309
+ path,
7310
+ message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
7311
+ hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
7312
+ });
7313
+ }
7314
+ }
7315
+ const sites = [];
7316
+ equalitySites(ast, sites);
7317
+ for (const { op, right } of sites) {
7318
+ const chain = memberChain(right);
7319
+ if (!chain) continue;
7320
+ const text = chain.join(".");
7321
+ if (!PATH_SHAPED_RHS.test(text)) continue;
7322
+ const dotted = chain.length > 1;
6894
7323
  findings.push({
6895
- severity: "error",
6896
- rule: PREDICATE_PATH_UNROOTED,
7324
+ severity: dotted ? "error" : "warning",
7325
+ rule: PREDICATE_RHS_PATH_SHAPED,
6897
7326
  where,
6898
7327
  path,
6899
- message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
6900
- hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
7328
+ message: dotted ? `predicate compares against \`${text}\` on the RIGHT of \`${op}\`, which is a path but is not evaluated as one. A metadata-editing form resolves paths on the LEFT of \`${op}\` only; the right-hand side goes to the literal parser, so \`${text}\` is compared as the literal string "${text}". The verdict therefore does not depend on the right-hand path at all: \`a == ${text}\` is FALSE even when both sides hold the same value, and \`a != ${text}\` is correspondingly TRUE. An \`==\` written this way hides the element on every row, and nothing in the console says why (objectui#4049).` : `predicate compares against the unquoted word \`${text}\` on the RIGHT of \`${op}\`. The right-hand side of \`${op}\` is a literal, never a reference, so this is read as the literal string "${text}" \u2014 which is probably what you meant, and is why it appears to work. It is outside the declared subset all the same (\`path == 'literal'\`), and it stops working when this surface moves to the real CEL evaluator, where a bare \`${text}\` resolves to nothing (objectui#4049). The token also reads as a \`${ROOT}.\` root someone dropped, so this one finding carries BOTH readings: which one you meant is the thing no linter can know, and it changes the fix (#7696).`,
7329
+ hint: dotted ? `Two sanctioned spellings. (1) If you meant the TEXT, quote it: \`${op} '${text}'\`. (2) If you meant the PATH, restructure so the path is on the LEFT and a literal is on the right \u2014 comparing one path against another is outside the subset this surface renders, which is \`path == 'literal'\` / \`path != 'literal'\` and nothing wider. There is no third spelling that compares two paths here.` : `Two sanctioned spellings, and you must pick \u2014 they are not the same predicate. (1) If you meant the TEXT \`${text}\`, quote it: \`${op} '${text}'\`. That is what this renders as today, so it changes no behaviour and is the fix unless you know otherwise. (2) If you meant the FIELD \`${ROOT}.${text}\`, move it to the LEFT and put a literal on the right, e.g. \`${ROOT}.${text} == 'yes'\`. \u26D4 Do NOT simply add the root in place: \`${op} ${ROOT}.${text}\` is a path on the RIGHT, which this surface parses as the literal string "${ROOT}.${text}" \u2014 it is refused by this same rule at \`error\`, and it is FALSE on every row. The subset here is \`path == 'literal'\` and nothing wider.`
6901
7330
  });
6902
7331
  }
6903
7332
  }
@@ -6905,7 +7334,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
6905
7334
  if (!Array.isArray(entries) || depth > 12) return;
6906
7335
  for (let i = 0; i < entries.length; i++) {
6907
7336
  const entry = entries[i];
6908
- if (!isRec19(entry)) continue;
7337
+ if (!isRec22(entry)) continue;
6909
7338
  const path = `${base}[${i}]`;
6910
7339
  for (const key of PREDICATE_KEYS) {
6911
7340
  const source = predicateSource2(entry[key]);
@@ -6932,15 +7361,14 @@ function validatePredicatePathRefs(stack, opts = {}) {
6932
7361
  try {
6933
7362
  root = resolveSchema(schemaId);
6934
7363
  } catch {
6935
- continue;
7364
+ root = void 0;
6936
7365
  }
6937
- if (!root) continue;
6938
7366
  const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
6939
7367
  for (const bucket of ["sections", "groups"]) {
6940
7368
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6941
7369
  for (let s = 0; s < sections.length; s++) {
6942
7370
  const section = sections[s];
6943
- if (!isRec19(section)) continue;
7371
+ if (!isRec22(section)) continue;
6944
7372
  const sectionPath = `${site.path}.${bucket}[${s}]`;
6945
7373
  for (const key of PREDICATE_KEYS) {
6946
7374
  const source = predicateSource2(section[key]);
@@ -6971,6 +7399,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
6971
7399
  var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
6972
7400
  var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
6973
7401
  var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
7402
+ var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
6974
7403
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
6975
7404
  var OWD_ALIAS_FIX = {
6976
7405
  read: "public_read",
@@ -6983,7 +7412,7 @@ var OWD_WIDTH = {
6983
7412
  public_read: 1,
6984
7413
  public_read_write: 2
6985
7414
  };
6986
- function asArray35(v) {
7415
+ function asArray36(v) {
6987
7416
  if (Array.isArray(v)) return v;
6988
7417
  if (v && typeof v === "object") {
6989
7418
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7009,21 +7438,28 @@ function refOf(def) {
7009
7438
  return typeof r === "string" && r ? r : void 0;
7010
7439
  }
7011
7440
  function firstMasterDetailField(obj) {
7012
- for (const f of asArray35(obj.fields)) {
7441
+ for (const f of asArray36(obj.fields)) {
7013
7442
  if (f.type === "master_detail") {
7014
7443
  return { name: String(f.name ?? "?"), parent: refOf(f) };
7015
7444
  }
7016
7445
  }
7017
7446
  return void 0;
7018
7447
  }
7448
+ function resolveCbpRelation(obj) {
7449
+ const entries = asArray36(obj.fields);
7450
+ const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
7451
+ const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
7452
+ if (!found) return void 0;
7453
+ return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
7454
+ }
7019
7455
  function grantsObjectAccess(p) {
7020
7456
  return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
7021
7457
  }
7022
7458
  function validateSecurityPosture(stack, opts) {
7023
7459
  const findings = [];
7024
7460
  if (!stack || typeof stack !== "object") return findings;
7025
- const objects = asArray35(stack.objects);
7026
- const permissionSets = asArray35(stack.permissions);
7461
+ const objects = asArray36(stack.objects);
7462
+ const permissionSets = asArray36(stack.permissions);
7027
7463
  for (let i = 0; i < objects.length; i++) {
7028
7464
  const obj = objects[i];
7029
7465
  if (!obj || typeof obj !== "object") continue;
@@ -7061,6 +7497,16 @@ function validateSecurityPosture(stack, opts) {
7061
7497
  });
7062
7498
  }
7063
7499
  }
7500
+ if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
7501
+ findings.push({
7502
+ severity: "error",
7503
+ rule: SECURITY_CBP_NO_RELATION,
7504
+ where: `object "${objName}"`,
7505
+ path: `${objPath}.sharingModel`,
7506
+ message: `"${objName}" declares sharingModel 'controlled_by_parent' but has no relation the platform can derive access from. ADR-0055 resolves the master through a required master_detail, then any master_detail, then a required lookup \u2014 each of which must also name a reference target \u2014 and this object matches none of the three. At runtime every read is DENIED and every write is refused with 422 INVALID_METADATA (#7474), so the object is unusable rather than merely locked down.`,
7507
+ hint: `Add the master relation this object is derived from, e.g. fields.parent: { type: 'master_detail', reference: '<master_object>', required: true }. If the object has no master, its baseline is its own decision \u2014 use sharingModel: 'private' (owner + shares), 'public_read', or 'public_read_write'.`
7508
+ });
7509
+ }
7064
7510
  if (typeof external === "string") {
7065
7511
  if (OWD_ALIAS_FIX[external]) {
7066
7512
  findings.push({
@@ -7126,57 +7572,10 @@ function validateSecurityPosture(stack, opts) {
7126
7572
  }
7127
7573
  }
7128
7574
  }
7129
- const flagRole = (kind, name, label2, where, path) => {
7130
- if (identifierHasRoleToken(name)) {
7131
- findings.push({
7132
- severity: "error",
7133
- rule: SECURITY_ROLE_WORD,
7134
- where,
7135
- path,
7136
- message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
7137
- hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
7138
- });
7139
- } else if (labelHasRoleWord(label2)) {
7140
- findings.push({
7141
- severity: "error",
7142
- rule: SECURITY_ROLE_WORD,
7143
- where,
7144
- path: `${path.replace(/\.name$/, "")}.label`,
7145
- message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
7146
- hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
7147
- });
7148
- }
7149
- };
7150
- for (let i = 0; i < objects.length; i++) {
7151
- const obj = objects[i];
7152
- if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
7153
- const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
7154
- flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
7155
- for (const f of asArray35(obj.fields)) {
7156
- flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
7157
- }
7158
- for (const [ai, action] of asArray35(obj.actions).entries()) {
7159
- flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
7160
- }
7161
- }
7162
- for (let i = 0; i < permissionSets.length; i++) {
7163
- const ps = permissionSets[i];
7164
- if (!ps || typeof ps !== "object") continue;
7165
- flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
7166
- }
7167
- for (const [i, pos] of asArray35(stack.positions).entries()) {
7168
- flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
7169
- }
7170
- for (const [i, app] of asArray35(stack.apps).entries()) {
7171
- flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
7172
- }
7173
- for (const [i, book] of asArray35(stack.books).entries()) {
7174
- flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
7175
- }
7176
7575
  const stackSetNames = new Set(
7177
7576
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
7178
7577
  );
7179
- for (const [i, book] of asArray35(stack.books).entries()) {
7578
+ for (const [i, book] of asArray36(stack.books).entries()) {
7180
7579
  const audience = book.audience;
7181
7580
  if (!audience || typeof audience !== "object") continue;
7182
7581
  const setName = audience.permissionSet;
@@ -7254,7 +7653,7 @@ function validateSecurityPosture(stack, opts) {
7254
7653
  }
7255
7654
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
7256
7655
  const nowMs = opts?.nowMs ?? Date.now();
7257
- for (const [i, seed] of asArray35(stack.data).entries()) {
7656
+ for (const [i, seed] of asArray36(stack.data).entries()) {
7258
7657
  const seedObject = typeof seed.object === "string" ? seed.object : "";
7259
7658
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
7260
7659
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -7293,13 +7692,67 @@ function validateSecurityPosture(stack, opts) {
7293
7692
  }
7294
7693
  return findings;
7295
7694
  }
7695
+ function validateSecurityRoleWord(stack) {
7696
+ const findings = [];
7697
+ if (!stack || typeof stack !== "object") return findings;
7698
+ const objects = asArray36(stack.objects);
7699
+ const permissionSets = asArray36(stack.permissions);
7700
+ const flagRole = (kind, name, label2, where, path) => {
7701
+ if (identifierHasRoleToken(name)) {
7702
+ findings.push({
7703
+ severity: "error",
7704
+ rule: SECURITY_ROLE_WORD,
7705
+ where,
7706
+ path,
7707
+ message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
7708
+ hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
7709
+ });
7710
+ } else if (labelHasRoleWord(label2)) {
7711
+ findings.push({
7712
+ severity: "error",
7713
+ rule: SECURITY_ROLE_WORD,
7714
+ where,
7715
+ path: `${path.replace(/\.name$/, "")}.label`,
7716
+ message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
7717
+ hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
7718
+ });
7719
+ }
7720
+ };
7721
+ for (let i = 0; i < objects.length; i++) {
7722
+ const obj = objects[i];
7723
+ if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
7724
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
7725
+ flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
7726
+ for (const f of asArray36(obj.fields)) {
7727
+ flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
7728
+ }
7729
+ for (const [ai, action] of asArray36(obj.actions).entries()) {
7730
+ flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
7731
+ }
7732
+ }
7733
+ for (let i = 0; i < permissionSets.length; i++) {
7734
+ const ps = permissionSets[i];
7735
+ if (!ps || typeof ps !== "object") continue;
7736
+ flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
7737
+ }
7738
+ for (const [i, pos] of asArray36(stack.positions).entries()) {
7739
+ flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
7740
+ }
7741
+ for (const [i, app] of asArray36(stack.apps).entries()) {
7742
+ flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
7743
+ }
7744
+ for (const [i, book] of asArray36(stack.books).entries()) {
7745
+ flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
7746
+ }
7747
+ return findings;
7748
+ }
7296
7749
 
7297
7750
  // src/validate-org-axis-red-lines.ts
7298
7751
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
7299
7752
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
7300
7753
  var ORG_PARENT_FIELD = "parent_organization_id";
7301
7754
  var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
7302
- function asArray36(v) {
7755
+ function asArray37(v) {
7303
7756
  if (Array.isArray(v)) return v;
7304
7757
  if (v && typeof v === "object") {
7305
7758
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7329,9 +7782,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
7329
7782
  function validateOrgAxisRedLines(stack) {
7330
7783
  const findings = [];
7331
7784
  const cfg = stack ?? {};
7332
- const permissionSets = asArray36(cfg.permissions);
7785
+ const permissionSets = asArray37(cfg.permissions);
7333
7786
  permissionSets.forEach((ps, psIndex) => {
7334
- asArray36(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7787
+ asArray37(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7335
7788
  for (const clause of ["using", "check"]) {
7336
7789
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
7337
7790
  findings.push({
@@ -7345,7 +7798,7 @@ function validateOrgAxisRedLines(stack) {
7345
7798
  }
7346
7799
  });
7347
7800
  });
7348
- asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
7801
+ asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
7349
7802
  const slots = [
7350
7803
  { key: "condition", text: expressionText(rule.condition) },
7351
7804
  { key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
@@ -7363,9 +7816,9 @@ function validateOrgAxisRedLines(stack) {
7363
7816
  }
7364
7817
  });
7365
7818
  const tenancyDisabledObjects = new Set(
7366
- asArray36(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
7819
+ asArray37(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
7367
7820
  );
7368
- asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
7821
+ asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
7369
7822
  const target = str(rule.object);
7370
7823
  if (!target || !tenancyDisabledObjects.has(target)) return;
7371
7824
  const sharedWith = rule.sharedWith;
@@ -7388,7 +7841,7 @@ function validateOrgAxisRedLines(stack) {
7388
7841
  var import_formula6 = require("@objectstack/formula");
7389
7842
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
7390
7843
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
7391
- function asArray37(v) {
7844
+ function asArray38(v) {
7392
7845
  if (Array.isArray(v)) return v;
7393
7846
  if (v && typeof v === "object") {
7394
7847
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7415,7 +7868,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
7415
7868
  function validateSharingRuleEnforceability(stack) {
7416
7869
  const findings = [];
7417
7870
  const cfg = stack ?? {};
7418
- asArray37(cfg.sharingRules).forEach((rule, index) => {
7871
+ asArray38(cfg.sharingRules).forEach((rule, index) => {
7419
7872
  const input = toCompilerInput(rule.condition);
7420
7873
  if (input === null) return;
7421
7874
  const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
@@ -7455,7 +7908,7 @@ var import_formula7 = require("@objectstack/formula");
7455
7908
  var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
7456
7909
  var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
7457
7910
  var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
7458
- function asArray38(v) {
7911
+ function asArray39(v) {
7459
7912
  if (Array.isArray(v)) return v;
7460
7913
  if (v && typeof v === "object") {
7461
7914
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7480,8 +7933,8 @@ function consequence(clause) {
7480
7933
  function validateRlsPredicateEnforceability(stack) {
7481
7934
  const findings = [];
7482
7935
  const cfg = stack ?? {};
7483
- asArray38(cfg.permissions).forEach((ps, psIndex) => {
7484
- asArray38(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7936
+ asArray39(cfg.permissions).forEach((ps, psIndex) => {
7937
+ asArray39(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7485
7938
  for (const clause of ["using", "check"]) {
7486
7939
  const source = str3(policy[clause]);
7487
7940
  if (!source.trim()) continue;
@@ -7541,11 +7994,11 @@ var import_meta4 = {};
7541
7994
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
7542
7995
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
7543
7996
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
7544
- var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7545
- function asArray39(v) {
7546
- if (Array.isArray(v)) return v.filter(isRec20);
7547
- if (isRec20(v)) {
7548
- return Object.entries(v).filter(([, def]) => isRec20(def)).map(([name, def]) => ({ name, ...def }));
7997
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7998
+ 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 }));
7549
8002
  }
7550
8003
  return [];
7551
8004
  }
@@ -7562,7 +8015,7 @@ function loadAjv() {
7562
8015
  `@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.`
7563
8016
  );
7564
8017
  }
7565
- const ctor = isRec20(mod) && "default" in mod ? mod.default : mod;
8018
+ const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
7566
8019
  cachedAjv = ctor;
7567
8020
  return ctor;
7568
8021
  }
@@ -7577,7 +8030,7 @@ function loadAddFormats() {
7577
8030
  `@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.`
7578
8031
  );
7579
8032
  }
7580
- const plugin = isRec20(mod) && "default" in mod ? mod.default : mod;
8033
+ const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
7581
8034
  cachedAddFormats = plugin;
7582
8035
  return plugin;
7583
8036
  }
@@ -7607,17 +8060,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
7607
8060
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
7608
8061
  for (const branch of ["then", "otherwise"]) {
7609
8062
  const nested = rule[branch];
7610
- if (isRec20(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8063
+ if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7611
8064
  }
7612
8065
  return out;
7613
8066
  }
7614
8067
  function walkObjectValidationRules(stack) {
7615
8068
  const walked = [];
7616
- if (!isRec20(stack)) return walked;
7617
- for (const obj of asArray39(stack.objects)) {
8069
+ if (!isRec23(stack)) return walked;
8070
+ for (const obj of asArray40(stack.objects)) {
7618
8071
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7619
8072
  const validations = obj.validations;
7620
- for (const authored of asArray39(validations)) {
8073
+ for (const authored of asArray40(validations)) {
7621
8074
  for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
7622
8075
  walked.push({
7623
8076
  rule,
@@ -7648,7 +8101,7 @@ function validateRuleCompilability(stack) {
7648
8101
  });
7649
8102
  }
7650
8103
  }
7651
- if (rule.type === "json_schema" && isRec20(rule.schema)) {
8104
+ if (rule.type === "json_schema" && isRec23(rule.schema)) {
7652
8105
  try {
7653
8106
  createRuntimeAjv().compile(rule.schema);
7654
8107
  } catch (err) {
@@ -7668,7 +8121,7 @@ function validateRuleCompilability(stack) {
7668
8121
 
7669
8122
  // src/validate-rule-schema-formats.ts
7670
8123
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7671
- var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8124
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7672
8125
  var SUBSCHEMA_KEYS = [
7673
8126
  "additionalItems",
7674
8127
  "additionalProperties",
@@ -7692,7 +8145,7 @@ var SUBSCHEMA_MAP_KEYS = [
7692
8145
  var MAX_SCHEMA_WALK_DEPTH = 32;
7693
8146
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7694
8147
  function collectFormatUses(schema, pointer, out, depth) {
7695
- if (!isRec21(schema)) return;
8148
+ if (!isRec24(schema)) return;
7696
8149
  if (typeof schema.format === "string") {
7697
8150
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7698
8151
  }
@@ -7711,7 +8164,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7711
8164
  }
7712
8165
  for (const key of SUBSCHEMA_MAP_KEYS) {
7713
8166
  const value = schema[key];
7714
- if (!isRec21(value)) continue;
8167
+ if (!isRec24(value)) continue;
7715
8168
  for (const [name, entry] of Object.entries(value)) {
7716
8169
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7717
8170
  }
@@ -7719,13 +8172,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7719
8172
  const items = schema.items;
7720
8173
  if (Array.isArray(items)) {
7721
8174
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7722
- } else if (isRec21(items)) {
8175
+ } else if (isRec24(items)) {
7723
8176
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7724
8177
  }
7725
8178
  const dependencies = schema.dependencies;
7726
- if (isRec21(dependencies)) {
8179
+ if (isRec24(dependencies)) {
7727
8180
  for (const [name, entry] of Object.entries(dependencies)) {
7728
- if (!isRec21(entry)) continue;
8181
+ if (!isRec24(entry)) continue;
7729
8182
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7730
8183
  }
7731
8184
  }
@@ -7764,7 +8217,7 @@ function validateRuleSchemaFormats(stack) {
7764
8217
  const findings = [];
7765
8218
  const pending = [];
7766
8219
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7767
- if (rule.type !== "json_schema" || !isRec21(rule.schema)) continue;
8220
+ if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
7768
8221
  const uses = [];
7769
8222
  collectFormatUses(rule.schema, "", uses, 0);
7770
8223
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7790,14 +8243,14 @@ function validateRuleSchemaFormats(stack) {
7790
8243
 
7791
8244
  // src/validate-action-locations.ts
7792
8245
  var ACTION_NO_PLACEMENT = "action-no-placement";
7793
- function asArray40(v) {
8246
+ function asArray41(v) {
7794
8247
  if (Array.isArray(v)) return v;
7795
8248
  if (v && typeof v === "object") {
7796
8249
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7797
8250
  }
7798
8251
  return [];
7799
8252
  }
7800
- function strName19(v) {
8253
+ function strName20(v) {
7801
8254
  return typeof v === "string" && v.length > 0 ? v : void 0;
7802
8255
  }
7803
8256
  function strList3(v) {
@@ -7811,8 +8264,8 @@ function collectNamePlacedActions(stack) {
7811
8264
  for (const key of ["rowActions", "bulkActions"]) {
7812
8265
  for (const n of strList3(list3[key])) placed.add(n);
7813
8266
  }
7814
- for (const def of asArray40(list3.bulkActionDefs)) {
7815
- const n = strName19(def?.name);
8267
+ for (const def of asArray41(list3.bulkActionDefs)) {
8268
+ const n = strName20(def?.name);
7816
8269
  if (n) placed.add(n);
7817
8270
  }
7818
8271
  };
@@ -7820,12 +8273,12 @@ function collectNamePlacedActions(stack) {
7820
8273
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
7821
8274
  for (const lv of Object.values(listViews)) harvest(lv);
7822
8275
  };
7823
- for (const view of asArray40(stack.views)) {
8276
+ for (const view of asArray41(stack.views)) {
7824
8277
  if (!view || typeof view !== "object") continue;
7825
8278
  harvest(view.list);
7826
8279
  harvestListViews(view.listViews);
7827
8280
  }
7828
- for (const obj of asArray40(stack.objects)) {
8281
+ for (const obj of asArray41(stack.objects)) {
7829
8282
  if (!obj || typeof obj !== "object") continue;
7830
8283
  harvestListViews(obj.listViews);
7831
8284
  }
@@ -7838,7 +8291,7 @@ function validateActionLocations(stack) {
7838
8291
  const check = (action, path) => {
7839
8292
  if (!action || typeof action !== "object") return;
7840
8293
  if ("locations" in action) return;
7841
- const name = strName19(action.name);
8294
+ const name = strName20(action.name);
7842
8295
  if (!name) return;
7843
8296
  if (namePlaced.has(name)) return;
7844
8297
  findings.push({
@@ -7850,13 +8303,13 @@ function validateActionLocations(stack) {
7850
8303
  hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
7851
8304
  });
7852
8305
  };
7853
- const actions = asArray40(stack.actions);
8306
+ const actions = asArray41(stack.actions);
7854
8307
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
7855
- const objects = asArray40(stack.objects);
8308
+ const objects = asArray41(stack.objects);
7856
8309
  for (let oi = 0; oi < objects.length; oi++) {
7857
8310
  const obj = objects[oi];
7858
8311
  if (!obj || typeof obj !== "object") continue;
7859
- const own = asArray40(obj.actions);
8312
+ const own = asArray41(obj.actions);
7860
8313
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
7861
8314
  }
7862
8315
  return findings;
@@ -7864,8 +8317,8 @@ function validateActionLocations(stack) {
7864
8317
 
7865
8318
  // src/lint-flow-patterns.ts
7866
8319
  var import_automation5 = require("@objectstack/spec/automation");
7867
- var import_data8 = require("@objectstack/spec/data");
7868
- function asArray41(v) {
8320
+ var import_data10 = require("@objectstack/spec/data");
8321
+ function asArray42(v) {
7869
8322
  if (Array.isArray(v)) return v;
7870
8323
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7871
8324
  return [];
@@ -8139,7 +8592,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
8139
8592
  function filterCarriesNoCondition(filter) {
8140
8593
  if (filter === void 0 || filter === null) return true;
8141
8594
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
8142
- return (0, import_data8.reduceFilterVerdict)(filter) === "true";
8595
+ return (0, import_data10.reduceFilterVerdict)(filter) === "true";
8143
8596
  }
8144
8597
  function describeUnboundedFilter(filter) {
8145
8598
  if (filter === void 0 || filter === null) return "no `filter` key";
@@ -8249,7 +8702,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
8249
8702
  }
8250
8703
  function lintFlowPatterns(stack) {
8251
8704
  const findings = [];
8252
- for (const flow of asArray41(stack.flows)) {
8705
+ for (const flow of asArray42(stack.flows)) {
8253
8706
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
8254
8707
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
8255
8708
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -8347,7 +8800,7 @@ var import_node_fs = require("fs");
8347
8800
  var import_meta5 = {};
8348
8801
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
8349
8802
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
8350
- function asArray42(v) {
8803
+ function asArray43(v) {
8351
8804
  if (Array.isArray(v)) return v;
8352
8805
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
8353
8806
  return [];
@@ -8484,11 +8937,11 @@ function lintLivenessProperties(stack) {
8484
8937
  const findings = [];
8485
8938
  const objectWarn = loadWarnMap(dir, "object");
8486
8939
  const fieldWarn = loadWarnMap(dir, "field");
8487
- for (const obj of asArray42(stack.objects)) {
8940
+ for (const obj of asArray43(stack.objects)) {
8488
8941
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8489
8942
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
8490
8943
  if (fieldWarn.size > 0) {
8491
- for (const field of asArray42(obj.fields)) {
8944
+ for (const field of asArray43(obj.fields)) {
8492
8945
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
8493
8946
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
8494
8947
  }
@@ -8497,7 +8950,7 @@ function lintLivenessProperties(stack) {
8497
8950
  for (const { type, key } of TYPE_COLLECTIONS) {
8498
8951
  const warnMap = loadWarnMap(dir, type);
8499
8952
  if (warnMap.size === 0) continue;
8500
- for (const item of asArray42(stack[key])) {
8953
+ for (const item of asArray43(stack[key])) {
8501
8954
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
8502
8955
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
8503
8956
  }
@@ -8506,12 +8959,12 @@ function lintLivenessProperties(stack) {
8506
8959
  }
8507
8960
 
8508
8961
  // src/lint-autonumber-formats.ts
8509
- var import_data9 = require("@objectstack/spec/data");
8962
+ var import_data11 = require("@objectstack/spec/data");
8510
8963
  var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
8511
8964
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
8512
8965
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
8513
8966
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
8514
- function asArray43(v) {
8967
+ function asArray44(v) {
8515
8968
  if (Array.isArray(v)) return v;
8516
8969
  if (v && typeof v === "object") {
8517
8970
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -8520,9 +8973,9 @@ function asArray43(v) {
8520
8973
  }
8521
8974
  function lintAutonumberFormats(stack) {
8522
8975
  const findings = [];
8523
- for (const obj of asArray43(stack.objects)) {
8976
+ for (const obj of asArray44(stack.objects)) {
8524
8977
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8525
- const fields = asArray43(obj.fields);
8978
+ const fields = asArray44(obj.fields);
8526
8979
  const fieldMeta = /* @__PURE__ */ new Map();
8527
8980
  for (const f of fields) {
8528
8981
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -8532,8 +8985,8 @@ function lintAutonumberFormats(stack) {
8532
8985
  const name = typeof f.name === "string" ? f.name : "(unnamed field)";
8533
8986
  const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
8534
8987
  if (!fmt) continue;
8535
- const tokens = (0, import_data9.parseAutonumberFormat)(fmt);
8536
- const refs = (0, import_data9.referencedFields)(tokens);
8988
+ const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
8989
+ const refs = (0, import_data11.referencedFields)(tokens);
8537
8990
  const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
8538
8991
  for (const t of tokens) {
8539
8992
  if (t.kind !== "literal") continue;
@@ -8588,7 +9041,7 @@ function lintAutonumberFormats(stack) {
8588
9041
 
8589
9042
  // src/lint-view-refs.ts
8590
9043
  var import_spec5 = require("@objectstack/spec");
8591
- function asArray44(v) {
9044
+ function asArray45(v) {
8592
9045
  if (Array.isArray(v)) return v;
8593
9046
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
8594
9047
  return [];
@@ -8616,7 +9069,7 @@ function lintViewRefs(stack) {
8616
9069
  s.add(kind);
8617
9070
  };
8618
9071
  const containers = [];
8619
- for (const v of asArray44(stack.views)) {
9072
+ for (const v of asArray45(stack.views)) {
8620
9073
  if (v.viewKind) {
8621
9074
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
8622
9075
  continue;
@@ -8625,7 +9078,7 @@ function lintViewRefs(stack) {
8625
9078
  const object = viewContainerObjectName(v);
8626
9079
  if (object) containers.push({ object, container: v });
8627
9080
  }
8628
- for (const obj of asArray44(stack.objects)) {
9081
+ for (const obj of asArray45(stack.objects)) {
8629
9082
  const object = typeof obj.name === "string" ? obj.name : void 0;
8630
9083
  if (!object) continue;
8631
9084
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -8679,11 +9132,11 @@ function lintViewRefs(stack) {
8679
9132
  });
8680
9133
  }
8681
9134
  };
8682
- for (const obj of asArray44(stack.objects)) {
9135
+ for (const obj of asArray45(stack.objects)) {
8683
9136
  const object = typeof obj.name === "string" ? obj.name : void 0;
8684
- for (const action of asArray44(obj.actions)) checkAction(action, object);
9137
+ for (const action of asArray45(obj.actions)) checkAction(action, object);
8685
9138
  }
8686
- for (const action of asArray44(stack.actions)) checkAction(action);
9139
+ for (const action of asArray45(stack.actions)) checkAction(action);
8687
9140
  return findings;
8688
9141
  }
8689
9142
 
@@ -8818,7 +9271,6 @@ var CLI_ONLY = ["cli"];
8818
9271
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
8819
9272
  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.";
8820
9273
  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.";
8821
- var RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY = "Deliberate, and not a snapshot limitation: this rule needs only the written item, but every other rule on the `views[]` visibility-predicate surface (validate-visibility-predicates.ts) is CLI-only. Gating one of three sibling verdicts about the same predicate at the Studio door is less predictable than gating none; move the family together, as one measured edit.";
8822
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.";
8823
9275
  var EXPRESSION_INVALID = "expression-invalid";
8824
9276
  var AUTHORING_RULES = [
@@ -8884,6 +9336,31 @@ var AUTHORING_RULES = [
8884
9336
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8885
9337
  run: (stack) => validateFunctionalCompleteness(stack)
8886
9338
  },
9339
+ // [#7521, via cloud#1225] A managed object advertising a generic write verb
9340
+ // in `enable.apiMethods` that its own resolved affordances refuse. Every key
9341
+ // is one we know and each is individually valid, so #4001's unknown-key
9342
+ // rejection and the Zod parse both pass it; the contradiction is only visible
9343
+ // when the two keys are read TOGETHER, which nothing did at authoring time.
9344
+ //
9345
+ // `gating` because the declaration is already false when it ships: objectql's
9346
+ // registry strips the verb at registration, so the metadata advertises an API
9347
+ // the product does not serve. That strip has been correct and silent — a
9348
+ // `console.warn` on every control-plane boot that went unread for the life of
9349
+ // a real divergence (`sys_environment`/`sys_package`). This entry is the
9350
+ // ruling's "close it where the author is"; boot stays warn-and-strip.
9351
+ //
9352
+ // Pre-parse: the predicate reads only authored keys, and the finding must
9353
+ // survive an unrelated schema error elsewhere in the stack.
9354
+ {
9355
+ name: "validateManagedApiMethods",
9356
+ tier: "gating",
9357
+ input: "normalized",
9358
+ commands: ALL,
9359
+ source: "packages/lint/src/validate-managed-api-methods.ts",
9360
+ surfaces: CLI_ONLY,
9361
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9362
+ run: (stack) => validateManagedApiMethods(stack)
9363
+ },
8887
9364
  // A view container in `views: []` that registers zero views: nothing appears
8888
9365
  // in the Console, and the schema step cannot tell it from an intentionally
8889
9366
  // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
@@ -9263,14 +9740,47 @@ var AUTHORING_RULES = [
9263
9740
  // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9264
9741
  // has worked since #3760. The promotion follows the #5762 precedent: a family
9265
9742
  // that gains an `error` finding moves its registry tier in the same edit.
9743
+ //
9744
+ // ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
9745
+ //
9746
+ // This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
9747
+ // in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
9748
+ // `advisories` channel landed (PR #7435). Before that move a `view` written
9749
+ // through Studio / REST `/meta` / MCP — the only door most tenants have, and
9750
+ // the door AI authors use — was judged by NONE of the family's rule ids (six
9751
+ // at the time of the move; seven since #7659 added
9752
+ // `predicate-rhs-path-shaped` inside the second entry).
9753
+ //
9754
+ // They move together on purpose, and the two entries carry one comment because
9755
+ // they are one wall: #7214's implementer wired its own rule here alone and then
9756
+ // REVERTED it, because a `view` refused for an unresolvable predicate PATH
9757
+ // while a predicate that does not parse at all walks through the same door is
9758
+ // less predictable than refusing neither. A half-wired wall is worse than an
9759
+ // unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
9760
+ // directly: every id on this surface is gated at the runtime door, or none is.
9761
+ //
9762
+ // The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
9763
+ // and re-measuring it at move time found it false: both rule functions read
9764
+ // `stack.views` and `stack.pages` and NO other collection — never `objects` —
9765
+ // so the per-write snapshot the gate builds is not partial for them, it is
9766
+ // complete. (`pages` is simply absent on a `view` write, so the page half
9767
+ // contributes zero findings to both differential passes rather than inventing
9768
+ // any.) The reason was not describing this rule; it was the default a rule got
9769
+ // when nobody measured, which is the #4409/#4463 defect one layer in.
9770
+ //
9771
+ // Runtime input tier: the gate hands the rules the body as persisted, without
9772
+ // `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
9773
+ // door. That costs the family nothing — `validateVisibilityPredicates` reads
9774
+ // `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
9775
+ // so a caller handing it a raw authored object still gets a verdict.
9266
9776
  {
9267
9777
  name: "validateVisibilityPredicates",
9268
9778
  tier: "gating",
9269
9779
  input: "normalized",
9270
9780
  commands: ALL,
9271
9781
  source: "packages/lint/src/validate-visibility-predicates.ts",
9272
- surfaces: CLI_ONLY,
9273
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9782
+ surfaces: CLI_AND_RUNTIME,
9783
+ runtimeTypes: ["view"],
9274
9784
  run: (stack) => validateVisibilityPredicates(stack)
9275
9785
  },
9276
9786
  // #7010 — the same predicate surface, one question further in. The three
@@ -9287,14 +9797,28 @@ var AUTHORING_RULES = [
9287
9797
  // object's addressable path set is NOT closed (lookup traversal, system
9288
9798
  // columns, formula outputs), and an `error` gate over an open set generates
9289
9799
  // false build errors. See the rule's module note.
9800
+ //
9801
+ // #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
9802
+ // resolution question at all: the metadata-admin renderer resolves paths only
9803
+ // on the LEFT of `==` / `!=` and hands the right side to its literal parser,
9804
+ // so `data.a == data.b` resolves both sides cleanly, passes the two rules
9805
+ // above, and still compares against the string "data.b" — a constant verdict.
9806
+ // It carries `error` on a dotted chain (no reading under which it worked) and
9807
+ // `warning` on a bare word (`status == active` compares as the text today, so
9808
+ // refusing it would fail a build over metadata that renders correctly). The
9809
+ // per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
9810
+ // since #3760; the entry's `gating` tier is unchanged because it already was.
9290
9811
  {
9291
9812
  name: "validatePredicatePathRefs",
9292
9813
  tier: "gating",
9293
9814
  input: "normalized",
9294
9815
  commands: ALL,
9295
9816
  source: "packages/lint/src/validate-predicate-path-refs.ts",
9296
- surfaces: CLI_ONLY,
9297
- surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
9817
+ // The second half of the #7220 family move — see the block above the
9818
+ // `validateVisibilityPredicates` entry. This is the rule whose solo wiring
9819
+ // was reverted; it is wired now because its siblings are.
9820
+ surfaces: CLI_AND_RUNTIME,
9821
+ runtimeTypes: ["view"],
9298
9822
  run: (stack) => validatePredicatePathRefs(stack)
9299
9823
  },
9300
9824
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
@@ -9454,16 +9978,110 @@ var AUTHORING_RULES = [
9454
9978
  // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
9455
9979
  // binding gate, vocabulary freeze), moving the failure from a runtime deny to
9456
9980
  // an author-time fix-it. Per ADR-0049 this is not advisory security.
9981
+ //
9982
+ // [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
9983
+ // was false in both halves — it read: "Already gated at this surface by a
9984
+ // DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
9985
+ // `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
9986
+ // on every runtime write. Running the linter here as well would double-report
9987
+ // one refusal in two vocabularies."
9988
+ //
9989
+ // - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
9990
+ // `externalSharingModel` through a local `OWD_WIDTH`, and never touches
9991
+ // `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
9992
+ // block carries it covers ONE — `security-external-wider-than-internal`
9993
+ // (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
9994
+ // corresponds to no lint rule at all, so it is not coverage in the other
9995
+ // direction either. Twelve rules were enforced at no runtime door while
9996
+ // this field said they were.
9997
+ // - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
9998
+ // `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
9999
+ // `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
10000
+ // both refuse by THROWING. The first to fire ends the write, so an author
10001
+ // sees one refusal, never two. The stated cost of moving was imaginary; the
10002
+ // reason it has not moved is the measured one below.
10003
+ //
10004
+ // The move IS taken now — the #7891 programme's three slices, in order:
10005
+ //
10006
+ // - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
10007
+ // the isolation proof that the differential cancels every finding this
10008
+ // function derives from the sibling collections.
10009
+ // - #8309: the snapshot repair. The gate used to carry `objects` and
10010
+ // nothing else, so the three cross-collection rules judged a universe
10011
+ // missing the collection they compare against (measured: 38 phantom
10012
+ // `security-master-detail-ungranted` per-write vs 4 whole-stack,
10013
+ // PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
10014
+ // BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
10015
+ // - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
10016
+ // `object` measured DIRTY on that tree and was escalated, not forced.
10017
+ // - #8310 slice 2 (this state): `object` crosses under the maintainer
10018
+ // ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
10019
+ // authored OWD is REQUIRED at the runtime object door — an object
10020
+ // publish with no authored `sharingModel` is refused with the 422 lint
10021
+ // envelope (`security-owd-unset`); absence is not a decision. The ~16
10022
+ // objectql/rest suite files that relied on OWD-less publishes were
10023
+ // repaired honestly (fixtures author their posture), and
10024
+ // `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
10025
+ // answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
10026
+ // ADR-0094-seam 403 doors answer for what passes lint. The same ruling
10027
+ // retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
10028
+ // of this door (R1 env-tighten-only STAYS — no lint rule covers it);
10029
+ // see `object-posture-gate.ts` and the ADR-0094 amendment.
10030
+ //
10031
+ // `security-role-word` is NOT in this entry any more — that is what the
10032
+ // `validateSecurityRoleWord` entry below records. It judges six collections
10033
+ // (objects, fields, actions, permission sets, positions, apps — plus books),
10034
+ // and `positions`/`apps` are neither carried by the per-write snapshot nor
10035
+ // mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
10036
+ // function that still contained it would have enforced ONE rule id for a
10037
+ // strict subset of its collections: a door where a permission set named
10038
+ // `role_manager` is refused and a position named `sales_role` walks through
10039
+ // — the #7220 failure this table refuses to build, in either direction. The
10040
+ // rule therefore stays behind WHOLE (#8310's explicit call), as its own
10041
+ // entry.
10042
+ //
10043
+ // This entry remains the rest of the D7 block (12 rule ids) as ONE
10044
+ // registration, not a per-rule split: the baseline/candidate differential is
10045
+ // what keeps a write of one declared type from leaking the other rules'
10046
+ // whole-stack findings — every finding derived from a sibling collection is
10047
+ // produced byte-identically in both passes and cancels in the diff. Only
10048
+ // findings the written item itself adds are attributed to the write.
9457
10049
  {
9458
10050
  name: "validateSecurityPosture",
9459
10051
  tier: "gating",
9460
10052
  input: "parsed",
9461
10053
  commands: ALL,
9462
10054
  source: "packages/lint/src/validate-security-posture.ts",
9463
- surfaces: CLI_ONLY,
9464
- surfaceReason: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on `object` (`registerAuthoringGate`) that enforces the same OWD posture rules on every runtime write. Running the linter here as well would double-report one refusal in two vocabularies. Consolidating the two onto this table is P2 (#4463), and is a merge, not a hole.",
10055
+ surfaces: CLI_AND_RUNTIME,
10056
+ runtimeTypes: ["seed", "permission", "book", "object"],
9465
10057
  run: (stack) => validateSecurityPosture(stack)
9466
10058
  },
10059
+ // [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
10060
+ // `validateSecurityPosture` the day the rest of that block crossed the
10061
+ // runtime wall — so that it could stay behind WHOLE rather than cross for
10062
+ // three of the six collections it judges (#7220: one rule id must sit on ONE
10063
+ // side of the wall). The split is a surface boundary, not taste: the rule's
10064
+ // verdict and findings are byte-identical to before on every CLI command
10065
+ // (both entries run on all three), and the runtime door does not run it for
10066
+ // ANY type.
10067
+ //
10068
+ // The road to crossing is concrete and short, recorded here so the next
10069
+ // seat prices it correctly: carry `positions`/`apps` in
10070
+ // `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
10071
+ // `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
10072
+ // are real), then declare `runtimeTypes: ['object', 'permission', 'book',
10073
+ // 'position', 'app']` on THIS entry — all six collections in one edit, the
10074
+ // #7220 discipline satisfied.
10075
+ {
10076
+ name: "validateSecurityRoleWord",
10077
+ tier: "gating",
10078
+ input: "parsed",
10079
+ commands: ALL,
10080
+ source: "packages/lint/src/validate-security-posture.ts",
10081
+ surfaces: CLI_ONLY,
10082
+ surfaceReason: "P2 (#4463)/#8310: judges six collections (objects, fields, actions, permission sets, positions, apps \u2014 plus books), and the per-write snapshot neither carries nor maps positions/apps. Wiring it for the mapped types alone would enforce one rule id for three of its six collections \u2014 the #7220 split (an object named sales_role refused while a position named sales_role walks through). It crosses whole \u2014 positions/apps carried, mapped and declared \u2014 or stays behind; it stays behind until that wiring exists.",
10083
+ run: (stack) => validateSecurityRoleWord(stack)
10084
+ },
9467
10085
  // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
9468
10086
  // sharing rule that walks it builds a second permission hierarchy (the
9469
10087
  // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
@@ -9514,7 +10132,7 @@ var AUTHORING_RULES = [
9514
10132
  commands: ALL,
9515
10133
  source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
9516
10134
  surfaces: CLI_ONLY,
9517
- surfaceReason: "P2 (#4463): the rule reads `stack.permissions[]`, a stack-wide collection the per-write snapshot does not carry, and P1 gates `flow` alone. It is otherwise snapshot-ready \u2014 it needs no other collection \u2014 so widening it is a `runtimeTypes: ['permission_set']` edit once the gate builds that snapshot, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10135
+ surfaceReason: "The rule reads `stack.permissions[]`, which the per-write snapshot DOES carry since #8309 \u2014 the remaining gap is only the declaration: no `runtimeTypes` names `permission` here, and that flip is a rollout decision on #8310's axis, not a wiring fix. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
9518
10136
  run: (stack) => validateRlsPredicateEnforceability(stack)
9519
10137
  },
9520
10138
  // #4762 — the same "declared but enforces nothing" question, for the two
@@ -9572,8 +10190,34 @@ var TYPE_TO_STACK_KEY = {
9572
10190
  dashboard: "dashboards",
9573
10191
  agent: "agents",
9574
10192
  hook: "hooks",
9575
- seed: "seeds"
10193
+ // [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
10194
+ // holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
10195
+ // — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
10196
+ // mapping onto one either.
10197
+ //
10198
+ // The wrong spelling was INERT rather than harmless, and it is the #4449 shape
10199
+ // one surface over: the wiring guard asks only that a declared type HAS a
10200
+ // mapping, never that the mapping names a key some rule reads. So it would
10201
+ // have stayed green while the gate built `{ objects, seeds: [item] }` for
10202
+ // every seed write and every rule reading `stack.data` saw nothing — wired,
10203
+ // and running on nothing, with `rulesRun` reporting the rules as having run.
10204
+ // Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
10205
+ // changed no behaviour then; it was corrected here, with the measurement
10206
+ // that found it (#7576), rather than left for the rollout card to trip
10207
+ // over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
10208
+ // mapping is load-bearing today, not merely inert-and-correct.
10209
+ seed: "data",
10210
+ // [#8309] `permission`/`book` map ahead of their registration (#8310), the
10211
+ // same order `seed` arrived in: the mapping plus the enriched snapshot below
10212
+ // are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
10213
+ // a mapping without a declaring rule is inert by construction (the gate
10214
+ // filters by `runtimeTypes` before it ever consults this table), while a
10215
+ // declaration without the mapping is the wired-onto-nothing state the wiring
10216
+ // guard refuses. Landing the mapping first keeps #8310 a registry data edit.
10217
+ permission: "permissions",
10218
+ book: "books"
9576
10219
  };
10220
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
9577
10221
  function runtimeAuthoringRulesFor(type) {
9578
10222
  return AUTHORING_RULES.filter(
9579
10223
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -9591,6 +10235,23 @@ function stackKeyForType(type) {
9591
10235
  return TYPE_TO_STACK_KEY[type] ?? null;
9592
10236
  }
9593
10237
  var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
10238
+ function buildRuntimeWriteSnapshots(args) {
10239
+ const stackKey = stackKeyForType(args.type);
10240
+ if (!stackKey) return null;
10241
+ if (!args.item || typeof args.item !== "object") return null;
10242
+ const item = args.item;
10243
+ const itemName = typeof item.name === "string" ? item.name : void 0;
10244
+ const baseline = {};
10245
+ for (const key of CONTEXT_STACK_KEYS) {
10246
+ const collection = args.context?.[key] ?? [];
10247
+ baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10248
+ }
10249
+ const candidate = {
10250
+ ...baseline,
10251
+ [stackKey]: [...baseline[stackKey] ?? [], item]
10252
+ };
10253
+ return { baseline, candidate };
10254
+ }
9594
10255
  function runRules(rules, stack, ctx) {
9595
10256
  const findings = [];
9596
10257
  for (const rule of rules) {
@@ -9613,19 +10274,15 @@ function runRuntimeAuthoringRules(args) {
9613
10274
  const rules = runtimeAuthoringRulesFor(args.type);
9614
10275
  const empty = { errors: [], advisories: [], rulesRun: [] };
9615
10276
  if (rules.length === 0) return empty;
9616
- const stackKey = stackKeyForType(args.type);
9617
- if (!stackKey) return empty;
9618
- if (!args.item || typeof args.item !== "object") return empty;
9619
- const item = args.item;
9620
- const itemName = typeof item.name === "string" ? item.name : void 0;
9621
- const contextObjects = args.context?.objects ?? [];
10277
+ const snapshots = buildRuntimeWriteSnapshots({
10278
+ type: args.type,
10279
+ item: args.item,
10280
+ ...args.context !== void 0 ? { context: args.context } : {}
10281
+ });
10282
+ if (!snapshots) return empty;
9622
10283
  const ctx = { sduiManifest: args.sduiManifest };
9623
- const writesIntoContext = stackKey === "objects";
9624
- const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
9625
- const baseline = { objects: baselineObjects };
9626
- const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
9627
- const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
9628
- const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
10284
+ 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)));
9629
10286
  return {
9630
10287
  errors: added.filter((f) => f.severity === "error"),
9631
10288
  advisories: added.filter((f) => f.severity !== "error"),
@@ -9634,6 +10291,7 @@ function runRuntimeAuthoringRules(args) {
9634
10291
  }
9635
10292
  // Annotate the CommonJS export names for ESM import in node:
9636
10293
  0 && (module.exports = {
10294
+ buildRuntimeWriteSnapshots,
9637
10295
  runRuntimeAuthoringRules,
9638
10296
  runtimeAuthoringRulesFor,
9639
10297
  runtimeGatedTypes,