@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/index.js CHANGED
@@ -3,7 +3,11 @@ import { isIncoherentAggregate } from "@objectstack/spec/data";
3
3
  import { ChartTypeSchema } from "@objectstack/spec/ui";
4
4
 
5
5
  // src/system-fields.ts
6
- import { FIELD_GROUP_SYSTEM_FIELDS, resolveInjectedSystemColumns } from "@objectstack/spec/data";
6
+ import {
7
+ FIELD_GROUP_SYSTEM_FIELDS,
8
+ resolveInjectedSystemColumns,
9
+ unprovisionedInjectedColumns
10
+ } from "@objectstack/spec/data";
7
11
  import { SystemFieldName } from "@objectstack/spec/system";
8
12
  var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
9
13
  ...FIELD_GROUP_SYSTEM_FIELDS,
@@ -12,6 +16,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
12
16
  function injectedColumnsFor(objectDef) {
13
17
  return resolveInjectedSystemColumns(objectDef).names;
14
18
  }
19
+ function unprovisionedInjectedColumnsFor(objectDef) {
20
+ return new Set(unprovisionedInjectedColumns(objectDef));
21
+ }
22
+ function objectDefsOf(stack) {
23
+ if (!stack || typeof stack !== "object") return [];
24
+ const objects = stack.objects;
25
+ if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
26
+ if (objects && typeof objects === "object") {
27
+ return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
28
+ }
29
+ return [];
30
+ }
31
+ function indexUnprovisionedAnchors(stack) {
32
+ const index = /* @__PURE__ */ new Map();
33
+ for (const obj of objectDefsOf(stack)) {
34
+ const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
35
+ if (!name) continue;
36
+ const anchors = unprovisionedInjectedColumnsFor(obj);
37
+ if (anchors.size > 0) index.set(name, anchors);
38
+ }
39
+ return index;
40
+ }
41
+ function unprovisionedAnchorCause(objectName, field) {
42
+ 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`;
43
+ }
44
+ function unprovisionedAnchorHint(objectName, field) {
45
+ 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).`;
46
+ }
15
47
 
16
48
  // src/validate-widget-bindings.ts
17
49
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
@@ -24,6 +56,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
24
56
  var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
25
57
  var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
26
58
  var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
59
+ var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
27
60
  var LEGACY_ANALYTICS_KEYS = [
28
61
  "categoryField",
29
62
  "valueField",
@@ -143,6 +176,7 @@ function validateWidgetBindings(stack) {
143
176
  }
144
177
  objectFieldTypes.set(o.name, fm);
145
178
  }
179
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
146
180
  const datasetList = asArray(stack.datasets);
147
181
  for (let i = 0; i < datasetList.length; i++) {
148
182
  const ds = datasetList[i];
@@ -229,13 +263,24 @@ function validateWidgetBindings(stack) {
229
263
  if (dashFilterDefs.length > 0) {
230
264
  const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
231
265
  const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
232
- if (objectFields) {
266
+ const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
267
+ if (objectFields && datasetObject) {
233
268
  for (const def of dashFilterDefs) {
234
269
  const eff = effectiveFilterField(w, def);
235
270
  if (!eff) continue;
236
271
  const field = eff.field;
237
272
  if (field.includes(".")) continue;
238
- if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
273
+ if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
274
+ if (anchors?.has(field)) {
275
+ push2({
276
+ severity: "warning",
277
+ rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
278
+ 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).`,
279
+ 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.`
280
+ });
281
+ }
282
+ continue;
283
+ }
239
284
  push2({
240
285
  severity: "error",
241
286
  rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
@@ -333,7 +378,7 @@ function validateWidgetBindings(stack) {
333
378
  }
334
379
 
335
380
  // src/validate-expressions.ts
336
- import { validateExpression, collectCelRootIdentifiers, SCOPE_ROOTS } from "@objectstack/formula";
381
+ import { validateExpression, collectCelRootIdentifiers, parseCelToAst as parseCelToAst2, SCOPE_ROOTS } from "@objectstack/formula";
337
382
  import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
338
383
 
339
384
  // src/validate-null-guards.ts
@@ -568,6 +613,37 @@ function buildFieldIndex(objects) {
568
613
  }
569
614
  return idx;
570
615
  }
616
+ var BOUND_RECORD_ROOTS = ["record", "previous"];
617
+ function isCelNode(v) {
618
+ return !!v && typeof v === "object" && typeof v.op === "string";
619
+ }
620
+ function collectBoundRecordReads(source) {
621
+ const out = /* @__PURE__ */ new Map();
622
+ const ast = parseCelToAst2(source);
623
+ if (!ast) return out;
624
+ const pending = [ast];
625
+ while (pending.length > 0) {
626
+ const celNode = pending.pop();
627
+ if (!isCelNode(celNode)) continue;
628
+ if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
629
+ const [celRecv, seg] = celNode.args;
630
+ if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
631
+ if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
632
+ }
633
+ }
634
+ const celArgs = celNode.args;
635
+ if (isCelNode(celArgs)) pending.push(celArgs);
636
+ else if (Array.isArray(celArgs)) {
637
+ for (const a of celArgs) {
638
+ if (isCelNode(a)) pending.push(a);
639
+ else if (Array.isArray(a)) {
640
+ for (const b of a) if (isCelNode(b)) pending.push(b);
641
+ }
642
+ }
643
+ }
644
+ }
645
+ return out;
646
+ }
571
647
  function buildFieldTypeIndex(objects) {
572
648
  const idx = /* @__PURE__ */ new Map();
573
649
  for (const obj of objects) {
@@ -667,6 +743,29 @@ function validateStackExpressions(stack) {
667
743
  const fieldIndex = buildFieldIndex(objects);
668
744
  const fieldTypeIndex = buildFieldTypeIndex(objects);
669
745
  const nullableIndex = buildNullableFieldIndex(objects);
746
+ const unprovisionedIndex = /* @__PURE__ */ new Map();
747
+ for (const obj of objects) {
748
+ const name = typeof obj.name === "string" ? obj.name : void 0;
749
+ if (!name) continue;
750
+ const anchors = unprovisionedInjectedColumnsFor(obj);
751
+ if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
752
+ }
753
+ const warnUnprovisionedAnchors = (where, raw, objectName) => {
754
+ if (!objectName) return;
755
+ const anchors = unprovisionedIndex.get(objectName);
756
+ if (!anchors) return;
757
+ const source = celSourceOf(raw);
758
+ if (!source) return;
759
+ for (const [field, operand] of collectBoundRecordReads(source)) {
760
+ if (!anchors.has(field)) continue;
761
+ issues.push({
762
+ where,
763
+ 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).`,
764
+ source,
765
+ severity: "warning"
766
+ });
767
+ }
768
+ };
670
769
  const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
671
770
  if (!objectName) return;
672
771
  const nullableFields = nullableIndex.get(objectName);
@@ -693,6 +792,7 @@ function validateStackExpressions(stack) {
693
792
  );
694
793
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
695
794
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
795
+ warnUnprovisionedAnchors(where, raw, objectName);
696
796
  };
697
797
  const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
698
798
  const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
@@ -839,6 +939,7 @@ function validateStackExpressions(stack) {
839
939
  const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
840
940
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
841
941
  for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
942
+ warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
842
943
  }
843
944
  }
844
945
  }
@@ -1389,6 +1490,46 @@ function validateFunctionalCompleteness(stack) {
1389
1490
  return out;
1390
1491
  }
1391
1492
 
1493
+ // src/validate-managed-api-methods.ts
1494
+ import {
1495
+ checkManagedApiMethodAffordances,
1496
+ describeManagedApiMethodConflicts
1497
+ } from "@objectstack/spec/data";
1498
+ var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
1499
+ var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1500
+ function entriesOf2(v) {
1501
+ if (Array.isArray(v)) {
1502
+ return v.flatMap(
1503
+ (def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
1504
+ );
1505
+ }
1506
+ if (isRec2(v)) {
1507
+ return Object.entries(v).flatMap(
1508
+ ([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
1509
+ );
1510
+ }
1511
+ return [];
1512
+ }
1513
+ function validateManagedApiMethods(stack) {
1514
+ const out = [];
1515
+ if (!isRec2(stack)) return out;
1516
+ for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
1517
+ const conflicts = checkManagedApiMethodAffordances(obj.def);
1518
+ if (conflicts.length === 0) continue;
1519
+ const verbs = conflicts.map((c) => c.verb).join(", ");
1520
+ const flags = [...new Set(conflicts.map((c) => c.needs))];
1521
+ out.push({
1522
+ severity: "error",
1523
+ rule: MANAGED_API_METHOD_UNAFFORDABLE,
1524
+ where: `object "${obj.name}"`,
1525
+ path: `objects[${oi}].enable.apiMethods`,
1526
+ message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + 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.`,
1527
+ 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.`
1528
+ });
1529
+ }
1530
+ return out;
1531
+ }
1532
+
1392
1533
  // src/validate-flow-trigger-readiness.ts
1393
1534
  import { TimeRelativeTriggerSchema } from "@objectstack/spec/automation";
1394
1535
  var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
@@ -1573,7 +1714,7 @@ function validateFlowTriggerReadiness(stack) {
1573
1714
 
1574
1715
  // src/flow-walk.ts
1575
1716
  import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
1576
- function isRec2(v) {
1717
+ function isRec3(v) {
1577
1718
  return !!v && typeof v === "object" && !Array.isArray(v);
1578
1719
  }
1579
1720
  function strName(v) {
@@ -1588,7 +1729,7 @@ function flowNodeLabel(node, index) {
1588
1729
  return strName(node.label) ?? strName(node.id) ?? `#${index}`;
1589
1730
  }
1590
1731
  function stripRegions(config) {
1591
- if (!isRec2(config)) return void 0;
1732
+ if (!isRec3(config)) return void 0;
1592
1733
  let out;
1593
1734
  for (const key of Object.keys(config)) {
1594
1735
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -1599,11 +1740,11 @@ function stripRegions(config) {
1599
1740
  }
1600
1741
  function walkFlowNodes(flow, flowPath) {
1601
1742
  const out = [];
1602
- if (!isRec2(flow)) return out;
1743
+ if (!isRec3(flow)) return out;
1603
1744
  const visitList = (nodes, basePath, trail, depth) => {
1604
1745
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
1605
1746
  nodes.forEach((raw, index) => {
1606
- if (!isRec2(raw)) return;
1747
+ if (!isRec3(raw)) return;
1607
1748
  const path = `${basePath}[${index}]`;
1608
1749
  out.push({
1609
1750
  node: raw,
@@ -1614,7 +1755,7 @@ function walkFlowNodes(flow, flowPath) {
1614
1755
  });
1615
1756
  const type = strName(raw.type);
1616
1757
  const slots = type ? REGION_SLOTS.get(type) : void 0;
1617
- if (!slots || !isRec2(raw.config)) return;
1758
+ if (!slots || !isRec3(raw.config)) return;
1618
1759
  const config = raw.config;
1619
1760
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
1620
1761
  for (const slot of slots) {
@@ -1622,7 +1763,7 @@ function walkFlowNodes(flow, flowPath) {
1622
1763
  if (slot === "branches") {
1623
1764
  if (!Array.isArray(value)) continue;
1624
1765
  value.forEach((branch, b) => {
1625
- if (!isRec2(branch)) return;
1766
+ if (!isRec3(branch)) return;
1626
1767
  const branchName = strName(branch.name) ?? `#${b}`;
1627
1768
  visitList(
1628
1769
  branch.nodes,
@@ -1633,7 +1774,7 @@ function walkFlowNodes(flow, flowPath) {
1633
1774
  });
1634
1775
  continue;
1635
1776
  }
1636
- if (!isRec2(value)) continue;
1777
+ if (!isRec3(value)) continue;
1637
1778
  visitList(
1638
1779
  value.nodes,
1639
1780
  `${path}.config.${slot}.nodes`,
@@ -1653,6 +1794,7 @@ function joinTrail(trail, segment) {
1653
1794
  // src/validate-flow-template-paths.ts
1654
1795
  var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
1655
1796
  var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
1797
+ var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
1656
1798
  function asArray5(v) {
1657
1799
  if (Array.isArray(v)) return v;
1658
1800
  if (v && typeof v === "object") {
@@ -1795,6 +1937,7 @@ function validateFlowTemplatePaths(stack) {
1795
1937
  const obj = objectsByName.get(objectName);
1796
1938
  if (!obj) return;
1797
1939
  const fieldTypes = fieldTypesOf(obj);
1940
+ const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
1798
1941
  const expandSet = declaredExpandOf(flow);
1799
1942
  walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
1800
1943
  const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
@@ -1806,6 +1949,7 @@ function validateFlowTemplatePaths(stack) {
1806
1949
  if (leaves.length === 0) return;
1807
1950
  const seenUnknown = /* @__PURE__ */ new Set();
1808
1951
  const seenTraversal = /* @__PURE__ */ new Set();
1952
+ const seenUnprovisioned = /* @__PURE__ */ new Set();
1809
1953
  for (const leaf of leaves) {
1810
1954
  const inFilter = leaf.inFilter;
1811
1955
  for (const rest of recordRefsIn(leaf.text)) {
@@ -1813,6 +1957,19 @@ function validateFlowTemplatePaths(stack) {
1813
1957
  const hasSubPath = rest.length > 1;
1814
1958
  const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
1815
1959
  const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
1960
+ if (unprovisionedAnchors.has(head)) {
1961
+ if (!seenUnprovisioned.has(head)) {
1962
+ seenUnprovisioned.add(head);
1963
+ findings.push({
1964
+ severity: "warning",
1965
+ rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
1966
+ where,
1967
+ path: nodePath,
1968
+ 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).`),
1969
+ hint: unprovisionedAnchorHint(objectName, head)
1970
+ });
1971
+ }
1972
+ }
1816
1973
  if (!isKnown) {
1817
1974
  if (seenUnknown.has(head)) continue;
1818
1975
  seenUnknown.add(head);
@@ -1957,10 +2114,32 @@ function containerViewCount(rec) {
1957
2114
  function validateViewContainers(stack) {
1958
2115
  const out = [];
1959
2116
  if (!stack || typeof stack !== "object") return out;
2117
+ const viewItems = stack.viewItems;
2118
+ if (viewItems != null && asEntries(viewItems).length > 0) {
2119
+ out.push({
2120
+ severity: "error",
2121
+ rule: VIEW_CONTAINER_SHAPE,
2122
+ where: "viewItems",
2123
+ path: "viewItems",
2124
+ 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.",
2125
+ 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."
2126
+ });
2127
+ }
1960
2128
  for (const { key, value } of asEntries(stack.views)) {
1961
2129
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
1962
2130
  const rec = value;
1963
- if (rec.viewKind != null) continue;
2131
+ if (rec.viewKind != null) {
2132
+ const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
2133
+ out.push({
2134
+ severity: "error",
2135
+ rule: VIEW_CONTAINER_SHAPE,
2136
+ where: `views${key}${label3}`,
2137
+ path: `views${key}`,
2138
+ 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).",
2139
+ 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:`."
2140
+ });
2141
+ continue;
2142
+ }
1964
2143
  if (containerViewCount(rec) > 0) continue;
1965
2144
  const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
1966
2145
  const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
@@ -2408,7 +2587,7 @@ function asArray10(v) {
2408
2587
  }
2409
2588
  return [];
2410
2589
  }
2411
- function isRec3(v) {
2590
+ function isRec4(v) {
2412
2591
  return !!v && typeof v === "object" && !Array.isArray(v);
2413
2592
  }
2414
2593
  function strName2(v) {
@@ -2484,7 +2663,7 @@ function distance(a, b) {
2484
2663
  }
2485
2664
  function indexObjectSearchTargets(stack) {
2486
2665
  const fieldsByObject = /* @__PURE__ */ new Map();
2487
- if (!isRec3(stack)) return fieldsByObject;
2666
+ if (!isRec4(stack)) return fieldsByObject;
2488
2667
  for (const obj of asArray10(stack.objects)) {
2489
2668
  const name = strName2(obj.name);
2490
2669
  if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
@@ -2566,7 +2745,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2566
2745
  }
2567
2746
  function validateSearchableFields(stack) {
2568
2747
  const findings = [];
2569
- if (!isRec3(stack)) return findings;
2748
+ if (!isRec4(stack)) return findings;
2570
2749
  const objects = asArray10(stack.objects);
2571
2750
  const fieldsByObject = indexObjectSearchTargets(stack);
2572
2751
  const check = (declared, objectName, where, path, subject, role) => {
@@ -2576,7 +2755,7 @@ function validateSearchableFields(stack) {
2576
2755
  };
2577
2756
  for (let oi = 0; oi < objects.length; oi++) {
2578
2757
  const obj = objects[oi];
2579
- if (!isRec3(obj)) continue;
2758
+ if (!isRec4(obj)) continue;
2580
2759
  const objName = strName2(obj.name);
2581
2760
  const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
2582
2761
  check(
@@ -2587,9 +2766,9 @@ function validateSearchableFields(stack) {
2587
2766
  "searchableFields",
2588
2767
  "canonical"
2589
2768
  );
2590
- if (isRec3(obj.listViews)) {
2769
+ if (isRec4(obj.listViews)) {
2591
2770
  for (const [key, lv] of Object.entries(obj.listViews)) {
2592
- if (!isRec3(lv)) continue;
2771
+ if (!isRec4(lv)) continue;
2593
2772
  check(
2594
2773
  lv.searchableFields,
2595
2774
  // A built-in list view belongs to its object; an inline `data.object`
@@ -2606,10 +2785,10 @@ function validateSearchableFields(stack) {
2606
2785
  const views = asArray10(stack.views);
2607
2786
  for (let vi = 0; vi < views.length; vi++) {
2608
2787
  const view = views[vi];
2609
- if (!isRec3(view)) continue;
2788
+ if (!isRec4(view)) continue;
2610
2789
  const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
2611
2790
  const viewObject = strName2(view.objectName) ?? strName2(view.object);
2612
- if (isRec3(view.list)) {
2791
+ if (isRec4(view.list)) {
2613
2792
  check(
2614
2793
  view.list.searchableFields,
2615
2794
  listViewObject(view.list) ?? viewObject,
@@ -2619,9 +2798,9 @@ function validateSearchableFields(stack) {
2619
2798
  "narrowing"
2620
2799
  );
2621
2800
  }
2622
- if (isRec3(view.listViews)) {
2801
+ if (isRec4(view.listViews)) {
2623
2802
  for (const [key, lv] of Object.entries(view.listViews)) {
2624
- if (!isRec3(lv)) continue;
2803
+ if (!isRec4(lv)) continue;
2625
2804
  check(
2626
2805
  lv.searchableFields,
2627
2806
  listViewObject(lv) ?? viewObject,
@@ -2637,11 +2816,11 @@ function validateSearchableFields(stack) {
2637
2816
  }
2638
2817
  function listViewObject(listView) {
2639
2818
  const data = listView.data;
2640
- return isRec3(data) ? strName2(data.object) : void 0;
2819
+ return isRec4(data) ? strName2(data.object) : void 0;
2641
2820
  }
2642
2821
 
2643
2822
  // src/page-walk.ts
2644
- function isRec4(v) {
2823
+ function isRec5(v) {
2645
2824
  return !!v && typeof v === "object" && !Array.isArray(v);
2646
2825
  }
2647
2826
  function strName3(v) {
@@ -2654,19 +2833,19 @@ function isSourceAuthoredPage(page) {
2654
2833
  }
2655
2834
  function walkPageComponents(page, pagePath) {
2656
2835
  const out = [];
2657
- if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
2836
+ if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
2658
2837
  const pageObject = strName3(page.object);
2659
2838
  const visit = (node, path, inheritedObject) => {
2660
- if (!isRec4(node)) return;
2661
- const props = isRec4(node.properties) ? node.properties : void 0;
2662
- const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
2839
+ if (!isRec5(node)) return;
2840
+ const props = isRec5(node.properties) ? node.properties : void 0;
2841
+ const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
2663
2842
  const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
2664
2843
  out.push({ component: node, path, objectName });
2665
2844
  if (!props) return;
2666
2845
  if (Array.isArray(props.items)) {
2667
2846
  for (let i = 0; i < props.items.length; i++) {
2668
2847
  const item = props.items[i];
2669
- if (!isRec4(item) || !Array.isArray(item.children)) continue;
2848
+ if (!isRec5(item) || !Array.isArray(item.children)) continue;
2670
2849
  for (let c = 0; c < item.children.length; c++) {
2671
2850
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
2672
2851
  }
@@ -2688,12 +2867,12 @@ function walkPageComponents(page, pagePath) {
2688
2867
  const regions = Array.isArray(page.regions) ? page.regions : [];
2689
2868
  for (let r = 0; r < regions.length; r++) {
2690
2869
  const region = regions[r];
2691
- if (!isRec4(region) || !Array.isArray(region.components)) continue;
2870
+ if (!isRec5(region) || !Array.isArray(region.components)) continue;
2692
2871
  for (let c = 0; c < region.components.length; c++) {
2693
2872
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
2694
2873
  }
2695
2874
  }
2696
- const slots = isRec4(page.slots) ? page.slots : void 0;
2875
+ const slots = isRec5(page.slots) ? page.slots : void 0;
2697
2876
  if (slots) {
2698
2877
  for (const [slot, value] of Object.entries(slots)) {
2699
2878
  const list3 = Array.isArray(value) ? value : [value];
@@ -2708,6 +2887,7 @@ function walkPageComponents(page, pagePath) {
2708
2887
 
2709
2888
  // src/validate-page-field-bindings.ts
2710
2889
  var PAGE_FIELD_UNKNOWN = "page-field-unknown";
2890
+ var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
2711
2891
  function asArray11(v) {
2712
2892
  if (Array.isArray(v)) return v;
2713
2893
  if (v && typeof v === "object") {
@@ -2718,7 +2898,7 @@ function asArray11(v) {
2718
2898
  function strName4(v) {
2719
2899
  return typeof v === "string" && v.length > 0 ? v : void 0;
2720
2900
  }
2721
- function isRec5(v) {
2901
+ function isRec6(v) {
2722
2902
  return !!v && typeof v === "object" && !Array.isArray(v);
2723
2903
  }
2724
2904
  function fieldRefsFrom(value, basePath) {
@@ -2729,7 +2909,7 @@ function fieldRefsFrom(value, basePath) {
2729
2909
  out.push({ name: bare, path });
2730
2910
  return;
2731
2911
  }
2732
- if (!isRec5(v)) return;
2912
+ if (!isRec6(v)) return;
2733
2913
  const named = strName4(v.field) ?? strName4(v.name);
2734
2914
  if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
2735
2915
  };
@@ -2787,15 +2967,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2787
2967
  const sections = Array.isArray(props[key]) ? props[key] : [];
2788
2968
  for (let si = 0; si < sections.length; si++) {
2789
2969
  const section = sections[si];
2790
- if (!isRec5(section)) continue;
2970
+ if (!isRec6(section)) continue;
2791
2971
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2792
2972
  }
2793
2973
  }
2794
2974
  return refs;
2795
2975
  }
2796
2976
  function relatedListFieldRefs(props, basePath, sep = ".") {
2797
- const add = isRec5(props.add) ? props.add : void 0;
2798
- const picker = add && isRec5(add.picker) ? add.picker : void 0;
2977
+ const add = isRec6(props.add) ? props.add : void 0;
2978
+ const picker = add && isRec6(add.picker) ? add.picker : void 0;
2799
2979
  const at = (key) => `${basePath}${sep}${key}`;
2800
2980
  return {
2801
2981
  relatedObject: strName4(props.objectName),
@@ -2816,7 +2996,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2816
2996
  }
2817
2997
  function indexObjectFields(stack) {
2818
2998
  const objectFields = /* @__PURE__ */ new Map();
2819
- if (!isRec5(stack)) return objectFields;
2999
+ if (!isRec6(stack)) return objectFields;
2820
3000
  for (const obj of asArray11(stack.objects)) {
2821
3001
  const name = strName4(obj.name);
2822
3002
  if (!name) continue;
@@ -2829,14 +3009,27 @@ function indexObjectFields(stack) {
2829
3009
  }
2830
3010
  return objectFields;
2831
3011
  }
2832
- function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
3012
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
2833
3013
  const findings = [];
2834
3014
  if (!objectName) return findings;
2835
3015
  const known = objectFields.get(objectName);
2836
3016
  if (!known) return findings;
3017
+ const anchors = unprovisionedAnchors?.get(objectName);
2837
3018
  for (const ref of refs) {
2838
3019
  if (ref.name.includes(".")) continue;
2839
- if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
3020
+ if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
3021
+ if (anchors?.has(ref.name)) {
3022
+ findings.push({
3023
+ severity: "warning",
3024
+ rule: PAGE_FIELD_UNPROVISIONED,
3025
+ where,
3026
+ path: ref.path,
3027
+ 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."),
3028
+ hint: unprovisionedAnchorHint(objectName, ref.name)
3029
+ });
3030
+ }
3031
+ continue;
3032
+ }
2840
3033
  findings.push({
2841
3034
  severity: consequence2 === "queried" ? "error" : "warning",
2842
3035
  rule: PAGE_FIELD_UNKNOWN,
@@ -2852,17 +3045,20 @@ function validatePageFieldBindings(stack) {
2852
3045
  const findings = [];
2853
3046
  if (!stack || typeof stack !== "object") return findings;
2854
3047
  const objectFields = indexObjectFields(stack);
3048
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2855
3049
  const pages = asArray11(stack.pages);
2856
3050
  for (let pi = 0; pi < pages.length; pi++) {
2857
3051
  const page = pages[pi];
2858
3052
  if (!page || typeof page !== "object") continue;
2859
3053
  const pageName = strName4(page.name) ?? `#${pi}`;
2860
3054
  const checkRefs = (refs, objectName, where) => {
2861
- findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
3055
+ findings.push(
3056
+ ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
3057
+ );
2862
3058
  };
2863
3059
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2864
3060
  const type = strName4(component.type);
2865
- const props = isRec5(component.properties) ? component.properties : void 0;
3061
+ const props = isRec6(component.properties) ? component.properties : void 0;
2866
3062
  if (!type || !props) continue;
2867
3063
  const where = `page "${pageName}" \xB7 ${type}`;
2868
3064
  const base = `${path}.properties`;
@@ -2877,7 +3073,7 @@ function validatePageFieldBindings(stack) {
2877
3073
  if (!refs) continue;
2878
3074
  checkRefs(refs, objectName, where);
2879
3075
  }
2880
- const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
3076
+ const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
2881
3077
  if (cfg) {
2882
3078
  const cfgObject = strName4(cfg.source) ?? strName4(page.object);
2883
3079
  const base = `pages[${pi}].interfaceConfig`;
@@ -2886,7 +3082,7 @@ function validatePageFieldBindings(stack) {
2886
3082
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2887
3083
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2888
3084
  ];
2889
- const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
3085
+ const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
2890
3086
  if (userFilters) {
2891
3087
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2892
3088
  }
@@ -2897,11 +3093,11 @@ function validatePageFieldBindings(stack) {
2897
3093
  }
2898
3094
 
2899
3095
  // src/zod-issue-format.ts
2900
- var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3096
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2901
3097
  var valueAtPath = (root, path) => {
2902
3098
  let cur = root;
2903
3099
  for (const key of path) {
2904
- if (!isRec6(cur) && !Array.isArray(cur)) return void 0;
3100
+ if (!isRec7(cur) && !Array.isArray(cur)) return void 0;
2905
3101
  cur = cur[key];
2906
3102
  }
2907
3103
  return cur;
@@ -3034,12 +3230,13 @@ function filterAttrValue(tsc, sf, attr) {
3034
3230
  return perPosition(init.expression);
3035
3231
  }
3036
3232
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
3233
+ var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
3037
3234
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
3038
3235
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
3039
3236
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
3040
3237
  function checkChartDrillDown(raw, push2) {
3041
3238
  if (raw === void 0 || raw === NOT_STATIC) return;
3042
- if (!isRec7(raw)) {
3239
+ if (!isRec8(raw)) {
3043
3240
  push2(
3044
3241
  "error",
3045
3242
  REACT_CHART_DRILLDOWN_INVALID,
@@ -3062,7 +3259,7 @@ function checkChartDrillDown(raw, push2) {
3062
3259
  }
3063
3260
  function checkChartAggregate(raw, push2) {
3064
3261
  if (raw === void 0 || raw === NOT_STATIC) return;
3065
- if (!isRec7(raw)) {
3262
+ if (!isRec8(raw)) {
3066
3263
  push2(
3067
3264
  "error",
3068
3265
  REACT_CHART_AGGREGATE_INVALID,
@@ -3093,9 +3290,9 @@ function checkChartAggregate(raw, push2) {
3093
3290
  );
3094
3291
  }
3095
3292
  }
3096
- var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3293
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3097
3294
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
3098
- function checkObjectChart(attrs, objectFields, findings) {
3295
+ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
3099
3296
  const { values, where, path } = attrs;
3100
3297
  const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
3101
3298
  checkChartDrillDown(values.get("drillDown"), push2);
@@ -3103,18 +3300,29 @@ function checkObjectChart(attrs, objectFields, findings) {
3103
3300
  const aggregate = values.get("aggregate");
3104
3301
  checkChartAggregate(aggregate, push2);
3105
3302
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
3106
- if (!isRec7(aggregate)) return;
3303
+ if (!isRec8(aggregate)) return;
3107
3304
  const fn = strOf(aggregate.function);
3108
3305
  const field = strOf(aggregate.field);
3109
3306
  const groupBy = aggregate.groupBy;
3110
- const groupByField = strOf(groupBy) ?? (isRec7(groupBy) ? strOf(groupBy.field) : void 0);
3307
+ const groupByField = strOf(groupBy) ?? (isRec8(groupBy) ? strOf(groupBy.field) : void 0);
3111
3308
  const objectName = strOf(values.get("objectName"));
3112
3309
  const known = objectName ? objectFields.get(objectName) : void 0;
3113
3310
  if (objectName && known) {
3311
+ const anchors = unprovisionedAnchors.get(objectName);
3114
3312
  const fieldRef = (name, prop) => {
3115
3313
  if (!name) return;
3116
3314
  if (name.includes(".")) return;
3117
- if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
3315
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) {
3316
+ if (anchors?.has(name)) {
3317
+ push2(
3318
+ "warning",
3319
+ REACT_CHART_FIELD_UNPROVISIONED,
3320
+ `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.`,
3321
+ unprovisionedAnchorHint(objectName, name)
3322
+ );
3323
+ }
3324
+ return;
3325
+ }
3118
3326
  push2(
3119
3327
  "error",
3120
3328
  REACT_CHART_FIELD_UNKNOWN,
@@ -3140,18 +3348,18 @@ function checkObjectChart(attrs, objectFields, findings) {
3140
3348
  );
3141
3349
  };
3142
3350
  const xAxisRaw = values.get("xAxis");
3143
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec7(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
3351
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec8(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
3144
3352
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
3145
3353
  axisRef(categoryAxis, categoryProp);
3146
3354
  const yAxisRaw = values.get("yAxis");
3147
3355
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
3148
3356
  for (const a of yAxisList) {
3149
- axisRef(strOf(a) ?? (isRec7(a) ? strOf(a.field) : void 0), "yAxis[].field");
3357
+ axisRef(strOf(a) ?? (isRec8(a) ? strOf(a.field) : void 0), "yAxis[].field");
3150
3358
  }
3151
3359
  const series = values.get("series");
3152
3360
  if (Array.isArray(series)) {
3153
3361
  for (const s of series) {
3154
- if (!isRec7(s)) continue;
3362
+ if (!isRec8(s)) continue;
3155
3363
  const dataKey = strOf(s.dataKey);
3156
3364
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
3157
3365
  }
@@ -3207,7 +3415,7 @@ function subformFieldRefs(value, basePath) {
3207
3415
  if (!Array.isArray(value)) return { child, parent };
3208
3416
  for (let i = 0; i < value.length; i++) {
3209
3417
  const sub = value[i];
3210
- if (!isRec7(sub)) continue;
3418
+ if (!isRec8(sub)) continue;
3211
3419
  const at = (key) => `${basePath}[${i}].${key}`;
3212
3420
  child.push({
3213
3421
  objectName: strOf(sub.childObject),
@@ -3252,20 +3460,20 @@ function reactFieldRefs(spec, values, basePath) {
3252
3460
  }
3253
3461
  for (const key of spec.nestedFields ?? []) {
3254
3462
  const v = readable(key);
3255
- if (isRec7(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
3463
+ if (isRec8(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
3256
3464
  }
3257
3465
  for (const key of spec.sections ?? []) {
3258
3466
  const v = readable(key);
3259
3467
  if (!Array.isArray(v)) continue;
3260
3468
  for (let i = 0; i < v.length; i++) {
3261
3469
  const section = v[i];
3262
- if (!isRec7(section)) continue;
3470
+ if (!isRec8(section)) continue;
3263
3471
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
3264
3472
  }
3265
3473
  }
3266
3474
  for (const key of spec.keyedByField ?? []) {
3267
3475
  const v = readable(key);
3268
- if (!isRec7(v)) continue;
3476
+ if (!isRec8(v)) continue;
3269
3477
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
3270
3478
  }
3271
3479
  for (const key of spec.filterArrays ?? []) {
@@ -3273,22 +3481,30 @@ function reactFieldRefs(spec, values, basePath) {
3273
3481
  }
3274
3482
  return { own, queried };
3275
3483
  }
3276
- function checkBlockFieldProps(tag, values, objectFields, where, path) {
3484
+ function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
3277
3485
  const objectName = strOf(values.get("objectName"));
3278
3486
  const out = [];
3279
3487
  const spec = REACT_FIELD_SPECS[tag];
3280
3488
  if (spec) {
3281
3489
  const { own, queried } = reactFieldRefs(spec, values, path);
3282
- out.push(...checkFieldRefs(own, objectName, objectFields, where));
3283
- out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
3490
+ out.push(
3491
+ ...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
3492
+ );
3493
+ out.push(
3494
+ ...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
3495
+ );
3284
3496
  }
3285
3497
  if (tag === "ObjectForm") {
3286
3498
  const raw = values.get("subforms");
3287
3499
  const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
3288
3500
  for (const sub of subs.child) {
3289
- out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
3501
+ out.push(
3502
+ ...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
3503
+ );
3290
3504
  }
3291
- out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
3505
+ out.push(
3506
+ ...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
3507
+ );
3292
3508
  }
3293
3509
  const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
3294
3510
  if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
@@ -3297,7 +3513,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
3297
3513
  componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
3298
3514
  objectName,
3299
3515
  objectFields,
3300
- where
3516
+ where,
3517
+ "skipped",
3518
+ unprovisionedAnchors
3301
3519
  )
3302
3520
  );
3303
3521
  }
@@ -3329,6 +3547,7 @@ function localComponentNames(tsc, sf) {
3329
3547
  function validateReactPageProps(stack) {
3330
3548
  const findings = [];
3331
3549
  const objectFields = indexObjectFields(stack);
3550
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
3332
3551
  const searchTargets = indexObjectSearchTargets(stack);
3333
3552
  const pages = asArray12(stack.pages);
3334
3553
  for (let p = 0; p < pages.length; p++) {
@@ -3411,7 +3630,7 @@ function validateReactPageProps(stack) {
3411
3630
  }
3412
3631
  }
3413
3632
  if (tag === "ObjectChart" && !hasSpread) {
3414
- checkObjectChart({ values, where, path }, objectFields, findings);
3633
+ checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
3415
3634
  }
3416
3635
  if (tag === "ListView" && !hasSpread) {
3417
3636
  findings.push(
@@ -3427,7 +3646,7 @@ function validateReactPageProps(stack) {
3427
3646
  }
3428
3647
  if (!hasSpread) {
3429
3648
  findings.push(
3430
- ...checkBlockFieldProps(tag, values, objectFields, where, path)
3649
+ ...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
3431
3650
  );
3432
3651
  }
3433
3652
  }
@@ -3519,6 +3738,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
3519
3738
  var FIELD_GROUP_EMPTY = "field-group-empty";
3520
3739
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
3521
3740
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
3741
+ var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
3522
3742
  function asArray15(v) {
3523
3743
  if (Array.isArray(v)) return v;
3524
3744
  if (v && typeof v === "object") {
@@ -3537,6 +3757,15 @@ function validateSemanticRoles(stack) {
3537
3757
  const path = `objects[${i}]`;
3538
3758
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
3539
3759
  const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
3760
+ const unprovisioned = unprovisionedInjectedColumnsFor(obj);
3761
+ const unprovisionedPointer = (slot, entry) => ({
3762
+ severity: "warning",
3763
+ rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
3764
+ where,
3765
+ path: `${path}.${slot}`,
3766
+ 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.`,
3767
+ 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.`
3768
+ });
3540
3769
  const declaredGroups = new Set(
3541
3770
  (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
3542
3771
  );
@@ -3578,10 +3807,16 @@ function validateSemanticRoles(stack) {
3578
3807
  message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
3579
3808
  hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
3580
3809
  });
3810
+ } else if (typeof stage === "string" && unprovisioned.has(stage)) {
3811
+ findings.push(unprovisionedPointer("stageField", stage));
3581
3812
  }
3582
3813
  const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
3583
3814
  for (const entry of highlights) {
3584
- if (typeof entry !== "string" || entry.length === 0 || fieldNames.has(entry)) continue;
3815
+ if (typeof entry !== "string" || entry.length === 0) continue;
3816
+ if (fieldNames.has(entry)) {
3817
+ if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
3818
+ continue;
3819
+ }
3585
3820
  findings.push({
3586
3821
  severity: "warning",
3587
3822
  rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -3621,45 +3856,45 @@ function validateSemanticRoles(stack) {
3621
3856
  }
3622
3857
 
3623
3858
  // src/collection-entries.ts
3624
- function isRec8(v) {
3859
+ function isRec9(v) {
3625
3860
  return !!v && typeof v === "object" && !Array.isArray(v);
3626
3861
  }
3627
3862
  function collectionEntries(v, base) {
3628
3863
  if (Array.isArray(v)) {
3629
3864
  const out = [];
3630
3865
  for (let i = 0; i < v.length; i++) {
3631
- if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3866
+ if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3632
3867
  }
3633
3868
  return out;
3634
3869
  }
3635
- if (isRec8(v)) {
3636
- return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3870
+ if (isRec9(v)) {
3871
+ return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3637
3872
  }
3638
3873
  return [];
3639
3874
  }
3640
3875
 
3641
3876
  // src/view-walk.ts
3642
- function isRec9(v) {
3877
+ function isRec10(v) {
3643
3878
  return !!v && typeof v === "object" && !Array.isArray(v);
3644
3879
  }
3645
3880
  function strName5(v) {
3646
3881
  return typeof v === "string" && v.length > 0 ? v : void 0;
3647
3882
  }
3648
3883
  function viewObjectName(view) {
3649
- return strName5(view.objectName) ?? strName5(view.object) ?? (isRec9(view.data) ? strName5(view.data.object) : void 0);
3884
+ return strName5(view.objectName) ?? strName5(view.object) ?? (isRec10(view.data) ? strName5(view.data.object) : void 0);
3650
3885
  }
3651
3886
  function viewContainerSites(view, basePath) {
3652
- if (!isRec9(view)) return [];
3887
+ if (!isRec10(view)) return [];
3653
3888
  const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3654
- if (isRec9(view.form)) {
3889
+ if (isRec10(view.form)) {
3655
3890
  sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3656
3891
  }
3657
3892
  for (const key of ["listViews", "formViews"]) {
3658
3893
  const container = view[key];
3659
- if (!isRec9(container)) continue;
3894
+ if (!isRec10(container)) continue;
3660
3895
  const kind = key === "listViews" ? "listView" : "formView";
3661
3896
  for (const [subKey, sub] of Object.entries(container)) {
3662
- if (!isRec9(sub)) continue;
3897
+ if (!isRec10(sub)) continue;
3663
3898
  sites.push({
3664
3899
  view: sub,
3665
3900
  path: `${basePath}.${key}.${subKey}`,
@@ -3684,7 +3919,7 @@ function asArray16(v) {
3684
3919
  }
3685
3920
  return [];
3686
3921
  }
3687
- function isRec10(v) {
3922
+ function isRec11(v) {
3688
3923
  return !!v && typeof v === "object" && !Array.isArray(v);
3689
3924
  }
3690
3925
  function strName6(v) {
@@ -3718,7 +3953,7 @@ function validateFormLayout(stack) {
3718
3953
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
3719
3954
  for (let s = 0; s < sections.length; s++) {
3720
3955
  const sec = sections[s];
3721
- const secFields = isRec10(sec) && Array.isArray(sec.fields) ? sec.fields : [];
3956
+ const secFields = isRec11(sec) && Array.isArray(sec.fields) ? sec.fields : [];
3722
3957
  for (let f = 0; f < secFields.length; f++) {
3723
3958
  const entry = secFields[f];
3724
3959
  const fname = fieldNameOf(entry);
@@ -3733,7 +3968,7 @@ function validateFormLayout(stack) {
3733
3968
  hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
3734
3969
  });
3735
3970
  }
3736
- const colSpan = isRec10(entry) ? entry.colSpan : void 0;
3971
+ const colSpan = isRec11(entry) ? entry.colSpan : void 0;
3737
3972
  if (colSpan != null) {
3738
3973
  findings.push({
3739
3974
  severity: "warning",
@@ -3756,9 +3991,67 @@ function validateFormLayout(stack) {
3756
3991
  import {
3757
3992
  collectCelRootIdentifiers as collectCelRootIdentifiers2,
3758
3993
  firstUndeclaredReference,
3759
- parseCelToAst as parseCelToAst2,
3994
+ parseCelToAst as parseCelToAst3,
3760
3995
  parseCelToAstWithReason
3761
3996
  } from "@objectstack/formula";
3997
+
3998
+ // src/predicate-rhs-position.ts
3999
+ function isNode2(v) {
4000
+ return !!v && typeof v === "object" && typeof v.op === "string";
4001
+ }
4002
+ var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
4003
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
4004
+ function bareId(node) {
4005
+ if (!isNode2(node)) return null;
4006
+ return node.op === "id" && typeof node.args === "string" ? node.args : null;
4007
+ }
4008
+ function bareRhsOnlyIdentifiers(ast) {
4009
+ const rhs = /* @__PURE__ */ new Set();
4010
+ const elsewhere = /* @__PURE__ */ new Set();
4011
+ const walk = (node, suppressible) => {
4012
+ if (Array.isArray(node)) {
4013
+ for (const child of node) walk(child, suppressible);
4014
+ return;
4015
+ }
4016
+ if (!isNode2(node)) return;
4017
+ const args = node.args;
4018
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
4019
+ walk(args[1], suppressible);
4020
+ walk(args[2], false);
4021
+ return;
4022
+ }
4023
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
4024
+ const right = suppressible ? bareId(args[1]) : null;
4025
+ walk(args[0], suppressible);
4026
+ if (right !== null) {
4027
+ rhs.add(right);
4028
+ return;
4029
+ }
4030
+ walk(args[1], suppressible);
4031
+ return;
4032
+ }
4033
+ const name = bareId(node);
4034
+ if (name !== null) {
4035
+ elsewhere.add(name);
4036
+ return;
4037
+ }
4038
+ walk(args, suppressible);
4039
+ };
4040
+ walk(ast, true);
4041
+ for (const name of elsewhere) rhs.delete(name);
4042
+ return rhs;
4043
+ }
4044
+ function isRec12(v) {
4045
+ return !!v && typeof v === "object" && !Array.isArray(v);
4046
+ }
4047
+ function schemaIdOf(view) {
4048
+ const data = view.data;
4049
+ if (!isRec12(data)) return void 0;
4050
+ if (data.provider !== "schema") return void 0;
4051
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
4052
+ }
4053
+
4054
+ // src/validate-visibility-predicates.ts
3762
4055
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
3763
4056
  var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
3764
4057
  var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
@@ -3806,7 +4099,7 @@ function boundName(overrun) {
3806
4099
  return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
3807
4100
  }
3808
4101
  var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
3809
- function isNode2(v) {
4102
+ function isNode3(v) {
3810
4103
  return !!v && typeof v === "object" && typeof v.op === "string";
3811
4104
  }
3812
4105
  function namespaceRoots(node, out) {
@@ -3814,22 +4107,27 @@ function namespaceRoots(node, out) {
3814
4107
  for (const child of node) namespaceRoots(child, out);
3815
4108
  return;
3816
4109
  }
3817
- if (!isNode2(node)) return;
4110
+ if (!isNode3(node)) return;
3818
4111
  const args = node.args;
3819
4112
  if (Array.isArray(args)) {
3820
4113
  const receiver = node.op === "rcall" ? args[1] : args[0];
3821
- if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
4114
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
3822
4115
  out.add(receiver.args);
3823
4116
  }
3824
4117
  }
3825
4118
  namespaceRoots(args, out);
3826
4119
  }
3827
- function firstBareIdentifier(source) {
3828
- const ast = parseCelToAst2(source);
4120
+ function firstBareIdentifier(source, literalRhs) {
4121
+ const ast = parseCelToAst3(source);
3829
4122
  if (!ast) return null;
3830
4123
  const rooted = /* @__PURE__ */ new Set();
3831
4124
  namespaceRoots(ast, rooted);
3832
- return firstUndeclaredReference(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
4125
+ const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
4126
+ return firstUndeclaredReference(source, [
4127
+ ...VIEW_PAGE_EXTRA_ROOTS,
4128
+ ...rooted,
4129
+ ...literalSlot
4130
+ ]);
3833
4131
  }
3834
4132
  var CANONICAL_ROOT_BY_LAYER = {
3835
4133
  runtime: "record",
@@ -3838,16 +4136,16 @@ var CANONICAL_ROOT_BY_LAYER = {
3838
4136
  var MISLAYER_BY_LAYER = {
3839
4137
  runtime: {
3840
4138
  forbiddenRoot: "data",
3841
- 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).",
4139
+ 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).",
3842
4140
  hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
3843
4141
  },
3844
4142
  metadata: {
3845
4143
  forbiddenRoot: "record",
3846
- 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).",
4144
+ 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).",
3847
4145
  hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
3848
4146
  }
3849
4147
  };
3850
- function checkElement(el, where, path, layer, findings) {
4148
+ function checkElement(el, where, path, layer, findings, literalRhs = false) {
3851
4149
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
3852
4150
  const source = predicateSource(raw);
3853
4151
  const rule = MISLAYER_BY_LAYER[layer];
@@ -3885,7 +4183,7 @@ function checkElement(el, where, path, layer, findings) {
3885
4183
  });
3886
4184
  }
3887
4185
  if (source && !refusal) {
3888
- const bare = firstBareIdentifier(source);
4186
+ const bare = firstBareIdentifier(source, literalRhs);
3889
4187
  if (bare) {
3890
4188
  const root = CANONICAL_ROOT_BY_LAYER[layer];
3891
4189
  findings.push({
@@ -3894,7 +4192,7 @@ function checkElement(el, where, path, layer, findings) {
3894
4192
  where,
3895
4193
  path,
3896
4194
  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).`,
3897
- 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`).")
4195
+ 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`).")
3898
4196
  });
3899
4197
  }
3900
4198
  }
@@ -3903,24 +4201,27 @@ function isFieldObject(entry) {
3903
4201
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
3904
4202
  }
3905
4203
  function validateVisibilityPredicates(stack, opts = {}) {
3906
- const layer = opts.layer ?? "runtime";
4204
+ const declaredLayer = opts.layer ?? "runtime";
3907
4205
  const findings = [];
3908
4206
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
3909
4207
  const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
3910
4208
  for (const site of formViewSites(view, viewPath)) {
3911
4209
  const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
4210
+ const schemaBound = schemaIdOf(site.view) !== void 0;
4211
+ const literalRhs = schemaBound;
4212
+ const layer = schemaBound ? "metadata" : declaredLayer;
3912
4213
  for (const bucket of ["sections", "groups"]) {
3913
4214
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
3914
4215
  for (let s = 0; s < sections.length; s++) {
3915
4216
  const sec = sections[s];
3916
4217
  if (!sec || typeof sec !== "object") continue;
3917
4218
  const secPath = `${site.path}.${bucket}[${s}]`;
3918
- checkElement(sec, where, secPath, layer, findings);
4219
+ checkElement(sec, where, secPath, layer, findings, literalRhs);
3919
4220
  const secFields = Array.isArray(sec.fields) ? sec.fields : [];
3920
4221
  for (let f = 0; f < secFields.length; f++) {
3921
4222
  const entry = secFields[f];
3922
4223
  if (isFieldObject(entry)) {
3923
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
4224
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
3924
4225
  }
3925
4226
  }
3926
4227
  }
@@ -3931,21 +4232,21 @@ function validateVisibilityPredicates(stack, opts = {}) {
3931
4232
  const pageName = typeof page.name === "string" ? page.name : void 0;
3932
4233
  const where = `page "${pageName ?? pagePath}"`;
3933
4234
  for (const walked of walkPageComponents(page, pagePath)) {
3934
- checkElement(walked.component, where, walked.path, layer, findings);
4235
+ checkElement(walked.component, where, walked.path, declaredLayer, findings);
3935
4236
  }
3936
4237
  }
3937
4238
  return findings;
3938
4239
  }
3939
4240
 
3940
4241
  // src/validate-predicate-path-refs.ts
3941
- import { parseCelToAst as parseCelToAst3 } from "@objectstack/formula";
4242
+ import { parseCelToAst as parseCelToAst4 } from "@objectstack/formula";
3942
4243
  import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
3943
4244
  import { findClosestMatches, formatSuggestion } from "@objectstack/spec";
3944
4245
  var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
3945
4246
  var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
4247
+ var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
3946
4248
  var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
3947
4249
  var ROOT = "data";
3948
- var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
3949
4250
  function defOf(schema) {
3950
4251
  if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
3951
4252
  const s = schema;
@@ -4038,11 +4339,11 @@ function stepInto(scope, segment) {
4038
4339
  if (!declared.includes(segment)) return { kind: "undeclared", declared };
4039
4340
  return { kind: "declared", next: propertyOf(u, segment) };
4040
4341
  }
4041
- function isNode3(v) {
4342
+ function isNode4(v) {
4042
4343
  return !!v && typeof v === "object" && typeof v.op === "string";
4043
4344
  }
4044
4345
  function memberChain(node) {
4045
- if (!isNode3(node)) return null;
4346
+ if (!isNode4(node)) return null;
4046
4347
  if (node.op === "id" && typeof node.args === "string") return [node.args];
4047
4348
  if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
4048
4349
  const head = memberChain(node.args[0]);
@@ -4055,7 +4356,7 @@ function rootedPaths(node, out) {
4055
4356
  for (const child of node) rootedPaths(child, out);
4056
4357
  return;
4057
4358
  }
4058
- if (!isNode3(node)) return;
4359
+ if (!isNode4(node)) return;
4059
4360
  if (node.op === ".") {
4060
4361
  const chain = memberChain(node);
4061
4362
  if (chain && chain[0] === ROOT && chain.length > 1) {
@@ -4065,23 +4366,40 @@ function rootedPaths(node, out) {
4065
4366
  }
4066
4367
  rootedPaths(node.args, out);
4067
4368
  }
4369
+ var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
4370
+ function equalitySites(node, out) {
4371
+ if (Array.isArray(node)) {
4372
+ for (const child of node) equalitySites(child, out);
4373
+ return;
4374
+ }
4375
+ if (!isNode4(node)) return;
4376
+ const args = node.args;
4377
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
4378
+ equalitySites(args[1], out);
4379
+ return;
4380
+ }
4381
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
4382
+ out.push({ op: node.op, right: args[1] });
4383
+ }
4384
+ equalitySites(args, out);
4385
+ }
4068
4386
  function classifyIdentifiers(node, values, excluded) {
4069
4387
  if (Array.isArray(node)) {
4070
4388
  for (const child of node) classifyIdentifiers(child, values, excluded);
4071
4389
  return;
4072
4390
  }
4073
- if (!isNode3(node)) return;
4391
+ if (!isNode4(node)) return;
4074
4392
  const args = node.args;
4075
4393
  if (Array.isArray(args)) {
4076
4394
  const receiver = node.op === "rcall" ? args[1] : args[0];
4077
- if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
4395
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
4078
4396
  excluded.add(receiver.args);
4079
4397
  }
4080
4398
  if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
4081
4399
  const macroArgs = args[2];
4082
4400
  if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
4083
4401
  const bound = macroArgs[0];
4084
- if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
4402
+ if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
4085
4403
  excluded.add(bound.args);
4086
4404
  }
4087
4405
  }
@@ -4100,17 +4418,11 @@ function predicateSource2(v) {
4100
4418
  }
4101
4419
  return void 0;
4102
4420
  }
4103
- function isRec11(v) {
4421
+ function isRec13(v) {
4104
4422
  return !!v && typeof v === "object" && !Array.isArray(v);
4105
4423
  }
4106
- function schemaIdOf(view) {
4107
- const data = view.data;
4108
- if (!isRec11(data)) return void 0;
4109
- if (data.provider !== "schema") return void 0;
4110
- return typeof data.schemaId === "string" ? data.schemaId : void 0;
4111
- }
4112
4424
  function checkPredicate(source, scope, where, path, findings) {
4113
- const ast = parseCelToAst3(source);
4425
+ const ast = parseCelToAst4(source);
4114
4426
  if (!ast) return;
4115
4427
  const paths = [];
4116
4428
  rootedPaths(ast, paths);
@@ -4138,19 +4450,38 @@ function checkPredicate(source, scope, where, path, findings) {
4138
4450
  }
4139
4451
  }
4140
4452
  const declaredHere = keysOf(scope);
4141
- if (!declaredHere) return;
4142
- const values = /* @__PURE__ */ new Set();
4143
- const excluded = /* @__PURE__ */ new Set();
4144
- classifyIdentifiers(ast, values, excluded);
4145
- for (const id of values) {
4146
- if (excluded.has(id) || !declaredHere.includes(id)) continue;
4453
+ const rhsOnly = bareRhsOnlyIdentifiers(ast);
4454
+ if (declaredHere) {
4455
+ const values = /* @__PURE__ */ new Set();
4456
+ const excluded = /* @__PURE__ */ new Set();
4457
+ classifyIdentifiers(ast, values, excluded);
4458
+ for (const id of values) {
4459
+ if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
4460
+ findings.push({
4461
+ severity: "error",
4462
+ rule: PREDICATE_PATH_UNROOTED,
4463
+ where,
4464
+ path,
4465
+ 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).`,
4466
+ 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).`
4467
+ });
4468
+ }
4469
+ }
4470
+ const sites = [];
4471
+ equalitySites(ast, sites);
4472
+ for (const { op, right } of sites) {
4473
+ const chain = memberChain(right);
4474
+ if (!chain) continue;
4475
+ const text = chain.join(".");
4476
+ if (!PATH_SHAPED_RHS.test(text)) continue;
4477
+ const dotted = chain.length > 1;
4147
4478
  findings.push({
4148
- severity: "error",
4149
- rule: PREDICATE_PATH_UNROOTED,
4479
+ severity: dotted ? "error" : "warning",
4480
+ rule: PREDICATE_RHS_PATH_SHAPED,
4150
4481
  where,
4151
4482
  path,
4152
- 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).`,
4153
- 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).`
4483
+ 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).`,
4484
+ 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.`
4154
4485
  });
4155
4486
  }
4156
4487
  }
@@ -4158,7 +4489,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
4158
4489
  if (!Array.isArray(entries) || depth > 12) return;
4159
4490
  for (let i = 0; i < entries.length; i++) {
4160
4491
  const entry = entries[i];
4161
- if (!isRec11(entry)) continue;
4492
+ if (!isRec13(entry)) continue;
4162
4493
  const path = `${base}[${i}]`;
4163
4494
  for (const key of PREDICATE_KEYS) {
4164
4495
  const source = predicateSource2(entry[key]);
@@ -4185,15 +4516,14 @@ function validatePredicatePathRefs(stack, opts = {}) {
4185
4516
  try {
4186
4517
  root = resolveSchema(schemaId);
4187
4518
  } catch {
4188
- continue;
4519
+ root = void 0;
4189
4520
  }
4190
- if (!root) continue;
4191
4521
  const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
4192
4522
  for (const bucket of ["sections", "groups"]) {
4193
4523
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
4194
4524
  for (let s = 0; s < sections.length; s++) {
4195
4525
  const section = sections[s];
4196
- if (!isRec11(section)) continue;
4526
+ if (!isRec13(section)) continue;
4197
4527
  const sectionPath = `${site.path}.${bucket}[${s}]`;
4198
4528
  for (const key of PREDICATE_KEYS) {
4199
4529
  const source = predicateSource2(section[key]);
@@ -4642,6 +4972,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
4642
4972
  var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
4643
4973
  var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
4644
4974
  var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
4975
+ var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
4645
4976
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
4646
4977
  var OWD_ALIAS_FIX = {
4647
4978
  read: "public_read",
@@ -4687,6 +5018,13 @@ function firstMasterDetailField(obj) {
4687
5018
  }
4688
5019
  return void 0;
4689
5020
  }
5021
+ function resolveCbpRelation(obj) {
5022
+ const entries = asArray19(obj.fields);
5023
+ const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
5024
+ const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
5025
+ if (!found) return void 0;
5026
+ return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
5027
+ }
4690
5028
  function grantsObjectAccess(p) {
4691
5029
  return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
4692
5030
  }
@@ -4732,6 +5070,16 @@ function validateSecurityPosture(stack, opts) {
4732
5070
  });
4733
5071
  }
4734
5072
  }
5073
+ if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
5074
+ findings.push({
5075
+ severity: "error",
5076
+ rule: SECURITY_CBP_NO_RELATION,
5077
+ where: `object "${objName}"`,
5078
+ path: `${objPath}.sharingModel`,
5079
+ 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.`,
5080
+ 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'.`
5081
+ });
5082
+ }
4735
5083
  if (typeof external === "string") {
4736
5084
  if (OWD_ALIAS_FIX[external]) {
4737
5085
  findings.push({
@@ -4797,53 +5145,6 @@ function validateSecurityPosture(stack, opts) {
4797
5145
  }
4798
5146
  }
4799
5147
  }
4800
- const flagRole = (kind, name, label2, where, path) => {
4801
- if (identifierHasRoleToken(name)) {
4802
- findings.push({
4803
- severity: "error",
4804
- rule: SECURITY_ROLE_WORD,
4805
- where,
4806
- path,
4807
- 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).`,
4808
- hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
4809
- });
4810
- } else if (labelHasRoleWord(label2)) {
4811
- findings.push({
4812
- severity: "error",
4813
- rule: SECURITY_ROLE_WORD,
4814
- where,
4815
- path: `${path.replace(/\.name$/, "")}.label`,
4816
- message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
4817
- hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
4818
- });
4819
- }
4820
- };
4821
- for (let i = 0; i < objects.length; i++) {
4822
- const obj = objects[i];
4823
- if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
4824
- const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
4825
- flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
4826
- for (const f of asArray19(obj.fields)) {
4827
- flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
4828
- }
4829
- for (const [ai, action] of asArray19(obj.actions).entries()) {
4830
- flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
4831
- }
4832
- }
4833
- for (let i = 0; i < permissionSets.length; i++) {
4834
- const ps = permissionSets[i];
4835
- if (!ps || typeof ps !== "object") continue;
4836
- flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
4837
- }
4838
- for (const [i, pos] of asArray19(stack.positions).entries()) {
4839
- flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
4840
- }
4841
- for (const [i, app] of asArray19(stack.apps).entries()) {
4842
- flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
4843
- }
4844
- for (const [i, book] of asArray19(stack.books).entries()) {
4845
- flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
4846
- }
4847
5148
  const stackSetNames = new Set(
4848
5149
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
4849
5150
  );
@@ -4964,6 +5265,60 @@ function validateSecurityPosture(stack, opts) {
4964
5265
  }
4965
5266
  return findings;
4966
5267
  }
5268
+ function validateSecurityRoleWord(stack) {
5269
+ const findings = [];
5270
+ if (!stack || typeof stack !== "object") return findings;
5271
+ const objects = asArray19(stack.objects);
5272
+ const permissionSets = asArray19(stack.permissions);
5273
+ const flagRole = (kind, name, label2, where, path) => {
5274
+ if (identifierHasRoleToken(name)) {
5275
+ findings.push({
5276
+ severity: "error",
5277
+ rule: SECURITY_ROLE_WORD,
5278
+ where,
5279
+ path,
5280
+ 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).`,
5281
+ hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
5282
+ });
5283
+ } else if (labelHasRoleWord(label2)) {
5284
+ findings.push({
5285
+ severity: "error",
5286
+ rule: SECURITY_ROLE_WORD,
5287
+ where,
5288
+ path: `${path.replace(/\.name$/, "")}.label`,
5289
+ message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
5290
+ hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
5291
+ });
5292
+ }
5293
+ };
5294
+ for (let i = 0; i < objects.length; i++) {
5295
+ const obj = objects[i];
5296
+ if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
5297
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
5298
+ flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
5299
+ for (const f of asArray19(obj.fields)) {
5300
+ flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
5301
+ }
5302
+ for (const [ai, action] of asArray19(obj.actions).entries()) {
5303
+ flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
5304
+ }
5305
+ }
5306
+ for (let i = 0; i < permissionSets.length; i++) {
5307
+ const ps = permissionSets[i];
5308
+ if (!ps || typeof ps !== "object") continue;
5309
+ flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
5310
+ }
5311
+ for (const [i, pos] of asArray19(stack.positions).entries()) {
5312
+ flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
5313
+ }
5314
+ for (const [i, app] of asArray19(stack.apps).entries()) {
5315
+ flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
5316
+ }
5317
+ for (const [i, book] of asArray19(stack.books).entries()) {
5318
+ flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
5319
+ }
5320
+ return findings;
5321
+ }
4967
5322
 
4968
5323
  // src/validate-org-axis-red-lines.ts
4969
5324
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
@@ -5773,10 +6128,10 @@ function validateObjectReferences(stack) {
5773
6128
 
5774
6129
  // src/validate-nav-target-refs.ts
5775
6130
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
5776
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6131
+ var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5777
6132
  function asArray26(v) {
5778
- if (Array.isArray(v)) return v.filter(isRec12);
5779
- if (isRec12(v)) return Object.entries(v).map(([name, def]) => isRec12(def) ? { name, ...def } : { name });
6133
+ if (Array.isArray(v)) return v.filter(isRec14);
6134
+ if (isRec14(v)) return Object.entries(v).map(([name, def]) => isRec14(def) ? { name, ...def } : { name });
5780
6135
  return [];
5781
6136
  }
5782
6137
  function strName9(v) {
@@ -5798,7 +6153,7 @@ function namesOf(collection) {
5798
6153
  }
5799
6154
  function validateNavTargetRefs(stack) {
5800
6155
  const findings = [];
5801
- if (!isRec12(stack)) return findings;
6156
+ if (!isRec14(stack)) return findings;
5802
6157
  const apps = asArray26(stack.apps);
5803
6158
  if (apps.length === 0) return findings;
5804
6159
  const declared = /* @__PURE__ */ new Map();
@@ -5810,7 +6165,7 @@ function validateNavTargetRefs(stack) {
5810
6165
  const walk = (items, basePath) => {
5811
6166
  if (!Array.isArray(items)) return;
5812
6167
  for (const [ni, raw] of items.entries()) {
5813
- if (!isRec12(raw)) continue;
6168
+ if (!isRec14(raw)) continue;
5814
6169
  const nav = raw;
5815
6170
  const navPath = `${basePath}[${ni}]`;
5816
6171
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
@@ -5841,16 +6196,84 @@ function validateNavTargetRefs(stack) {
5841
6196
  return findings;
5842
6197
  }
5843
6198
 
6199
+ // src/validate-nav-object-servability.ts
6200
+ import { canServeApiOperation } from "@objectstack/spec/data";
6201
+ var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
6202
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6203
+ function asArray27(v) {
6204
+ if (Array.isArray(v)) return v.filter(isRec15);
6205
+ if (isRec15(v)) return Object.entries(v).map(([name, def]) => isRec15(def) ? { name, ...def } : { name });
6206
+ return [];
6207
+ }
6208
+ function strName10(v) {
6209
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6210
+ }
6211
+ var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
6212
+ function validateNavObjectServability(stack) {
6213
+ const findings = [];
6214
+ if (!isRec15(stack)) return findings;
6215
+ const apps = asArray27(stack.apps);
6216
+ if (apps.length === 0) return findings;
6217
+ const ownEnable = /* @__PURE__ */ new Map();
6218
+ const objects = asArray27(stack.objects);
6219
+ for (const [oi, obj] of objects.entries()) {
6220
+ const n = strName10(obj.name);
6221
+ if (!n) continue;
6222
+ ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
6223
+ }
6224
+ if (ownEnable.size === 0) return findings;
6225
+ for (const [ai, app] of apps.entries()) {
6226
+ const appName = strName10(app.name) ?? `#${ai}`;
6227
+ const walk = (items, basePath) => {
6228
+ if (!Array.isArray(items)) return;
6229
+ for (const [ni, raw] of items.entries()) {
6230
+ if (!isRec15(raw)) continue;
6231
+ const nav = raw;
6232
+ const navPath = `${basePath}[${ni}]`;
6233
+ if (nav.type === "object") {
6234
+ const target = strName10(nav.objectName);
6235
+ const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
6236
+ if (target && declared && !canServeApiOperation(declared.enable, "list")) {
6237
+ const enable = isRec15(declared.enable) ? declared.enable : {};
6238
+ const apiDisabled = enable.apiEnabled === false;
6239
+ 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(", ")})` : "");
6240
+ const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
6241
+ const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
6242
+ findings.push({
6243
+ severity: "error",
6244
+ rule: NAV_OBJECT_UNSERVABLE,
6245
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
6246
+ // The nav entry is where the dead row is authored; the `enable`
6247
+ // key that condemns it is named in the message, because the fix
6248
+ // may belong at either end.
6249
+ path: `${navPath}.objectName`,
6250
+ 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.`,
6251
+ 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.`
6252
+ });
6253
+ }
6254
+ }
6255
+ if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
6256
+ }
6257
+ };
6258
+ walk(app.navigation, `apps[${ai}].navigation`);
6259
+ for (const [ari, area] of asArray27(app.areas).entries()) {
6260
+ walk(area.items, `apps[${ai}].areas[${ari}].items`);
6261
+ walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
6262
+ }
6263
+ }
6264
+ return findings;
6265
+ }
6266
+
5844
6267
  // src/validate-action-name-refs.ts
5845
6268
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
5846
- function asArray27(v) {
6269
+ function asArray28(v) {
5847
6270
  if (Array.isArray(v)) return v;
5848
6271
  if (v && typeof v === "object") {
5849
6272
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5850
6273
  }
5851
6274
  return [];
5852
6275
  }
5853
- function strName10(v) {
6276
+ function strName11(v) {
5854
6277
  return typeof v === "string" && v.length > 0 ? v : void 0;
5855
6278
  }
5856
6279
  function strList(v) {
@@ -5887,14 +6310,14 @@ function suggest4(target, known) {
5887
6310
  }
5888
6311
  function collectActionNames(stack) {
5889
6312
  const names = /* @__PURE__ */ new Set();
5890
- for (const action of asArray27(stack.actions)) {
5891
- const n = strName10(action?.name);
6313
+ for (const action of asArray28(stack.actions)) {
6314
+ const n = strName11(action?.name);
5892
6315
  if (n) names.add(n);
5893
6316
  }
5894
- for (const obj of asArray27(stack.objects)) {
6317
+ for (const obj of asArray28(stack.objects)) {
5895
6318
  if (!obj || typeof obj !== "object") continue;
5896
- for (const action of asArray27(obj.actions)) {
5897
- const n = strName10(action?.name);
6319
+ for (const action of asArray28(obj.actions)) {
6320
+ const n = strName11(action?.name);
5898
6321
  if (n) names.add(n);
5899
6322
  }
5900
6323
  }
@@ -5937,7 +6360,7 @@ function validateActionNameRefs(stack) {
5937
6360
  if (!def || typeof def !== "object") continue;
5938
6361
  if (def.execution !== "aggregate") continue;
5939
6362
  if (def.actionDef !== void 0) continue;
5940
- const name = strName10(def.name);
6363
+ const name = strName11(def.name);
5941
6364
  if (!name) continue;
5942
6365
  check(
5943
6366
  name,
@@ -5948,11 +6371,11 @@ function validateActionNameRefs(stack) {
5948
6371
  );
5949
6372
  }
5950
6373
  };
5951
- const views = asArray27(stack.views);
6374
+ const views = asArray28(stack.views);
5952
6375
  for (let vi = 0; vi < views.length; vi++) {
5953
6376
  const view = views[vi];
5954
6377
  if (!view || typeof view !== "object") continue;
5955
- const viewName = strName10(view.name) ?? strName10(view.object) ?? `#${vi}`;
6378
+ const viewName = strName11(view.name) ?? strName11(view.object) ?? `#${vi}`;
5956
6379
  const owner = `view "${viewName}"`;
5957
6380
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
5958
6381
  const listViews = view.listViews;
@@ -5962,22 +6385,22 @@ function validateActionNameRefs(stack) {
5962
6385
  }
5963
6386
  }
5964
6387
  }
5965
- const objects = asArray27(stack.objects);
6388
+ const objects = asArray28(stack.objects);
5966
6389
  for (let oi = 0; oi < objects.length; oi++) {
5967
6390
  const obj = objects[oi];
5968
6391
  if (!obj || typeof obj !== "object") continue;
5969
6392
  const objListViews = obj.listViews;
5970
6393
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
5971
- const owner = `object "${strName10(obj.name) ?? `#${oi}`}"`;
6394
+ const owner = `object "${strName11(obj.name) ?? `#${oi}`}"`;
5972
6395
  for (const [key, lv] of Object.entries(objListViews)) {
5973
6396
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
5974
6397
  }
5975
6398
  }
5976
- const pages = asArray27(stack.pages);
6399
+ const pages = asArray28(stack.pages);
5977
6400
  for (let pi = 0; pi < pages.length; pi++) {
5978
6401
  const page = pages[pi];
5979
6402
  if (!page || typeof page !== "object") continue;
5980
- const pageName = strName10(page.name) ?? `#${pi}`;
6403
+ const pageName = strName11(page.name) ?? `#${pi}`;
5981
6404
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5982
6405
  const props = component.properties;
5983
6406
  if (!props || typeof props !== "object") continue;
@@ -5985,39 +6408,39 @@ function validateActionNameRefs(stack) {
5985
6408
  for (let ai = 0; ai < names.length; ai++) {
5986
6409
  check(
5987
6410
  names[ai],
5988
- `page "${pageName}" \xB7 component "${strName10(component.type) ?? "?"}"`,
6411
+ `page "${pageName}" \xB7 component "${strName11(component.type) ?? "?"}"`,
5989
6412
  `${path}.properties.actionNames[${ai}]`,
5990
6413
  "Quick-actions bar"
5991
6414
  );
5992
6415
  }
5993
6416
  }
5994
6417
  }
5995
- const apps = asArray27(stack.apps);
6418
+ const apps = asArray28(stack.apps);
5996
6419
  for (let ai = 0; ai < apps.length; ai++) {
5997
6420
  const app = apps[ai];
5998
6421
  if (!app || typeof app !== "object") continue;
5999
- const appName = strName10(app.name) ?? `#${ai}`;
6422
+ const appName = strName11(app.name) ?? `#${ai}`;
6000
6423
  const walkNav = (items, basePath) => {
6001
- const navItems = asArray27(items);
6424
+ const navItems = asArray28(items);
6002
6425
  for (let ni = 0; ni < navItems.length; ni++) {
6003
6426
  const nav = navItems[ni];
6004
6427
  if (!nav || typeof nav !== "object") continue;
6005
6428
  const navPath = `${basePath}[${ni}]`;
6006
6429
  const actionDef = nav.actionDef;
6007
- const actionName = strName10(actionDef?.actionName);
6430
+ const actionName = strName11(actionDef?.actionName);
6008
6431
  if (nav.type === "action" && actionName) {
6009
6432
  check(
6010
6433
  actionName,
6011
- `app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
6434
+ `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6012
6435
  `${navPath}.actionDef.actionName`,
6013
6436
  "Navigation action item"
6014
6437
  );
6015
6438
  }
6016
- const runAction = strName10(nav.runAction);
6439
+ const runAction = strName11(nav.runAction);
6017
6440
  if (nav.type === "object" && runAction) {
6018
6441
  check(
6019
6442
  runAction,
6020
- `app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
6443
+ `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6021
6444
  `${navPath}.runAction`,
6022
6445
  "Navigation deep-link auto-run"
6023
6446
  );
@@ -6026,7 +6449,7 @@ function validateActionNameRefs(stack) {
6026
6449
  }
6027
6450
  };
6028
6451
  walkNav(app.navigation, `apps[${ai}].navigation`);
6029
- const areas = asArray27(app.areas);
6452
+ const areas = asArray28(app.areas);
6030
6453
  for (let ri = 0; ri < areas.length; ri++) {
6031
6454
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
6032
6455
  }
@@ -6036,14 +6459,14 @@ function validateActionNameRefs(stack) {
6036
6459
 
6037
6460
  // src/validate-action-locations.ts
6038
6461
  var ACTION_NO_PLACEMENT = "action-no-placement";
6039
- function asArray28(v) {
6462
+ function asArray29(v) {
6040
6463
  if (Array.isArray(v)) return v;
6041
6464
  if (v && typeof v === "object") {
6042
6465
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6043
6466
  }
6044
6467
  return [];
6045
6468
  }
6046
- function strName11(v) {
6469
+ function strName12(v) {
6047
6470
  return typeof v === "string" && v.length > 0 ? v : void 0;
6048
6471
  }
6049
6472
  function strList2(v) {
@@ -6057,8 +6480,8 @@ function collectNamePlacedActions(stack) {
6057
6480
  for (const key of ["rowActions", "bulkActions"]) {
6058
6481
  for (const n of strList2(list3[key])) placed.add(n);
6059
6482
  }
6060
- for (const def of asArray28(list3.bulkActionDefs)) {
6061
- const n = strName11(def?.name);
6483
+ for (const def of asArray29(list3.bulkActionDefs)) {
6484
+ const n = strName12(def?.name);
6062
6485
  if (n) placed.add(n);
6063
6486
  }
6064
6487
  };
@@ -6066,12 +6489,12 @@ function collectNamePlacedActions(stack) {
6066
6489
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
6067
6490
  for (const lv of Object.values(listViews)) harvest(lv);
6068
6491
  };
6069
- for (const view of asArray28(stack.views)) {
6492
+ for (const view of asArray29(stack.views)) {
6070
6493
  if (!view || typeof view !== "object") continue;
6071
6494
  harvest(view.list);
6072
6495
  harvestListViews(view.listViews);
6073
6496
  }
6074
- for (const obj of asArray28(stack.objects)) {
6497
+ for (const obj of asArray29(stack.objects)) {
6075
6498
  if (!obj || typeof obj !== "object") continue;
6076
6499
  harvestListViews(obj.listViews);
6077
6500
  }
@@ -6084,7 +6507,7 @@ function validateActionLocations(stack) {
6084
6507
  const check = (action, path) => {
6085
6508
  if (!action || typeof action !== "object") return;
6086
6509
  if ("locations" in action) return;
6087
- const name = strName11(action.name);
6510
+ const name = strName12(action.name);
6088
6511
  if (!name) return;
6089
6512
  if (namePlaced.has(name)) return;
6090
6513
  findings.push({
@@ -6096,13 +6519,13 @@ function validateActionLocations(stack) {
6096
6519
  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."
6097
6520
  });
6098
6521
  };
6099
- const actions = asArray28(stack.actions);
6522
+ const actions = asArray29(stack.actions);
6100
6523
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
6101
- const objects = asArray28(stack.objects);
6524
+ const objects = asArray29(stack.objects);
6102
6525
  for (let oi = 0; oi < objects.length; oi++) {
6103
6526
  const obj = objects[oi];
6104
6527
  if (!obj || typeof obj !== "object") continue;
6105
- const own = asArray28(obj.actions);
6528
+ const own = asArray29(obj.actions);
6106
6529
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
6107
6530
  }
6108
6531
  return findings;
@@ -6113,13 +6536,13 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
6113
6536
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
6114
6537
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
6115
6538
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
6116
- function isRec13(v) {
6539
+ function isRec16(v) {
6117
6540
  return !!v && typeof v === "object" && !Array.isArray(v);
6118
6541
  }
6119
- function strName12(v) {
6542
+ function strName13(v) {
6120
6543
  return typeof v === "string" && v.length > 0 ? v : void 0;
6121
6544
  }
6122
- function asArray29(v) {
6545
+ function asArray30(v) {
6123
6546
  if (Array.isArray(v)) return v;
6124
6547
  if (v && typeof v === "object") {
6125
6548
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6130,23 +6553,36 @@ var PROPS_SCHEMAS = ComponentPropsMap;
6130
6553
  var DATASOURCE_SUPPLIED_PROP = "object";
6131
6554
  function suppliedByDataSource(issue, component) {
6132
6555
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
6133
- const dataSource = isRec13(component.dataSource) ? component.dataSource : void 0;
6134
- return strName12(dataSource?.object) !== void 0;
6556
+ const dataSource = isRec16(component.dataSource) ? component.dataSource : void 0;
6557
+ return strName13(dataSource?.object) !== void 0;
6558
+ }
6559
+ function unrecognizedKeysFromUnionArm(issue) {
6560
+ if (issue.code !== "invalid_union") return void 0;
6561
+ const arms = issue.errors;
6562
+ if (!arms || arms.length === 0) return void 0;
6563
+ let found;
6564
+ for (const arm of arms) {
6565
+ const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
6566
+ if (keyIssues.length === 0) continue;
6567
+ if (keyIssues.length !== arm.length || found) return void 0;
6568
+ found = keyIssues[0];
6569
+ }
6570
+ return found;
6135
6571
  }
6136
6572
  function validateComponentProps(stack) {
6137
6573
  const findings = [];
6138
- if (!isRec13(stack)) return findings;
6139
- const pages = asArray29(stack.pages);
6574
+ if (!isRec16(stack)) return findings;
6575
+ const pages = asArray30(stack.pages);
6140
6576
  for (let pi = 0; pi < pages.length; pi++) {
6141
6577
  const page = pages[pi];
6142
- if (!isRec13(page)) continue;
6143
- const pageName = strName12(page.name) ?? `#${pi}`;
6578
+ if (!isRec16(page)) continue;
6579
+ const pageName = strName13(page.name) ?? `#${pi}`;
6144
6580
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6145
- const type = strName12(component.type);
6581
+ const type = strName13(component.type);
6146
6582
  if (!type) continue;
6147
6583
  const schema = PROPS_SCHEMAS[type];
6148
6584
  if (!schema) continue;
6149
- const props = isRec13(component.properties) ? component.properties : void 0;
6585
+ const props = isRec16(component.properties) ? component.properties : void 0;
6150
6586
  if (!props) continue;
6151
6587
  const where = `page "${pageName}" \xB7 ${type}`;
6152
6588
  const base = `${path}.properties`;
@@ -6165,6 +6601,20 @@ function validateComponentProps(stack) {
6165
6601
  for (const issue of parsed.error?.issues ?? []) {
6166
6602
  if (suppliedByDataSource(issue, component)) continue;
6167
6603
  const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
6604
+ const armIssue = unrecognizedKeysFromUnionArm(issue);
6605
+ if (armIssue) {
6606
+ for (const key of armIssue.keys ?? []) {
6607
+ findings.push({
6608
+ severity: "warning",
6609
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
6610
+ where,
6611
+ path: `${at}.${key}`,
6612
+ message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
6613
+ hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
6614
+ });
6615
+ }
6616
+ continue;
6617
+ }
6168
6618
  if (issue.code === "unrecognized_keys") {
6169
6619
  for (const key of issue.keys ?? []) {
6170
6620
  findings.push({
@@ -6197,20 +6647,20 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
6197
6647
  var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
6198
6648
  var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
6199
6649
  var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
6200
- function asArray30(v) {
6650
+ function asArray31(v) {
6201
6651
  if (Array.isArray(v)) return v;
6202
6652
  if (v && typeof v === "object") {
6203
6653
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6204
6654
  }
6205
6655
  return [];
6206
6656
  }
6207
- function strName13(v) {
6657
+ function strName14(v) {
6208
6658
  return typeof v === "string" && v.length > 0 ? v : void 0;
6209
6659
  }
6210
6660
  function strList3(v) {
6211
6661
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6212
6662
  }
6213
- function isRec14(v) {
6663
+ function isRec17(v) {
6214
6664
  return !!v && typeof v === "object" && !Array.isArray(v);
6215
6665
  }
6216
6666
  function distance4(a, b) {
@@ -6248,17 +6698,17 @@ function list2(names) {
6248
6698
  }
6249
6699
  function indexDatasets(stack) {
6250
6700
  const out = /* @__PURE__ */ new Map();
6251
- for (const ds of asArray30(stack.datasets)) {
6252
- const name = strName13(ds.name);
6701
+ for (const ds of asArray31(stack.datasets)) {
6702
+ const name = strName14(ds.name);
6253
6703
  if (!name) continue;
6254
6704
  const dimensions = /* @__PURE__ */ new Set();
6255
- for (const d of asArray30(ds.dimensions)) {
6256
- const n = strName13(d.name);
6705
+ for (const d of asArray31(ds.dimensions)) {
6706
+ const n = strName14(d.name);
6257
6707
  if (n) dimensions.add(n);
6258
6708
  }
6259
6709
  const measures = /* @__PURE__ */ new Set();
6260
- for (const m of asArray30(ds.measures)) {
6261
- const n = strName13(m.name);
6710
+ for (const m of asArray31(ds.measures)) {
6711
+ const n = strName14(m.name);
6262
6712
  if (n) measures.add(n);
6263
6713
  }
6264
6714
  out.set(name, { dimensions, measures });
@@ -6336,29 +6786,29 @@ function validateChartBindings(stack) {
6336
6786
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
6337
6787
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
6338
6788
  };
6339
- const reports = asArray30(stack.reports);
6789
+ const reports = asArray31(stack.reports);
6340
6790
  for (let ri = 0; ri < reports.length; ri++) {
6341
6791
  const report = reports[ri];
6342
- if (!isRec14(report)) continue;
6343
- const reportName = strName13(report.name) ?? `#${ri}`;
6792
+ if (!isRec17(report)) continue;
6793
+ const reportName = strName14(report.name) ?? `#${ri}`;
6344
6794
  const checkReportChart = (chart, dataset, values, where, path) => {
6345
- if (!isRec14(chart)) return;
6795
+ if (!isRec17(chart)) return;
6346
6796
  check({
6347
6797
  dataset,
6348
6798
  // `values` is the report's measure SELECTION, not a chart ref; feeding
6349
6799
  // it in lets the yAxis "declared but not selected" check work without
6350
6800
  // reporting the selection itself twice.
6351
6801
  values: { names: values, path: `${path}.values` },
6352
- xAxis: strName13(chart.xAxis) ? { name: strName13(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
6353
- yAxis: strName13(chart.yAxis) ? { name: strName13(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
6354
- series: asArray30(chart.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
6802
+ xAxis: strName14(chart.xAxis) ? { name: strName14(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
6803
+ yAxis: strName14(chart.yAxis) ? { name: strName14(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
6804
+ series: asArray31(chart.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
6355
6805
  where,
6356
6806
  path: `${path}.chart`
6357
6807
  });
6358
6808
  };
6359
6809
  checkReportChart(
6360
6810
  report.chart,
6361
- strName13(report.dataset),
6811
+ strName14(report.dataset),
6362
6812
  strList3(report.values),
6363
6813
  `report "${reportName}" \xB7 chart`,
6364
6814
  `reports[${ri}]`
@@ -6366,45 +6816,45 @@ function validateChartBindings(stack) {
6366
6816
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
6367
6817
  for (let bi = 0; bi < blocks.length; bi++) {
6368
6818
  const block = blocks[bi];
6369
- if (!isRec14(block)) continue;
6819
+ if (!isRec17(block)) continue;
6370
6820
  checkReportChart(
6371
6821
  block.chart,
6372
- strName13(block.dataset),
6822
+ strName14(block.dataset),
6373
6823
  strList3(block.values),
6374
- `report "${reportName}" \xB7 block "${strName13(block.name) ?? `#${bi}`}" chart`,
6824
+ `report "${reportName}" \xB7 block "${strName14(block.name) ?? `#${bi}`}" chart`,
6375
6825
  `reports[${ri}].blocks[${bi}]`
6376
6826
  );
6377
6827
  }
6378
6828
  }
6379
6829
  const checkListChart = (container, where, path) => {
6380
- if (!isRec14(container)) return;
6830
+ if (!isRec17(container)) return;
6381
6831
  const chart = container.chart;
6382
- if (!isRec14(chart)) return;
6832
+ if (!isRec17(chart)) return;
6383
6833
  check({
6384
- dataset: strName13(chart.dataset),
6834
+ dataset: strName14(chart.dataset),
6385
6835
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
6386
6836
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
6387
6837
  where,
6388
6838
  path: `${path}.chart`
6389
6839
  });
6390
6840
  };
6391
- const views = asArray30(stack.views);
6841
+ const views = asArray31(stack.views);
6392
6842
  for (let vi = 0; vi < views.length; vi++) {
6393
6843
  const view = views[vi];
6394
- if (!isRec14(view)) continue;
6395
- const viewName = strName13(view.name) ?? strName13(view.objectName) ?? `#${vi}`;
6844
+ if (!isRec17(view)) continue;
6845
+ const viewName = strName14(view.name) ?? strName14(view.objectName) ?? `#${vi}`;
6396
6846
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
6397
- if (isRec14(view.listViews)) {
6847
+ if (isRec17(view.listViews)) {
6398
6848
  for (const [key, lv] of Object.entries(view.listViews)) {
6399
6849
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
6400
6850
  }
6401
6851
  }
6402
6852
  }
6403
- const objects = asArray30(stack.objects);
6853
+ const objects = asArray31(stack.objects);
6404
6854
  for (let oi = 0; oi < objects.length; oi++) {
6405
6855
  const obj = objects[oi];
6406
- if (!isRec14(obj) || !isRec14(obj.listViews)) continue;
6407
- const objName = strName13(obj.name) ?? `#${oi}`;
6856
+ if (!isRec17(obj) || !isRec17(obj.listViews)) continue;
6857
+ const objName = strName14(obj.name) ?? `#${oi}`;
6408
6858
  for (const [key, lv] of Object.entries(obj.listViews)) {
6409
6859
  checkListChart(
6410
6860
  lv,
@@ -6413,22 +6863,22 @@ function validateChartBindings(stack) {
6413
6863
  );
6414
6864
  }
6415
6865
  }
6416
- const pages = asArray30(stack.pages);
6866
+ const pages = asArray31(stack.pages);
6417
6867
  for (let pi = 0; pi < pages.length; pi++) {
6418
6868
  const page = pages[pi];
6419
- if (!isRec14(page)) continue;
6420
- const pageName = strName13(page.name) ?? `#${pi}`;
6869
+ if (!isRec17(page)) continue;
6870
+ const pageName = strName14(page.name) ?? `#${pi}`;
6421
6871
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6422
- const props = isRec14(component.properties) ? component.properties : void 0;
6423
- if (!props || !strName13(props.dataset)) continue;
6424
- const axisRefs = asArray30(props.yAxis).map((a, ai) => ({ name: strName13(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
6425
- const seriesRefs = asArray30(props.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
6872
+ const props = isRec17(component.properties) ? component.properties : void 0;
6873
+ if (!props || !strName14(props.dataset)) continue;
6874
+ const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName14(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
6875
+ const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
6426
6876
  check({
6427
- dataset: strName13(props.dataset),
6877
+ dataset: strName14(props.dataset),
6428
6878
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
6429
6879
  values: { names: strList3(props.values), path: `${path}.properties.values` },
6430
6880
  series: [...axisRefs, ...seriesRefs],
6431
- where: `page "${pageName}" \xB7 ${strName13(component.type) ?? "chart"}`,
6881
+ where: `page "${pageName}" \xB7 ${strName14(component.type) ?? "chart"}`,
6432
6882
  path: `${path}.properties`
6433
6883
  });
6434
6884
  }
@@ -6441,11 +6891,11 @@ import { createRequire as createRequire4 } from "module";
6441
6891
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6442
6892
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6443
6893
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6444
- var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6445
- function asArray31(v) {
6446
- if (Array.isArray(v)) return v.filter(isRec15);
6447
- if (isRec15(v)) {
6448
- return Object.entries(v).filter(([, def]) => isRec15(def)).map(([name, def]) => ({ name, ...def }));
6894
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6895
+ function asArray32(v) {
6896
+ if (Array.isArray(v)) return v.filter(isRec18);
6897
+ if (isRec18(v)) {
6898
+ return Object.entries(v).filter(([, def]) => isRec18(def)).map(([name, def]) => ({ name, ...def }));
6449
6899
  }
6450
6900
  return [];
6451
6901
  }
@@ -6462,7 +6912,7 @@ function loadAjv() {
6462
6912
  `@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.`
6463
6913
  );
6464
6914
  }
6465
- const ctor = isRec15(mod) && "default" in mod ? mod.default : mod;
6915
+ const ctor = isRec18(mod) && "default" in mod ? mod.default : mod;
6466
6916
  cachedAjv = ctor;
6467
6917
  return ctor;
6468
6918
  }
@@ -6477,7 +6927,7 @@ function loadAddFormats() {
6477
6927
  `@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.`
6478
6928
  );
6479
6929
  }
6480
- const plugin = isRec15(mod) && "default" in mod ? mod.default : mod;
6930
+ const plugin = isRec18(mod) && "default" in mod ? mod.default : mod;
6481
6931
  cachedAddFormats = plugin;
6482
6932
  return plugin;
6483
6933
  }
@@ -6507,17 +6957,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6507
6957
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6508
6958
  for (const branch of ["then", "otherwise"]) {
6509
6959
  const nested = rule[branch];
6510
- if (isRec15(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6960
+ if (isRec18(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6511
6961
  }
6512
6962
  return out;
6513
6963
  }
6514
6964
  function walkObjectValidationRules(stack) {
6515
6965
  const walked = [];
6516
- if (!isRec15(stack)) return walked;
6517
- for (const obj of asArray31(stack.objects)) {
6966
+ if (!isRec18(stack)) return walked;
6967
+ for (const obj of asArray32(stack.objects)) {
6518
6968
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6519
6969
  const validations = obj.validations;
6520
- for (const authored of asArray31(validations)) {
6970
+ for (const authored of asArray32(validations)) {
6521
6971
  for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
6522
6972
  walked.push({
6523
6973
  rule,
@@ -6548,7 +6998,7 @@ function validateRuleCompilability(stack) {
6548
6998
  });
6549
6999
  }
6550
7000
  }
6551
- if (rule.type === "json_schema" && isRec15(rule.schema)) {
7001
+ if (rule.type === "json_schema" && isRec18(rule.schema)) {
6552
7002
  try {
6553
7003
  createRuntimeAjv().compile(rule.schema);
6554
7004
  } catch (err) {
@@ -6568,7 +7018,7 @@ function validateRuleCompilability(stack) {
6568
7018
 
6569
7019
  // src/validate-rule-schema-formats.ts
6570
7020
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
6571
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7021
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6572
7022
  var SUBSCHEMA_KEYS = [
6573
7023
  "additionalItems",
6574
7024
  "additionalProperties",
@@ -6592,7 +7042,7 @@ var SUBSCHEMA_MAP_KEYS = [
6592
7042
  var MAX_SCHEMA_WALK_DEPTH = 32;
6593
7043
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
6594
7044
  function collectFormatUses(schema, pointer, out, depth) {
6595
- if (!isRec16(schema)) return;
7045
+ if (!isRec19(schema)) return;
6596
7046
  if (typeof schema.format === "string") {
6597
7047
  out.push({ pointer: `${pointer}/format`, name: schema.format });
6598
7048
  }
@@ -6611,7 +7061,7 @@ function collectFormatUses(schema, pointer, out, depth) {
6611
7061
  }
6612
7062
  for (const key of SUBSCHEMA_MAP_KEYS) {
6613
7063
  const value = schema[key];
6614
- if (!isRec16(value)) continue;
7064
+ if (!isRec19(value)) continue;
6615
7065
  for (const [name, entry] of Object.entries(value)) {
6616
7066
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
6617
7067
  }
@@ -6619,13 +7069,13 @@ function collectFormatUses(schema, pointer, out, depth) {
6619
7069
  const items = schema.items;
6620
7070
  if (Array.isArray(items)) {
6621
7071
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
6622
- } else if (isRec16(items)) {
7072
+ } else if (isRec19(items)) {
6623
7073
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
6624
7074
  }
6625
7075
  const dependencies = schema.dependencies;
6626
- if (isRec16(dependencies)) {
7076
+ if (isRec19(dependencies)) {
6627
7077
  for (const [name, entry] of Object.entries(dependencies)) {
6628
- if (!isRec16(entry)) continue;
7078
+ if (!isRec19(entry)) continue;
6629
7079
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
6630
7080
  }
6631
7081
  }
@@ -6664,7 +7114,7 @@ function validateRuleSchemaFormats(stack) {
6664
7114
  const findings = [];
6665
7115
  const pending = [];
6666
7116
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
6667
- if (rule.type !== "json_schema" || !isRec16(rule.schema)) continue;
7117
+ if (rule.type !== "json_schema" || !isRec19(rule.schema)) continue;
6668
7118
  const uses = [];
6669
7119
  collectFormatUses(rule.schema, "", uses, 0);
6670
7120
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -6692,7 +7142,7 @@ function validateRuleSchemaFormats(stack) {
6692
7142
  import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
6693
7143
 
6694
7144
  // src/build-access-matrix.ts
6695
- function asArray32(v) {
7145
+ function asArray33(v) {
6696
7146
  if (Array.isArray(v)) return v;
6697
7147
  if (v && typeof v === "object") {
6698
7148
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6703,13 +7153,13 @@ function buildAccessMatrix(stack) {
6703
7153
  const entries = [];
6704
7154
  if (!stack || typeof stack !== "object") return { version: 1, entries };
6705
7155
  const owdByObject = /* @__PURE__ */ new Map();
6706
- for (const obj of asArray32(stack.objects)) {
7156
+ for (const obj of asArray33(stack.objects)) {
6707
7157
  const name = typeof obj.name === "string" ? obj.name : "";
6708
7158
  if (!name) continue;
6709
7159
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
6710
7160
  if (typeof owd === "string") owdByObject.set(name, owd);
6711
7161
  }
6712
- for (const ps of asArray32(stack.permissions)) {
7162
+ for (const ps of asArray33(stack.permissions)) {
6713
7163
  const psName = typeof ps.name === "string" ? ps.name : "";
6714
7164
  if (!psName) continue;
6715
7165
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -6782,34 +7232,34 @@ function diffAccessMatrix(before, after) {
6782
7232
 
6783
7233
  // src/validate-nav-access.ts
6784
7234
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
6785
- function asArray33(v) {
7235
+ function asArray34(v) {
6786
7236
  if (Array.isArray(v)) return v;
6787
7237
  if (v && typeof v === "object") {
6788
7238
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6789
7239
  }
6790
7240
  return [];
6791
7241
  }
6792
- function strName14(v) {
7242
+ function strName15(v) {
6793
7243
  return typeof v === "string" && v.length > 0 ? v : void 0;
6794
7244
  }
6795
7245
  function collectNavExposures(stack) {
6796
7246
  const out = [];
6797
- const apps = asArray33(stack.apps);
7247
+ const apps = asArray34(stack.apps);
6798
7248
  for (let ai = 0; ai < apps.length; ai++) {
6799
7249
  const app = apps[ai];
6800
7250
  if (!app || typeof app !== "object") continue;
6801
- const appName = strName14(app.name) ?? `#${ai}`;
7251
+ const appName = strName15(app.name) ?? `#${ai}`;
6802
7252
  const walk = (items, basePath) => {
6803
- const navItems = asArray33(items);
7253
+ const navItems = asArray34(items);
6804
7254
  for (let ni = 0; ni < navItems.length; ni++) {
6805
7255
  const nav = navItems[ni];
6806
7256
  if (!nav || typeof nav !== "object") continue;
6807
7257
  const navPath = `${basePath}[${ni}]`;
6808
- const objectName = strName14(nav.objectName);
7258
+ const objectName = strName15(nav.objectName);
6809
7259
  if (nav.type === "object" && objectName) {
6810
7260
  out.push({
6811
7261
  objectName,
6812
- where: `app "${appName}" \xB7 nav "${strName14(nav.id) ?? `#${ni}`}"`,
7262
+ where: `app "${appName}" \xB7 nav "${strName15(nav.id) ?? `#${ni}`}"`,
6813
7263
  path: `${navPath}.objectName`
6814
7264
  });
6815
7265
  }
@@ -6817,7 +7267,7 @@ function collectNavExposures(stack) {
6817
7267
  }
6818
7268
  };
6819
7269
  walk(app.navigation, `apps[${ai}].navigation`);
6820
- const areas = asArray33(app.areas);
7270
+ const areas = asArray34(app.areas);
6821
7271
  for (let ri = 0; ri < areas.length; ri++) {
6822
7272
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
6823
7273
  }
@@ -6827,13 +7277,13 @@ function collectNavExposures(stack) {
6827
7277
  function validateNavAccess(stack) {
6828
7278
  const findings = [];
6829
7279
  if (!stack || typeof stack !== "object") return findings;
6830
- const permissionSets = asArray33(stack.permissions);
7280
+ const permissionSets = asArray34(stack.permissions);
6831
7281
  if (permissionSets.length === 0) return findings;
6832
7282
  const exposures = collectNavExposures(stack);
6833
7283
  if (exposures.length === 0) return findings;
6834
7284
  const ownObjects = /* @__PURE__ */ new Set();
6835
- for (const obj of asArray33(stack.objects)) {
6836
- const n = strName14(obj.name);
7285
+ for (const obj of asArray34(stack.objects)) {
7286
+ const n = strName15(obj.name);
6837
7287
  if (n) ownObjects.add(n);
6838
7288
  }
6839
7289
  const readable = /* @__PURE__ */ new Set();
@@ -6866,15 +7316,15 @@ import { expandViewContainer } from "@objectstack/spec";
6866
7316
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
6867
7317
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
6868
7318
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
6869
- function isRec17(v) {
7319
+ function isRec20(v) {
6870
7320
  return !!v && typeof v === "object" && !Array.isArray(v);
6871
7321
  }
6872
- function asArray34(v) {
7322
+ function asArray35(v) {
6873
7323
  if (Array.isArray(v)) return v;
6874
- if (isRec17(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec17(def) ? def : {} }));
7324
+ if (isRec20(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec20(def) ? def : {} }));
6875
7325
  return [];
6876
7326
  }
6877
- function strName15(v) {
7327
+ function strName16(v) {
6878
7328
  return typeof v === "string" && v.length > 0 ? v : void 0;
6879
7329
  }
6880
7330
  function distance5(a, b) {
@@ -6934,34 +7384,34 @@ function collectViewRecord(view, factsFor) {
6934
7384
  };
6935
7385
  const addSections = (container, binding) => {
6936
7386
  if (!binding) return;
6937
- for (const section of asArray34(container.sections)) {
6938
- const sectionName = strName15(section.name);
7387
+ for (const section of asArray35(container.sections)) {
7388
+ const sectionName = strName16(section.name);
6939
7389
  if (sectionName) factsFor(binding).sections.add(sectionName);
6940
7390
  }
6941
7391
  };
6942
- const listBinding = isRec17(view.list) ? bindingOf(view.list) : void 0;
6943
- if (isRec17(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
6944
- addView(recordObject ?? listBinding, strName15(view.name));
7392
+ const listBinding = isRec20(view.list) ? bindingOf(view.list) : void 0;
7393
+ if (isRec20(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
7394
+ addView(recordObject ?? listBinding, strName16(view.name));
6945
7395
  const named = namedViewKeys(view);
6946
7396
  for (const family of ["listViews", "formViews"]) {
6947
7397
  const container = view[family];
6948
- if (!isRec17(container)) continue;
7398
+ if (!isRec20(container)) continue;
6949
7399
  const registryKeys = family === "listViews" ? named.list : named.form;
6950
7400
  let at = 0;
6951
7401
  for (const sub of Object.values(container)) {
6952
7402
  if (!sub || typeof sub !== "object") continue;
6953
7403
  const registryKey = registryKeys[at++];
6954
- if (!isRec17(sub)) continue;
7404
+ if (!isRec20(sub)) continue;
6955
7405
  const binding = bindingOf(sub) ?? listBinding;
6956
7406
  addView(binding, registryKey);
6957
7407
  addSections(sub, binding);
6958
7408
  }
6959
7409
  }
6960
- if (isRec17(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
7410
+ if (isRec20(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
6961
7411
  addSections(view, recordObject ?? listBinding);
6962
7412
  }
6963
7413
  function defaultListViewKey(object, container) {
6964
- if (!object || !isRec17(container.list)) return void 0;
7414
+ if (!object || !isRec20(container.list)) return void 0;
6965
7415
  const item = expandViewContainer(object, container).find(
6966
7416
  (i) => i.viewKind === "list" && i.isDefault
6967
7417
  );
@@ -6973,7 +7423,7 @@ function namedViewKeys(container) {
6973
7423
  const object = "probe";
6974
7424
  const prefix = `${object}.`;
6975
7425
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
6976
- const countEntries = (v) => isRec17(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
7426
+ const countEntries = (v) => isRec20(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
6977
7427
  const listCount = countEntries(container.listViews);
6978
7428
  const formCount = countEntries(container.formViews);
6979
7429
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -6991,14 +7441,14 @@ function readOptions(field) {
6991
7441
  values.add(opt);
6992
7442
  continue;
6993
7443
  }
6994
- if (!isRec17(opt)) continue;
6995
- const value = strName15(opt.value);
7444
+ if (!isRec20(opt)) continue;
7445
+ const value = strName16(opt.value);
6996
7446
  if (!value) continue;
6997
7447
  values.add(value);
6998
- const label2 = strName15(opt.label);
7448
+ const label2 = strName16(opt.label);
6999
7449
  if (label2) byLabel.set(label2.toLowerCase(), value);
7000
7450
  }
7001
- } else if (isRec17(raw)) {
7451
+ } else if (isRec20(raw)) {
7002
7452
  for (const [value, label2] of Object.entries(raw)) {
7003
7453
  values.add(value);
7004
7454
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -7018,48 +7468,48 @@ function buildUniverse(stack) {
7018
7468
  }
7019
7469
  return facts;
7020
7470
  };
7021
- for (const obj of asArray34(stack.objects)) {
7022
- const objectName = strName15(obj.name);
7471
+ for (const obj of asArray35(stack.objects)) {
7472
+ const objectName = strName16(obj.name);
7023
7473
  if (!objectName) continue;
7024
7474
  const facts = factsFor(objectName);
7025
- for (const field of asArray34(obj.fields)) {
7026
- const fieldName = strName15(field.name);
7475
+ for (const field of asArray35(obj.fields)) {
7476
+ const fieldName = strName16(field.name);
7027
7477
  if (fieldName) facts.fields.set(fieldName, field);
7028
7478
  }
7029
- for (const action of asArray34(obj.actions)) {
7030
- const actionName = strName15(action.name);
7479
+ for (const action of asArray35(obj.actions)) {
7480
+ const actionName = strName16(action.name);
7031
7481
  if (actionName) facts.actions.set(actionName, action);
7032
7482
  }
7033
- for (const view of asArray34(obj.views)) {
7034
- collectViewRecord({ ...view, object: strName15(view.object) ?? objectName }, factsFor);
7483
+ for (const view of asArray35(obj.views)) {
7484
+ collectViewRecord({ ...view, object: strName16(view.object) ?? objectName }, factsFor);
7035
7485
  }
7036
7486
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
7037
- for (const group of asArray34(obj.fieldGroups)) {
7038
- const key = strName15(group.key) ?? strName15(group.name);
7487
+ for (const group of asArray35(obj.fieldGroups)) {
7488
+ const key = strName16(group.key) ?? strName16(group.name);
7039
7489
  if (key) facts.sections.add(key);
7040
7490
  }
7041
7491
  }
7042
- for (const view of asArray34(stack.views)) {
7492
+ for (const view of asArray35(stack.views)) {
7043
7493
  collectViewRecord(view, factsFor);
7044
7494
  }
7045
- const pages = asArray34(stack.pages);
7495
+ const pages = asArray35(stack.pages);
7046
7496
  for (let pi = 0; pi < pages.length; pi++) {
7047
7497
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
7048
7498
  if (!walked.objectName) continue;
7049
- const props = isRec17(walked.component.properties) ? walked.component.properties : void 0;
7499
+ const props = isRec20(walked.component.properties) ? walked.component.properties : void 0;
7050
7500
  if (!props) continue;
7051
- for (const section of asArray34(props.sections)) {
7052
- const sectionName = strName15(section.name);
7501
+ for (const section of asArray35(props.sections)) {
7502
+ const sectionName = strName16(section.name);
7053
7503
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
7054
7504
  }
7055
7505
  }
7056
7506
  }
7057
7507
  const globalActions = /* @__PURE__ */ new Map();
7058
7508
  const actionOwners = /* @__PURE__ */ new Map();
7059
- for (const action of asArray34(stack.actions)) {
7060
- const actionName = strName15(action.name);
7509
+ for (const action of asArray35(stack.actions)) {
7510
+ const actionName = strName16(action.name);
7061
7511
  if (!actionName) continue;
7062
- const owner = strName15(action.objectName) ?? strName15(action.object);
7512
+ const owner = strName16(action.objectName) ?? strName16(action.object);
7063
7513
  if (owner) {
7064
7514
  factsFor(owner).actions.set(actionName, action);
7065
7515
  actionOwners.set(actionName, owner);
@@ -7073,41 +7523,41 @@ function buildUniverse(stack) {
7073
7523
  }
7074
7524
  }
7075
7525
  const apps = /* @__PURE__ */ new Map();
7076
- for (const app of asArray34(stack.apps)) {
7077
- const appName = strName15(app.name);
7526
+ for (const app of asArray35(stack.apps)) {
7527
+ const appName = strName16(app.name);
7078
7528
  if (!appName) continue;
7079
7529
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
7080
7530
  const walkNav = (items) => {
7081
- for (const item of asArray34(items)) {
7082
- const id = strName15(item.id);
7531
+ for (const item of asArray35(items)) {
7532
+ const id = strName16(item.id);
7083
7533
  if (id) navIds.add(id);
7084
7534
  if (item.children) walkNav(item.children);
7085
7535
  }
7086
7536
  };
7087
7537
  walkNav(app.navigation);
7088
- for (const area of asArray34(app.areas)) {
7089
- const areaId = strName15(area.id);
7538
+ for (const area of asArray35(app.areas)) {
7539
+ const areaId = strName16(area.id);
7090
7540
  if (areaId) navIds.add(areaId);
7091
7541
  walkNav(area.navigation);
7092
7542
  }
7093
7543
  apps.set(appName, navIds);
7094
7544
  }
7095
7545
  const dashboards = /* @__PURE__ */ new Map();
7096
- for (const dash of asArray34(stack.dashboards)) {
7097
- const dashName = strName15(dash.name);
7546
+ for (const dash of asArray35(stack.dashboards)) {
7547
+ const dashName = strName16(dash.name);
7098
7548
  if (!dashName) continue;
7099
7549
  const widgets = /* @__PURE__ */ new Set();
7100
- for (const widget of asArray34(dash.widgets)) {
7101
- const id = strName15(widget.id) ?? strName15(widget.name);
7550
+ for (const widget of asArray35(dash.widgets)) {
7551
+ const id = strName16(widget.id) ?? strName16(widget.name);
7102
7552
  if (id) widgets.add(id);
7103
7553
  }
7104
7554
  const actions = /* @__PURE__ */ new Set();
7105
7555
  const headerActions = [
7106
- ...asArray34(isRec17(dash.header) ? dash.header.actions : void 0),
7107
- ...asArray34(dash.actions)
7556
+ ...asArray35(isRec20(dash.header) ? dash.header.actions : void 0),
7557
+ ...asArray35(dash.actions)
7108
7558
  ];
7109
7559
  for (const action of headerActions) {
7110
- const key = strName15(action.actionUrl) ?? strName15(action.url) ?? strName15(action.name);
7560
+ const key = strName16(action.actionUrl) ?? strName16(action.url) ?? strName16(action.name);
7111
7561
  if (key) actions.add(key);
7112
7562
  }
7113
7563
  dashboards.set(dashName, { widgets, actions });
@@ -7119,7 +7569,7 @@ function localePath(bundleIndex, locale) {
7119
7569
  }
7120
7570
  function validateTranslationReferences(stack) {
7121
7571
  const findings = [];
7122
- if (!isRec17(stack)) return findings;
7572
+ if (!isRec20(stack)) return findings;
7123
7573
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7124
7574
  if (bundles.length === 0) return findings;
7125
7575
  const universe = buildUniverse(stack);
@@ -7128,13 +7578,13 @@ function validateTranslationReferences(stack) {
7128
7578
  };
7129
7579
  for (let bi = 0; bi < bundles.length; bi++) {
7130
7580
  const bundle = bundles[bi];
7131
- if (!isRec17(bundle)) continue;
7581
+ if (!isRec20(bundle)) continue;
7132
7582
  for (const [locale, rawData] of Object.entries(bundle)) {
7133
- if (!isRec17(rawData)) continue;
7583
+ if (!isRec20(rawData)) continue;
7134
7584
  const base = localePath(bi, locale);
7135
7585
  const inLocale = `locale "${locale}"`;
7136
7586
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
7137
- if (!isRec17(rawNode)) continue;
7587
+ if (!isRec20(rawNode)) continue;
7138
7588
  const objPath = `${base}.objects.${objectName}`;
7139
7589
  const facts = universe.objects.get(objectName);
7140
7590
  if (!facts) {
@@ -7160,7 +7610,7 @@ function validateTranslationReferences(stack) {
7160
7610
  );
7161
7611
  continue;
7162
7612
  }
7163
- if (!isRec17(rawField)) continue;
7613
+ if (!isRec20(rawField)) continue;
7164
7614
  checkOptionKeys(findings, {
7165
7615
  optionMap: rawField.options,
7166
7616
  field,
@@ -7242,7 +7692,7 @@ function validateTranslationReferences(stack) {
7242
7692
  );
7243
7693
  continue;
7244
7694
  }
7245
- if (!isRec17(rawApp)) continue;
7695
+ if (!isRec20(rawApp)) continue;
7246
7696
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
7247
7697
  if (navIds.has(navId)) continue;
7248
7698
  orphan(
@@ -7265,7 +7715,7 @@ function validateTranslationReferences(stack) {
7265
7715
  );
7266
7716
  continue;
7267
7717
  }
7268
- if (!isRec17(rawDash)) continue;
7718
+ if (!isRec20(rawDash)) continue;
7269
7719
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
7270
7720
  if (dash.widgets.has(widgetId)) continue;
7271
7721
  orphan(
@@ -7290,7 +7740,7 @@ function validateTranslationReferences(stack) {
7290
7740
  return findings;
7291
7741
  }
7292
7742
  function asRecord(v) {
7293
- return isRec17(v) ? v : {};
7743
+ return isRec20(v) ? v : {};
7294
7744
  }
7295
7745
  function checkOptionKeys(findings, ctx) {
7296
7746
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -7302,7 +7752,7 @@ function checkOptionKeys(findings, ctx) {
7302
7752
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
7303
7753
  where: ctx.where,
7304
7754
  path: ctx.path,
7305
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName15(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
7755
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName16(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
7306
7756
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
7307
7757
  });
7308
7758
  return;
@@ -7321,11 +7771,11 @@ function checkOptionKeys(findings, ctx) {
7321
7771
  }
7322
7772
  }
7323
7773
  function checkActionParams(findings, ctx) {
7324
- const rawParams = Object.keys(asRecord(isRec17(ctx.rawAction) ? ctx.rawAction.params : void 0));
7774
+ const rawParams = Object.keys(asRecord(isRec20(ctx.rawAction) ? ctx.rawAction.params : void 0));
7325
7775
  if (rawParams.length === 0) return;
7326
7776
  const declared = /* @__PURE__ */ new Set();
7327
- for (const param of asArray34(ctx.action.params)) {
7328
- const name = strName15(param.name) ?? strName15(param.field);
7777
+ for (const param of asArray35(ctx.action.params)) {
7778
+ const name = strName16(param.name) ?? strName16(param.field);
7329
7779
  if (name) declared.add(name);
7330
7780
  }
7331
7781
  for (const paramName of rawParams) {
@@ -7343,14 +7793,14 @@ function checkActionParams(findings, ctx) {
7343
7793
 
7344
7794
  // src/validate-translatable-sections.ts
7345
7795
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
7346
- function isRec18(v) {
7796
+ function isRec21(v) {
7347
7797
  return !!v && typeof v === "object" && !Array.isArray(v);
7348
7798
  }
7349
- function strName16(v) {
7799
+ function strName17(v) {
7350
7800
  return typeof v === "string" && v.length > 0 ? v : void 0;
7351
7801
  }
7352
7802
  function viewLabel(view) {
7353
- const name = strName16(view.name);
7803
+ const name = strName17(view.name);
7354
7804
  return name ? `view "${name}"` : "";
7355
7805
  }
7356
7806
  function joinWhere(...parts) {
@@ -7358,7 +7808,7 @@ function joinWhere(...parts) {
7358
7808
  }
7359
7809
  function collectViewSites(view, basePath, label2, sites) {
7360
7810
  const recordObject = viewObjectName(view);
7361
- const listBinding = isRec18(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
7811
+ const listBinding = isRec21(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
7362
7812
  for (const site of viewContainerSites(view, basePath)) {
7363
7813
  sites.push({
7364
7814
  path: `${site.path}.sections`,
@@ -7372,11 +7822,11 @@ function translatedObjectNames(stack) {
7372
7822
  const out = /* @__PURE__ */ new Set();
7373
7823
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7374
7824
  for (const bundle of bundles) {
7375
- if (!isRec18(bundle)) continue;
7825
+ if (!isRec21(bundle)) continue;
7376
7826
  for (const data of Object.values(bundle)) {
7377
- if (!isRec18(data) || !isRec18(data.objects)) continue;
7827
+ if (!isRec21(data) || !isRec21(data.objects)) continue;
7378
7828
  for (const [objectName, node] of Object.entries(data.objects)) {
7379
- if (isRec18(node)) out.add(objectName);
7829
+ if (isRec21(node)) out.add(objectName);
7380
7830
  }
7381
7831
  }
7382
7832
  }
@@ -7388,22 +7838,22 @@ function suggestedName(label2) {
7388
7838
  }
7389
7839
  function validateTranslatableSections(stack) {
7390
7840
  const findings = [];
7391
- if (!isRec18(stack)) return findings;
7841
+ if (!isRec21(stack)) return findings;
7392
7842
  const translated = translatedObjectNames(stack);
7393
7843
  if (translated.size === 0) return findings;
7394
7844
  const sites = [];
7395
7845
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
7396
- const objectName = strName16(obj.name);
7846
+ const objectName = strName17(obj.name);
7397
7847
  if (!objectName) continue;
7398
7848
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
7399
7849
  collectViewSites(
7400
- { ...view, object: strName16(view.object) ?? objectName },
7850
+ { ...view, object: strName17(view.object) ?? objectName },
7401
7851
  path,
7402
7852
  viewLabel(view),
7403
7853
  sites
7404
7854
  );
7405
7855
  }
7406
- if (isRec18(obj.listViews)) {
7856
+ if (isRec21(obj.listViews)) {
7407
7857
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
7408
7858
  }
7409
7859
  }
@@ -7411,13 +7861,13 @@ function validateTranslatableSections(stack) {
7411
7861
  collectViewSites(view, path, viewLabel(view), sites);
7412
7862
  }
7413
7863
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
7414
- const pageName = strName16(page.name);
7864
+ const pageName = strName17(page.name);
7415
7865
  const pageLabel = pageName ? `page "${pageName}"` : "";
7416
7866
  for (const walked of walkPageComponents(page, pagePath)) {
7417
7867
  if (!walked.objectName) continue;
7418
- const props = isRec18(walked.component.properties) ? walked.component.properties : void 0;
7868
+ const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
7419
7869
  if (!props) continue;
7420
- const type = strName16(walked.component.type) ?? "component";
7870
+ const type = strName17(walked.component.type) ?? "component";
7421
7871
  sites.push({
7422
7872
  path: `${walked.path}.properties.sections`,
7423
7873
  surface: joinWhere(pageLabel, type),
@@ -7432,9 +7882,9 @@ function validateTranslatableSections(stack) {
7432
7882
  if (!Array.isArray(site.sections)) continue;
7433
7883
  for (let i = 0; i < site.sections.length; i++) {
7434
7884
  const section = site.sections[i];
7435
- if (!isRec18(section)) continue;
7436
- if (strName16(section.name)) continue;
7437
- const heading = strName16(section.label);
7885
+ if (!isRec21(section)) continue;
7886
+ if (strName17(section.name)) continue;
7887
+ const heading = strName17(section.label);
7438
7888
  if (!heading) continue;
7439
7889
  const slug = suggestedName(heading);
7440
7890
  findings.push({
@@ -7452,14 +7902,14 @@ function validateTranslatableSections(stack) {
7452
7902
 
7453
7903
  // src/validate-ai-surface-affinity.ts
7454
7904
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
7455
- function asArray35(v) {
7905
+ function asArray36(v) {
7456
7906
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
7457
7907
  if (v && typeof v === "object") {
7458
7908
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7459
7909
  }
7460
7910
  return [];
7461
7911
  }
7462
- function strName17(v) {
7912
+ function strName18(v) {
7463
7913
  return typeof v === "string" && v.length > 0 ? v : void 0;
7464
7914
  }
7465
7915
  function surfaceOf(v) {
@@ -7469,18 +7919,18 @@ function validateAiSurfaceAffinity(stack) {
7469
7919
  const findings = [];
7470
7920
  if (!stack || typeof stack !== "object") return findings;
7471
7921
  const skillsByName = /* @__PURE__ */ new Map();
7472
- for (const skill of asArray35(stack.skills)) {
7473
- const n = strName17(skill.name);
7922
+ for (const skill of asArray36(stack.skills)) {
7923
+ const n = strName18(skill.name);
7474
7924
  if (n) skillsByName.set(n, skill);
7475
7925
  }
7476
- const agents = asArray35(stack.agents);
7926
+ const agents = asArray36(stack.agents);
7477
7927
  for (let ai = 0; ai < agents.length; ai++) {
7478
7928
  const agent = agents[ai];
7479
- const agentName = strName17(agent.name) ?? `#${ai}`;
7929
+ const agentName = strName18(agent.name) ?? `#${ai}`;
7480
7930
  const agentSurface = surfaceOf(agent.surface);
7481
7931
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
7482
7932
  for (let si = 0; si < skillRefs.length; si++) {
7483
- const ref = strName17(skillRefs[si]);
7933
+ const ref = strName18(skillRefs[si]);
7484
7934
  if (!ref) continue;
7485
7935
  const skill = skillsByName.get(ref);
7486
7936
  if (!skill) continue;
@@ -7502,14 +7952,14 @@ function validateAiSurfaceAffinity(stack) {
7502
7952
  // src/validate-ai-tool-references.ts
7503
7953
  import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
7504
7954
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
7505
- function asArray36(v) {
7955
+ function asArray37(v) {
7506
7956
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
7507
7957
  if (v && typeof v === "object") {
7508
7958
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7509
7959
  }
7510
7960
  return [];
7511
7961
  }
7512
- function strName18(v) {
7962
+ function strName19(v) {
7513
7963
  return typeof v === "string" && v.length > 0 ? v : void 0;
7514
7964
  }
7515
7965
  function distance6(a, b) {
@@ -7550,26 +8000,26 @@ function materialisesAsTool(action) {
7550
8000
  if (!ai || typeof ai !== "object") return false;
7551
8001
  const aiRec = ai;
7552
8002
  if (aiRec.exposed !== true) return false;
7553
- if (!strName18(aiRec.description)) return false;
7554
- const type = strName18(action.type);
8003
+ if (!strName19(aiRec.description)) return false;
8004
+ const type = strName19(action.type);
7555
8005
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
7556
8006
  if (type === "script") return Boolean(action.target || action.body);
7557
8007
  return Boolean(action.target);
7558
8008
  }
7559
8009
  function collectToolUniverse(stack) {
7560
8010
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
7561
- for (const tool of asArray36(stack.tools)) {
7562
- const n = strName18(tool.name);
8011
+ for (const tool of asArray37(stack.tools)) {
8012
+ const n = strName19(tool.name);
7563
8013
  if (n) universe.add(n);
7564
8014
  }
7565
8015
  const addActionFamily = (actions) => {
7566
- for (const action of asArray36(actions)) {
7567
- const n = strName18(action.name);
8016
+ for (const action of asArray37(actions)) {
8017
+ const n = strName19(action.name);
7568
8018
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
7569
8019
  }
7570
8020
  };
7571
8021
  addActionFamily(stack.actions);
7572
- for (const obj of asArray36(stack.objects)) {
8022
+ for (const obj of asArray37(stack.objects)) {
7573
8023
  addActionFamily(obj.actions);
7574
8024
  }
7575
8025
  return universe;
@@ -7577,13 +8027,13 @@ function collectToolUniverse(stack) {
7577
8027
  function collectUnexposedActionNames(stack) {
7578
8028
  const names = /* @__PURE__ */ new Set();
7579
8029
  const scan = (actions) => {
7580
- for (const action of asArray36(actions)) {
7581
- const n = strName18(action.name);
8030
+ for (const action of asArray37(actions)) {
8031
+ const n = strName19(action.name);
7582
8032
  if (n && !materialisesAsTool(action)) names.add(n);
7583
8033
  }
7584
8034
  };
7585
8035
  scan(stack.actions);
7586
- for (const obj of asArray36(stack.objects)) scan(obj.actions);
8036
+ for (const obj of asArray37(stack.objects)) scan(obj.actions);
7587
8037
  return names;
7588
8038
  }
7589
8039
  function validateAiToolReferences(stack) {
@@ -7601,13 +8051,13 @@ function validateAiToolReferences(stack) {
7601
8051
  }
7602
8052
  return universe.has(ref);
7603
8053
  };
7604
- const skills = asArray36(stack.skills);
8054
+ const skills = asArray37(stack.skills);
7605
8055
  for (let si = 0; si < skills.length; si++) {
7606
8056
  const skill = skills[si];
7607
- const skillName = strName18(skill.name) ?? `#${si}`;
8057
+ const skillName = strName19(skill.name) ?? `#${si}`;
7608
8058
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
7609
8059
  for (let ti = 0; ti < refs.length; ti++) {
7610
- const ref = strName18(refs[ti]);
8060
+ const ref = strName19(refs[ti]);
7611
8061
  if (!ref || resolves(ref)) continue;
7612
8062
  const isPattern = ref.endsWith("*");
7613
8063
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -7627,24 +8077,24 @@ function validateAiToolReferences(stack) {
7627
8077
  // src/validate-ai-agent-authoring.ts
7628
8078
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
7629
8079
  var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
7630
- function asArray37(v) {
8080
+ function asArray38(v) {
7631
8081
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
7632
8082
  if (v && typeof v === "object") {
7633
8083
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7634
8084
  }
7635
8085
  return [];
7636
8086
  }
7637
- function strName19(v) {
8087
+ function strName20(v) {
7638
8088
  return typeof v === "string" && v.length > 0 ? v : void 0;
7639
8089
  }
7640
8090
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
7641
8091
  function validateAiAgentAuthoring(stack) {
7642
8092
  const findings = [];
7643
8093
  if (!stack || typeof stack !== "object") return findings;
7644
- const agents = asArray37(stack.agents);
8094
+ const agents = asArray38(stack.agents);
7645
8095
  for (let ai = 0; ai < agents.length; ai++) {
7646
8096
  const agent = agents[ai];
7647
- const name = strName19(agent.name) ?? `#${ai}`;
8097
+ const name = strName20(agent.name) ?? `#${ai}`;
7648
8098
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
7649
8099
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
7650
8100
  findings.push({
@@ -7657,12 +8107,12 @@ function validateAiAgentAuthoring(stack) {
7657
8107
  });
7658
8108
  }
7659
8109
  const roster = [...PLATFORM_AGENT_NAMES].join(", ");
7660
- const apps = asArray37(stack.apps);
8110
+ const apps = asArray38(stack.apps);
7661
8111
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
7662
8112
  const app = apps[appIdx];
7663
- const defaultAgent = strName19(app.defaultAgent);
8113
+ const defaultAgent = strName20(app.defaultAgent);
7664
8114
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
7665
- const appName = strName19(app.name) ?? `#${appIdx}`;
8115
+ const appName = strName20(app.name) ?? `#${appIdx}`;
7666
8116
  findings.push({
7667
8117
  severity: "warning",
7668
8118
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -7764,24 +8214,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
7764
8214
  "owner",
7765
8215
  "record_type"
7766
8216
  ]);
7767
- var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7768
- function asArray38(v) {
7769
- if (Array.isArray(v)) return v.filter((x) => isRec19(x));
7770
- if (isRec19(v)) {
8217
+ var isRec22 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8218
+ function asArray39(v) {
8219
+ if (Array.isArray(v)) return v.filter((x) => isRec22(x));
8220
+ if (isRec22(v)) {
7771
8221
  return Object.entries(v).map(([name, def]) => ({
7772
8222
  name,
7773
- ...isRec19(def) ? def : {}
8223
+ ...isRec22(def) ? def : {}
7774
8224
  }));
7775
8225
  }
7776
8226
  return [];
7777
8227
  }
7778
8228
  function indexObjectFields2(stack) {
7779
8229
  const out = /* @__PURE__ */ new Map();
7780
- for (const obj of asArray38(stack.objects)) {
8230
+ for (const obj of asArray39(stack.objects)) {
7781
8231
  const name = typeof obj.name === "string" ? obj.name : void 0;
7782
8232
  if (!name) continue;
7783
8233
  const names = /* @__PURE__ */ new Set();
7784
- for (const f of asArray38(obj.fields)) {
8234
+ for (const f of asArray39(obj.fields)) {
7785
8235
  if (typeof f.name === "string" && f.name) names.add(f.name);
7786
8236
  }
7787
8237
  out.set(name, names);
@@ -7911,12 +8361,12 @@ ${source}
7911
8361
  }
7912
8362
  function validateHookBodyWrites(stack) {
7913
8363
  const findings = [];
7914
- const hooks = asArray38(stack.hooks);
8364
+ const hooks = asArray39(stack.hooks);
7915
8365
  if (hooks.length === 0) return findings;
7916
8366
  let objectFields = null;
7917
8367
  hooks.forEach((hook, hookIndex) => {
7918
8368
  const body = hook.body;
7919
- if (!isRec19(body) || body.language !== "js") return;
8369
+ if (!isRec22(body) || body.language !== "js") return;
7920
8370
  const source = body.source;
7921
8371
  if (typeof source !== "string" || source.trim() === "") return;
7922
8372
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -7997,13 +8447,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
7997
8447
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
7998
8448
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
7999
8449
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
8000
- var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8001
- function asArray39(v) {
8002
- if (Array.isArray(v)) return v.filter((x) => isRec20(x));
8003
- if (isRec20(v)) {
8450
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8451
+ function asArray40(v) {
8452
+ if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8453
+ if (isRec23(v)) {
8004
8454
  return Object.entries(v).map(([name, def]) => ({
8005
8455
  name,
8006
- ...isRec20(def) ? def : {}
8456
+ ...isRec23(def) ? def : {}
8007
8457
  }));
8008
8458
  }
8009
8459
  return [];
@@ -8017,11 +8467,11 @@ function collectActionBodies(stack) {
8017
8467
  const sites = [];
8018
8468
  const seen = /* @__PURE__ */ new Set();
8019
8469
  const collect = (actions, pathPrefix, parentObject) => {
8020
- asArray39(actions).forEach((action, index) => {
8470
+ asArray40(actions).forEach((action, index) => {
8021
8471
  const type = typeof action.type === "string" ? action.type : "script";
8022
8472
  if (type !== "script") return;
8023
8473
  const body = action.body;
8024
- if (!isRec20(body) || body.language !== "js") return;
8474
+ if (!isRec23(body) || body.language !== "js") return;
8025
8475
  const source = body.source;
8026
8476
  if (typeof source !== "string" || source.trim() === "") return;
8027
8477
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -8032,7 +8482,7 @@ function collectActionBodies(stack) {
8032
8482
  });
8033
8483
  };
8034
8484
  collect(stack.actions, "actions");
8035
- asArray39(stack.objects).forEach((obj, objIndex) => {
8485
+ asArray40(stack.objects).forEach((obj, objIndex) => {
8036
8486
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
8037
8487
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
8038
8488
  });
@@ -8040,7 +8490,7 @@ function collectActionBodies(stack) {
8040
8490
  }
8041
8491
  function validateActionBodyWrites(stack) {
8042
8492
  const findings = [];
8043
- if (!isRec20(stack)) return findings;
8493
+ if (!isRec23(stack)) return findings;
8044
8494
  const sites = collectActionBodies(stack);
8045
8495
  if (sites.length === 0) return findings;
8046
8496
  let objectFields = null;
@@ -8099,13 +8549,13 @@ import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSu
8099
8549
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
8100
8550
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
8101
8551
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
8102
- var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8103
- function asArray40(v) {
8104
- if (Array.isArray(v)) return v.filter((x) => isRec21(x));
8105
- if (isRec21(v)) {
8552
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8553
+ function asArray41(v) {
8554
+ if (Array.isArray(v)) return v.filter((x) => isRec24(x));
8555
+ if (isRec24(v)) {
8106
8556
  return Object.entries(v).map(([name, def]) => ({
8107
8557
  name,
8108
- ...isRec21(def) ? def : {}
8558
+ ...isRec24(def) ? def : {}
8109
8559
  }));
8110
8560
  }
8111
8561
  return [];
@@ -8118,8 +8568,8 @@ function readLiteralObjectName2(config) {
8118
8568
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
8119
8569
  function validateFlowNodeWrites(stack) {
8120
8570
  const findings = [];
8121
- if (!isRec21(stack)) return findings;
8122
- const flows = asArray40(stack.flows);
8571
+ if (!isRec24(stack)) return findings;
8572
+ const flows = asArray41(stack.flows);
8123
8573
  if (flows.length === 0) return findings;
8124
8574
  let objectFields = null;
8125
8575
  flows.forEach((flow, flowIndex) => {
@@ -8127,10 +8577,10 @@ function validateFlowNodeWrites(stack) {
8127
8577
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
8128
8578
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
8129
8579
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
8130
- const config = isRec21(node.config) ? node.config : void 0;
8580
+ const config = isRec24(node.config) ? node.config : void 0;
8131
8581
  if (!config) return;
8132
8582
  const fields = config.fields;
8133
- if (!isRec21(fields)) return;
8583
+ if (!isRec24(fields)) return;
8134
8584
  const written = Object.keys(fields);
8135
8585
  if (written.length === 0) return;
8136
8586
  const objectName = readLiteralObjectName2(config);
@@ -8176,6 +8626,15 @@ var REFERENCE_INTEGRITY_RULES = [
8176
8626
  // `action` is deliberately absent (validateActionNameRefs owns it) and so is
8177
8627
  // `component` (an unregistered ref renders a named diagnostic, not silence).
8178
8628
  { name: "validateNavTargetRefs", run: validateNavTargetRefs },
8629
+ // [#7912] The THIRD question about a nav entry, after "does the target
8630
+ // resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
8631
+ // destination serve at all? An object's own `enable` block can make its list
8632
+ // answer 404/405 for every persona, and no gate authorable on the entry
8633
+ // expresses that — which is how #7544's dead row survived review for a year.
8634
+ // The server now prunes such an entry from the `/meta` payload; the
8635
+ // maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
8636
+ // prune is never silent to the author who wrote the row.
8637
+ { name: "validateNavObjectServability", run: validateNavObjectServability },
8179
8638
  { name: "validateTranslationReferences", run: validateTranslationReferences },
8180
8639
  // The same family from the other end (#5417). Its sibling above asks "does
8181
8640
  // this bundle key resolve?"; this one asks "is there a key at all?" — a form
@@ -8276,7 +8735,7 @@ import {
8276
8735
  collectFlowGraphs as collectFlowGraphs2
8277
8736
  } from "@objectstack/spec/automation";
8278
8737
  import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
8279
- function asArray41(v) {
8738
+ function asArray42(v) {
8280
8739
  if (Array.isArray(v)) return v;
8281
8740
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
8282
8741
  return [];
@@ -8660,7 +9119,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
8660
9119
  }
8661
9120
  function lintFlowPatterns(stack) {
8662
9121
  const findings = [];
8663
- for (const flow of asArray41(stack.flows)) {
9122
+ for (const flow of asArray42(stack.flows)) {
8664
9123
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
8665
9124
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
8666
9125
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -8757,7 +9216,7 @@ import { dirname, join } from "path";
8757
9216
  import { existsSync, readFileSync } from "fs";
8758
9217
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
8759
9218
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
8760
- function asArray42(v) {
9219
+ function asArray43(v) {
8761
9220
  if (Array.isArray(v)) return v;
8762
9221
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
8763
9222
  return [];
@@ -8894,11 +9353,11 @@ function lintLivenessProperties(stack) {
8894
9353
  const findings = [];
8895
9354
  const objectWarn = loadWarnMap(dir, "object");
8896
9355
  const fieldWarn = loadWarnMap(dir, "field");
8897
- for (const obj of asArray42(stack.objects)) {
9356
+ for (const obj of asArray43(stack.objects)) {
8898
9357
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8899
9358
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
8900
9359
  if (fieldWarn.size > 0) {
8901
- for (const field of asArray42(obj.fields)) {
9360
+ for (const field of asArray43(obj.fields)) {
8902
9361
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
8903
9362
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
8904
9363
  }
@@ -8907,7 +9366,7 @@ function lintLivenessProperties(stack) {
8907
9366
  for (const { type, key } of TYPE_COLLECTIONS) {
8908
9367
  const warnMap = loadWarnMap(dir, type);
8909
9368
  if (warnMap.size === 0) continue;
8910
- for (const item of asArray42(stack[key])) {
9369
+ for (const item of asArray43(stack[key])) {
8911
9370
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
8912
9371
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
8913
9372
  }
@@ -8921,7 +9380,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
8921
9380
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
8922
9381
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
8923
9382
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
8924
- function asArray43(v) {
9383
+ function asArray44(v) {
8925
9384
  if (Array.isArray(v)) return v;
8926
9385
  if (v && typeof v === "object") {
8927
9386
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -8930,9 +9389,9 @@ function asArray43(v) {
8930
9389
  }
8931
9390
  function lintAutonumberFormats(stack) {
8932
9391
  const findings = [];
8933
- for (const obj of asArray43(stack.objects)) {
9392
+ for (const obj of asArray44(stack.objects)) {
8934
9393
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8935
- const fields = asArray43(obj.fields);
9394
+ const fields = asArray44(obj.fields);
8936
9395
  const fieldMeta = /* @__PURE__ */ new Map();
8937
9396
  for (const f of fields) {
8938
9397
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -8998,7 +9457,7 @@ function lintAutonumberFormats(stack) {
8998
9457
 
8999
9458
  // src/lint-view-refs.ts
9000
9459
  import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
9001
- function asArray44(v) {
9460
+ function asArray45(v) {
9002
9461
  if (Array.isArray(v)) return v;
9003
9462
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
9004
9463
  return [];
@@ -9026,7 +9485,7 @@ function lintViewRefs(stack) {
9026
9485
  s.add(kind);
9027
9486
  };
9028
9487
  const containers = [];
9029
- for (const v of asArray44(stack.views)) {
9488
+ for (const v of asArray45(stack.views)) {
9030
9489
  if (v.viewKind) {
9031
9490
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
9032
9491
  continue;
@@ -9035,7 +9494,7 @@ function lintViewRefs(stack) {
9035
9494
  const object = viewContainerObjectName(v);
9036
9495
  if (object) containers.push({ object, container: v });
9037
9496
  }
9038
- for (const obj of asArray44(stack.objects)) {
9497
+ for (const obj of asArray45(stack.objects)) {
9039
9498
  const object = typeof obj.name === "string" ? obj.name : void 0;
9040
9499
  if (!object) continue;
9041
9500
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -9089,11 +9548,11 @@ function lintViewRefs(stack) {
9089
9548
  });
9090
9549
  }
9091
9550
  };
9092
- for (const obj of asArray44(stack.objects)) {
9551
+ for (const obj of asArray45(stack.objects)) {
9093
9552
  const object = typeof obj.name === "string" ? obj.name : void 0;
9094
- for (const action of asArray44(obj.actions)) checkAction(action, object);
9553
+ for (const action of asArray45(obj.actions)) checkAction(action, object);
9095
9554
  }
9096
- for (const action of asArray44(stack.actions)) checkAction(action);
9555
+ for (const action of asArray45(stack.actions)) checkAction(action);
9097
9556
  return findings;
9098
9557
  }
9099
9558
 
@@ -9397,7 +9856,6 @@ var CLI_ONLY = ["cli"];
9397
9856
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9398
9857
  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.";
9399
9858
  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.";
9400
- 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.";
9401
9859
  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.";
9402
9860
  var EXPRESSION_INVALID = "expression-invalid";
9403
9861
  var AUTHORING_RULES = [
@@ -9463,6 +9921,31 @@ var AUTHORING_RULES = [
9463
9921
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9464
9922
  run: (stack) => validateFunctionalCompleteness(stack)
9465
9923
  },
9924
+ // [#7521, via cloud#1225] A managed object advertising a generic write verb
9925
+ // in `enable.apiMethods` that its own resolved affordances refuse. Every key
9926
+ // is one we know and each is individually valid, so #4001's unknown-key
9927
+ // rejection and the Zod parse both pass it; the contradiction is only visible
9928
+ // when the two keys are read TOGETHER, which nothing did at authoring time.
9929
+ //
9930
+ // `gating` because the declaration is already false when it ships: objectql's
9931
+ // registry strips the verb at registration, so the metadata advertises an API
9932
+ // the product does not serve. That strip has been correct and silent — a
9933
+ // `console.warn` on every control-plane boot that went unread for the life of
9934
+ // a real divergence (`sys_environment`/`sys_package`). This entry is the
9935
+ // ruling's "close it where the author is"; boot stays warn-and-strip.
9936
+ //
9937
+ // Pre-parse: the predicate reads only authored keys, and the finding must
9938
+ // survive an unrelated schema error elsewhere in the stack.
9939
+ {
9940
+ name: "validateManagedApiMethods",
9941
+ tier: "gating",
9942
+ input: "normalized",
9943
+ commands: ALL,
9944
+ source: "packages/lint/src/validate-managed-api-methods.ts",
9945
+ surfaces: CLI_ONLY,
9946
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9947
+ run: (stack) => validateManagedApiMethods(stack)
9948
+ },
9466
9949
  // A view container in `views: []` that registers zero views: nothing appears
9467
9950
  // in the Console, and the schema step cannot tell it from an intentionally
9468
9951
  // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
@@ -9842,14 +10325,47 @@ var AUTHORING_RULES = [
9842
10325
  // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9843
10326
  // has worked since #3760. The promotion follows the #5762 precedent: a family
9844
10327
  // that gains an `error` finding moves its registry tier in the same edit.
10328
+ //
10329
+ // ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
10330
+ //
10331
+ // This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
10332
+ // in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
10333
+ // `advisories` channel landed (PR #7435). Before that move a `view` written
10334
+ // through Studio / REST `/meta` / MCP — the only door most tenants have, and
10335
+ // the door AI authors use — was judged by NONE of the family's rule ids (six
10336
+ // at the time of the move; seven since #7659 added
10337
+ // `predicate-rhs-path-shaped` inside the second entry).
10338
+ //
10339
+ // They move together on purpose, and the two entries carry one comment because
10340
+ // they are one wall: #7214's implementer wired its own rule here alone and then
10341
+ // REVERTED it, because a `view` refused for an unresolvable predicate PATH
10342
+ // while a predicate that does not parse at all walks through the same door is
10343
+ // less predictable than refusing neither. A half-wired wall is worse than an
10344
+ // unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
10345
+ // directly: every id on this surface is gated at the runtime door, or none is.
10346
+ //
10347
+ // The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
10348
+ // and re-measuring it at move time found it false: both rule functions read
10349
+ // `stack.views` and `stack.pages` and NO other collection — never `objects` —
10350
+ // so the per-write snapshot the gate builds is not partial for them, it is
10351
+ // complete. (`pages` is simply absent on a `view` write, so the page half
10352
+ // contributes zero findings to both differential passes rather than inventing
10353
+ // any.) The reason was not describing this rule; it was the default a rule got
10354
+ // when nobody measured, which is the #4409/#4463 defect one layer in.
10355
+ //
10356
+ // Runtime input tier: the gate hands the rules the body as persisted, without
10357
+ // `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
10358
+ // door. That costs the family nothing — `validateVisibilityPredicates` reads
10359
+ // `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
10360
+ // so a caller handing it a raw authored object still gets a verdict.
9845
10361
  {
9846
10362
  name: "validateVisibilityPredicates",
9847
10363
  tier: "gating",
9848
10364
  input: "normalized",
9849
10365
  commands: ALL,
9850
10366
  source: "packages/lint/src/validate-visibility-predicates.ts",
9851
- surfaces: CLI_ONLY,
9852
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10367
+ surfaces: CLI_AND_RUNTIME,
10368
+ runtimeTypes: ["view"],
9853
10369
  run: (stack) => validateVisibilityPredicates(stack)
9854
10370
  },
9855
10371
  // #7010 — the same predicate surface, one question further in. The three
@@ -9866,14 +10382,28 @@ var AUTHORING_RULES = [
9866
10382
  // object's addressable path set is NOT closed (lookup traversal, system
9867
10383
  // columns, formula outputs), and an `error` gate over an open set generates
9868
10384
  // false build errors. See the rule's module note.
10385
+ //
10386
+ // #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
10387
+ // resolution question at all: the metadata-admin renderer resolves paths only
10388
+ // on the LEFT of `==` / `!=` and hands the right side to its literal parser,
10389
+ // so `data.a == data.b` resolves both sides cleanly, passes the two rules
10390
+ // above, and still compares against the string "data.b" — a constant verdict.
10391
+ // It carries `error` on a dotted chain (no reading under which it worked) and
10392
+ // `warning` on a bare word (`status == active` compares as the text today, so
10393
+ // refusing it would fail a build over metadata that renders correctly). The
10394
+ // per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
10395
+ // since #3760; the entry's `gating` tier is unchanged because it already was.
9869
10396
  {
9870
10397
  name: "validatePredicatePathRefs",
9871
10398
  tier: "gating",
9872
10399
  input: "normalized",
9873
10400
  commands: ALL,
9874
10401
  source: "packages/lint/src/validate-predicate-path-refs.ts",
9875
- surfaces: CLI_ONLY,
9876
- surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
10402
+ // The second half of the #7220 family move — see the block above the
10403
+ // `validateVisibilityPredicates` entry. This is the rule whose solo wiring
10404
+ // was reverted; it is wired now because its siblings are.
10405
+ surfaces: CLI_AND_RUNTIME,
10406
+ runtimeTypes: ["view"],
9877
10407
  run: (stack) => validatePredicatePathRefs(stack)
9878
10408
  },
9879
10409
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
@@ -10033,16 +10563,110 @@ var AUTHORING_RULES = [
10033
10563
  // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
10034
10564
  // binding gate, vocabulary freeze), moving the failure from a runtime deny to
10035
10565
  // an author-time fix-it. Per ADR-0049 this is not advisory security.
10566
+ //
10567
+ // [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
10568
+ // was false in both halves — it read: "Already gated at this surface by a
10569
+ // DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
10570
+ // `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
10571
+ // on every runtime write. Running the linter here as well would double-report
10572
+ // one refusal in two vocabularies."
10573
+ //
10574
+ // - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
10575
+ // `externalSharingModel` through a local `OWD_WIDTH`, and never touches
10576
+ // `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
10577
+ // block carries it covers ONE — `security-external-wider-than-internal`
10578
+ // (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
10579
+ // corresponds to no lint rule at all, so it is not coverage in the other
10580
+ // direction either. Twelve rules were enforced at no runtime door while
10581
+ // this field said they were.
10582
+ // - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
10583
+ // `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
10584
+ // `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
10585
+ // both refuse by THROWING. The first to fire ends the write, so an author
10586
+ // sees one refusal, never two. The stated cost of moving was imaginary; the
10587
+ // reason it has not moved is the measured one below.
10588
+ //
10589
+ // The move IS taken now — the #7891 programme's three slices, in order:
10590
+ //
10591
+ // - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
10592
+ // the isolation proof that the differential cancels every finding this
10593
+ // function derives from the sibling collections.
10594
+ // - #8309: the snapshot repair. The gate used to carry `objects` and
10595
+ // nothing else, so the three cross-collection rules judged a universe
10596
+ // missing the collection they compare against (measured: 38 phantom
10597
+ // `security-master-detail-ungranted` per-write vs 4 whole-stack,
10598
+ // PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
10599
+ // BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
10600
+ // - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
10601
+ // `object` measured DIRTY on that tree and was escalated, not forced.
10602
+ // - #8310 slice 2 (this state): `object` crosses under the maintainer
10603
+ // ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
10604
+ // authored OWD is REQUIRED at the runtime object door — an object
10605
+ // publish with no authored `sharingModel` is refused with the 422 lint
10606
+ // envelope (`security-owd-unset`); absence is not a decision. The ~16
10607
+ // objectql/rest suite files that relied on OWD-less publishes were
10608
+ // repaired honestly (fixtures author their posture), and
10609
+ // `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
10610
+ // answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
10611
+ // ADR-0094-seam 403 doors answer for what passes lint. The same ruling
10612
+ // retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
10613
+ // of this door (R1 env-tighten-only STAYS — no lint rule covers it);
10614
+ // see `object-posture-gate.ts` and the ADR-0094 amendment.
10615
+ //
10616
+ // `security-role-word` is NOT in this entry any more — that is what the
10617
+ // `validateSecurityRoleWord` entry below records. It judges six collections
10618
+ // (objects, fields, actions, permission sets, positions, apps — plus books),
10619
+ // and `positions`/`apps` are neither carried by the per-write snapshot nor
10620
+ // mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
10621
+ // function that still contained it would have enforced ONE rule id for a
10622
+ // strict subset of its collections: a door where a permission set named
10623
+ // `role_manager` is refused and a position named `sales_role` walks through
10624
+ // — the #7220 failure this table refuses to build, in either direction. The
10625
+ // rule therefore stays behind WHOLE (#8310's explicit call), as its own
10626
+ // entry.
10627
+ //
10628
+ // This entry remains the rest of the D7 block (12 rule ids) as ONE
10629
+ // registration, not a per-rule split: the baseline/candidate differential is
10630
+ // what keeps a write of one declared type from leaking the other rules'
10631
+ // whole-stack findings — every finding derived from a sibling collection is
10632
+ // produced byte-identically in both passes and cancels in the diff. Only
10633
+ // findings the written item itself adds are attributed to the write.
10036
10634
  {
10037
10635
  name: "validateSecurityPosture",
10038
10636
  tier: "gating",
10039
10637
  input: "parsed",
10040
10638
  commands: ALL,
10041
10639
  source: "packages/lint/src/validate-security-posture.ts",
10042
- surfaces: CLI_ONLY,
10043
- 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.",
10640
+ surfaces: CLI_AND_RUNTIME,
10641
+ runtimeTypes: ["seed", "permission", "book", "object"],
10044
10642
  run: (stack) => validateSecurityPosture(stack)
10045
10643
  },
10644
+ // [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
10645
+ // `validateSecurityPosture` the day the rest of that block crossed the
10646
+ // runtime wall — so that it could stay behind WHOLE rather than cross for
10647
+ // three of the six collections it judges (#7220: one rule id must sit on ONE
10648
+ // side of the wall). The split is a surface boundary, not taste: the rule's
10649
+ // verdict and findings are byte-identical to before on every CLI command
10650
+ // (both entries run on all three), and the runtime door does not run it for
10651
+ // ANY type.
10652
+ //
10653
+ // The road to crossing is concrete and short, recorded here so the next
10654
+ // seat prices it correctly: carry `positions`/`apps` in
10655
+ // `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
10656
+ // `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
10657
+ // are real), then declare `runtimeTypes: ['object', 'permission', 'book',
10658
+ // 'position', 'app']` on THIS entry — all six collections in one edit, the
10659
+ // #7220 discipline satisfied.
10660
+ {
10661
+ name: "validateSecurityRoleWord",
10662
+ tier: "gating",
10663
+ input: "parsed",
10664
+ commands: ALL,
10665
+ source: "packages/lint/src/validate-security-posture.ts",
10666
+ surfaces: CLI_ONLY,
10667
+ 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.",
10668
+ run: (stack) => validateSecurityRoleWord(stack)
10669
+ },
10046
10670
  // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
10047
10671
  // sharing rule that walks it builds a second permission hierarchy (the
10048
10672
  // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
@@ -10093,7 +10717,7 @@ var AUTHORING_RULES = [
10093
10717
  commands: ALL,
10094
10718
  source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
10095
10719
  surfaces: CLI_ONLY,
10096
- 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.",
10720
+ 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.",
10097
10721
  run: (stack) => validateRlsPredicateEnforceability(stack)
10098
10722
  },
10099
10723
  // #4762 — the same "declared but enforces nothing" question, for the two
@@ -10169,8 +10793,34 @@ var TYPE_TO_STACK_KEY = {
10169
10793
  dashboard: "dashboards",
10170
10794
  agent: "agents",
10171
10795
  hook: "hooks",
10172
- seed: "seeds"
10796
+ // [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
10797
+ // holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
10798
+ // — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
10799
+ // mapping onto one either.
10800
+ //
10801
+ // The wrong spelling was INERT rather than harmless, and it is the #4449 shape
10802
+ // one surface over: the wiring guard asks only that a declared type HAS a
10803
+ // mapping, never that the mapping names a key some rule reads. So it would
10804
+ // have stayed green while the gate built `{ objects, seeds: [item] }` for
10805
+ // every seed write and every rule reading `stack.data` saw nothing — wired,
10806
+ // and running on nothing, with `rulesRun` reporting the rules as having run.
10807
+ // Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
10808
+ // changed no behaviour then; it was corrected here, with the measurement
10809
+ // that found it (#7576), rather than left for the rollout card to trip
10810
+ // over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
10811
+ // mapping is load-bearing today, not merely inert-and-correct.
10812
+ seed: "data",
10813
+ // [#8309] `permission`/`book` map ahead of their registration (#8310), the
10814
+ // same order `seed` arrived in: the mapping plus the enriched snapshot below
10815
+ // are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
10816
+ // a mapping without a declaring rule is inert by construction (the gate
10817
+ // filters by `runtimeTypes` before it ever consults this table), while a
10818
+ // declaration without the mapping is the wired-onto-nothing state the wiring
10819
+ // guard refuses. Landing the mapping first keeps #8310 a registry data edit.
10820
+ permission: "permissions",
10821
+ book: "books"
10173
10822
  };
10823
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
10174
10824
  function runtimeAuthoringRulesFor(type) {
10175
10825
  return AUTHORING_RULES.filter(
10176
10826
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10188,6 +10838,23 @@ function stackKeyForType(type) {
10188
10838
  return TYPE_TO_STACK_KEY[type] ?? null;
10189
10839
  }
10190
10840
  var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
10841
+ function buildRuntimeWriteSnapshots(args) {
10842
+ const stackKey = stackKeyForType(args.type);
10843
+ if (!stackKey) return null;
10844
+ if (!args.item || typeof args.item !== "object") return null;
10845
+ const item = args.item;
10846
+ const itemName = typeof item.name === "string" ? item.name : void 0;
10847
+ const baseline = {};
10848
+ for (const key of CONTEXT_STACK_KEYS) {
10849
+ const collection = args.context?.[key] ?? [];
10850
+ baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10851
+ }
10852
+ const candidate = {
10853
+ ...baseline,
10854
+ [stackKey]: [...baseline[stackKey] ?? [], item]
10855
+ };
10856
+ return { baseline, candidate };
10857
+ }
10191
10858
  function runRules(rules, stack, ctx) {
10192
10859
  const findings = [];
10193
10860
  for (const rule of rules) {
@@ -10210,19 +10877,15 @@ function runRuntimeAuthoringRules(args) {
10210
10877
  const rules = runtimeAuthoringRulesFor(args.type);
10211
10878
  const empty = { errors: [], advisories: [], rulesRun: [] };
10212
10879
  if (rules.length === 0) return empty;
10213
- const stackKey = stackKeyForType(args.type);
10214
- if (!stackKey) return empty;
10215
- if (!args.item || typeof args.item !== "object") return empty;
10216
- const item = args.item;
10217
- const itemName = typeof item.name === "string" ? item.name : void 0;
10218
- const contextObjects = args.context?.objects ?? [];
10880
+ const snapshots = buildRuntimeWriteSnapshots({
10881
+ type: args.type,
10882
+ item: args.item,
10883
+ ...args.context !== void 0 ? { context: args.context } : {}
10884
+ });
10885
+ if (!snapshots) return empty;
10219
10886
  const ctx = { sduiManifest: args.sduiManifest };
10220
- const writesIntoContext = stackKey === "objects";
10221
- const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
10222
- const baseline = { objects: baselineObjects };
10223
- const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
10224
- const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
10225
- const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
10887
+ const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10888
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
10226
10889
  return {
10227
10890
  errors: added.filter((f) => f.severity === "error"),
10228
10891
  advisories: added.filter((f) => f.severity !== "error"),
@@ -10271,6 +10934,7 @@ export {
10271
10934
  DASHBOARD_ACTION_ROUTE_UNRESOLVED,
10272
10935
  DASHBOARD_ACTION_TARGET_UNDEFINED,
10273
10936
  DASHBOARD_FILTER_FIELD_UNKNOWN,
10937
+ DASHBOARD_FILTER_FIELD_UNPROVISIONED,
10274
10938
  DEFAULT_AGENT_OUTSIDE_ROSTER,
10275
10939
  EXPRESSION_INVALID,
10276
10940
  FIELD_GROUP_EMPTY,
@@ -10297,6 +10961,7 @@ export {
10297
10961
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
10298
10962
  FLOW_PHANTOM_AGGREGATION,
10299
10963
  FLOW_RUNAS_UNSCOPED,
10964
+ FLOW_TEMPLATE_FIELD_UNPROVISIONED,
10300
10965
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
10301
10966
  FLOW_TEMPLATE_UNKNOWN_FIELD,
10302
10967
  FLOW_TIME_RELATIVE_ANTIPATTERN,
@@ -10318,9 +10983,11 @@ export {
10318
10983
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
10319
10984
  LIVENESS_DEAD_PROPERTY,
10320
10985
  LIVENESS_EXPERIMENTAL_PROPERTY,
10986
+ MANAGED_API_METHOD_UNAFFORDABLE,
10321
10987
  MAX_SCHEMA_WALK_DEPTH,
10322
10988
  MEASURE_AGGREGATE_INCOHERENT,
10323
10989
  NAV_OBJECT_UNGRANTED,
10990
+ NAV_OBJECT_UNSERVABLE,
10324
10991
  NAV_TARGET_UNRESOLVED,
10325
10992
  NULL_GUARD_HINT,
10326
10993
  OBJECT_REFERENCE_UNKNOWN,
@@ -10329,15 +10996,18 @@ export {
10329
10996
  ORG_AXIS_CROSS_ORG_BU_GRANT,
10330
10997
  ORG_AXIS_PERMISSION_INHERITANCE,
10331
10998
  PAGE_FIELD_UNKNOWN,
10999
+ PAGE_FIELD_UNPROVISIONED,
10332
11000
  PAGE_SOURCE_CLASSNAME,
10333
11001
  PREDICATE_PATH_UNRESOLVED,
10334
11002
  PREDICATE_PATH_UNROOTED,
11003
+ PREDICATE_RHS_PATH_SHAPED,
10335
11004
  PRE_SEAL_PHASES,
10336
11005
  REACT_BLOCK_NEEDS_RECORD_CONTEXT,
10337
11006
  REACT_CHART_AGGREGATE_INVALID,
10338
11007
  REACT_CHART_AXIS_UNKNOWN,
10339
11008
  REACT_CHART_DRILLDOWN_INVALID,
10340
11009
  REACT_CHART_FIELD_UNKNOWN,
11010
+ REACT_CHART_FIELD_UNPROVISIONED,
10341
11011
  REFERENCE_INTEGRITY_RULES,
10342
11012
  RLS_PREDICATE_OVER_BUDGET,
10343
11013
  RLS_PREDICATE_UNENFORCEABLE,
@@ -10348,6 +11018,7 @@ export {
10348
11018
  SEARCHABLE_FIELD_UNSEARCHABLE,
10349
11019
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
10350
11020
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
11021
+ SECURITY_CBP_NO_RELATION,
10351
11022
  SECURITY_DELEGATION_MISSING_REASON,
10352
11023
  SECURITY_EXTERNAL_WIDER,
10353
11024
  SECURITY_FLS_UNQUALIFIED_KEY,
@@ -10361,6 +11032,7 @@ export {
10361
11032
  SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
10362
11033
  SEED_VALUE_OUTSIDE_STATE_MACHINE,
10363
11034
  SEMANTIC_ROLE_FIELD_UNKNOWN,
11035
+ SEMANTIC_ROLE_FIELD_UNPROVISIONED,
10364
11036
  SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
10365
11037
  SHARING_RULE_UNLOWERABLE_CONDITION,
10366
11038
  STARTUP_OPEN_VOCABULARY_VERDICT,
@@ -10398,6 +11070,7 @@ export {
10398
11070
  WIDGET_MEASURE_UNKNOWN,
10399
11071
  authoringRulesFor,
10400
11072
  buildAccessMatrix,
11073
+ buildRuntimeWriteSnapshots,
10401
11074
  diffAccessMatrix,
10402
11075
  extractHookBodyWriteSet,
10403
11076
  extractHookBodyWrites,
@@ -10441,7 +11114,9 @@ export {
10441
11114
  validateHookBodyWrites,
10442
11115
  validateJsxPages,
10443
11116
  validateListViewMode,
11117
+ validateManagedApiMethods,
10444
11118
  validateNavAccess,
11119
+ validateNavObjectServability,
10445
11120
  validateNavTargetRefs,
10446
11121
  validateObjectReferences,
10447
11122
  validateOrgAxisRedLines,
@@ -10459,6 +11134,7 @@ export {
10459
11134
  validateRuleSchemaFormats,
10460
11135
  validateSearchableFields,
10461
11136
  validateSecurityPosture,
11137
+ validateSecurityRoleWord,
10462
11138
  validateSeedReplaySafety,
10463
11139
  validateSeedStateMachine,
10464
11140
  validateSemanticRoles,