@objectstack/lint 17.0.0-rc.0 → 17.0.0-rc.1

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
@@ -1,5 +1,16 @@
1
1
  // src/validate-widget-bindings.ts
2
2
  import { isIncoherentAggregate } from "@objectstack/spec/data";
3
+ import { ChartTypeSchema } from "@objectstack/spec/ui";
4
+
5
+ // src/system-fields.ts
6
+ import { FIELD_GROUP_SYSTEM_FIELDS } from "@objectstack/spec/data";
7
+ import { SystemFieldName } from "@objectstack/spec/system";
8
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
9
+ ...FIELD_GROUP_SYSTEM_FIELDS,
10
+ ...Object.values(SystemFieldName)
11
+ ]);
12
+
13
+ // src/validate-widget-bindings.ts
3
14
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
4
15
  var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
5
16
  var WIDGET_MEASURE_UNKNOWN = "widget-measure-unknown";
@@ -30,20 +41,18 @@ function asArray(v) {
30
41
  function asStrings(v) {
31
42
  return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
32
43
  }
33
- var CHART_TYPES = /* @__PURE__ */ new Set([
34
- "bar",
35
- "horizontal-bar",
36
- "column",
37
- "line",
38
- "area",
39
- "pie",
40
- "donut",
41
- "funnel",
42
- "scatter",
43
- "treemap",
44
- "sankey",
45
- "radar"
44
+ var MEASURE_EXEMPT_CHART_TYPES = /* @__PURE__ */ new Set([
45
+ "gauge",
46
+ "solid-gauge",
47
+ "metric",
48
+ "kpi",
49
+ "bullet",
50
+ "table",
51
+ "pivot"
46
52
  ]);
53
+ var CHART_TYPES = new Set(
54
+ ChartTypeSchema.options.filter((t) => !MEASURE_EXEMPT_CHART_TYPES.has(t))
55
+ );
47
56
  function levenshtein(a, b) {
48
57
  const m = a.length, n = b.length;
49
58
  let prev = Array.from({ length: n + 1 }, (_, j) => j);
@@ -89,18 +98,6 @@ function list(names) {
89
98
  }
90
99
  var DATE_RANGE_FILTER_NAME = "dateRange";
91
100
  var DATE_RANGE_DEFAULT_FIELD = "created_at";
92
- var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
93
- "id",
94
- "created_at",
95
- "created_by",
96
- "updated_at",
97
- "updated_by",
98
- "owner_id",
99
- "organization_id",
100
- "tenant_id",
101
- "user_id",
102
- "deleted_at"
103
- ]);
104
101
  function dashboardFilterDefs(dash) {
105
102
  const byName = /* @__PURE__ */ new Map();
106
103
  const dateRange = dash.dateRange;
@@ -334,6 +331,7 @@ function validateWidgetBindings(stack) {
334
331
 
335
332
  // src/validate-expressions.ts
336
333
  import { validateExpression } from "@objectstack/formula";
334
+ import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
337
335
  function asArray2(v) {
338
336
  if (Array.isArray(v)) return v;
339
337
  if (v && typeof v === "object") {
@@ -394,37 +392,53 @@ function validateStackExpressions(stack) {
394
392
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
395
393
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
396
394
  };
395
+ const checkDeclaredPredicate = (where, raw) => {
396
+ if (raw == null) return;
397
+ const res = validateExpression("predicate", raw);
398
+ for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
399
+ for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
400
+ };
397
401
  for (const flow of asArray2(stack.flows)) {
398
402
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
399
403
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
400
- const edges = Array.isArray(flow.edges) ? flow.edges : [];
401
404
  const startNode = nodes.find((n) => n.type === "start");
402
405
  const startCfg = startNode?.config ?? {};
403
406
  const objectName = typeof startCfg.objectName === "string" ? startCfg.objectName : void 0;
404
- for (const node of nodes) {
405
- const cfg = node.config ?? {};
406
- check(`flow '${flowName}' \xB7 node '${node.id}' (${node.type}) condition`, cfg.condition, objectName);
407
- if (node.type === "script") {
408
- const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
409
- const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
410
- const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
411
- if (!fn && !action && !inline) {
412
- issues.push({
413
- where: `flow '${flowName}' \xB7 node '${node.id}' (script) callable`,
414
- message: `script node declares neither \`actionType\` nor \`function\` \u2014 it would do nothing at runtime. Name a built-in action (e.g. \`actionType: 'email'\`) or a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
415
- source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
416
- });
417
- } else if (action === "invoke_function" && !fn) {
418
- issues.push({
419
- where: `flow '${flowName}' \xB7 node '${node.id}' (script) callable`,
420
- message: `script node uses \`actionType: 'invoke_function'\` but no \`function\` (or \`functionName\`) \u2014 it names no callable. Set \`function: 'my_fn'\` and register it via \`defineStack({ functions })\`.`,
421
- source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
422
- });
407
+ for (const graph of collectFlowGraphs(flow)) {
408
+ const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
409
+ for (const node of graph.nodes) {
410
+ const cfg = node.config ?? {};
411
+ check(`${at} \xB7 node '${node.id}' (${node.type}) condition`, cfg.condition, objectName);
412
+ const nodeType = typeof node.type === "string" ? node.type : "";
413
+ for (const found of resolveFlowNodeExpressions(nodeType, cfg)) {
414
+ if (found.entry.role !== "predicate") continue;
415
+ checkDeclaredPredicate(
416
+ `${at} \xB7 node '${node.id}' (${nodeType}) ${found.entry.label} at config.${found.path}`,
417
+ found.value
418
+ );
419
+ }
420
+ if (node.type === "script") {
421
+ const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
422
+ const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
423
+ const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
424
+ if (!fn && !action && !inline) {
425
+ issues.push({
426
+ where: `${at} \xB7 node '${node.id}' (script) callable`,
427
+ message: `script node declares neither \`actionType\` nor \`function\` \u2014 it would do nothing at runtime. Name a built-in action (e.g. \`actionType: 'email'\`) or a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
428
+ source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
429
+ });
430
+ } else if (action === "invoke_function" && !fn) {
431
+ issues.push({
432
+ where: `${at} \xB7 node '${node.id}' (script) callable`,
433
+ message: `script node uses \`actionType: 'invoke_function'\` but no \`function\` (or \`functionName\`) \u2014 it names no callable. Set \`function: 'my_fn'\` and register it via \`defineStack({ functions })\`.`,
434
+ source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
435
+ });
436
+ }
423
437
  }
424
438
  }
425
- }
426
- for (const edge of edges) {
427
- check(`flow '${flowName}' \xB7 edge '${edge.id}' (${edge.source}\u2192${edge.target}) condition`, edge.condition, objectName);
439
+ for (const edge of graph.edges) {
440
+ check(`${at} \xB7 edge '${edge.id}' (${edge.source}\u2192${edge.target}) condition`, edge.condition, objectName);
441
+ }
428
442
  }
429
443
  }
430
444
  for (const obj of objects) {
@@ -677,6 +691,85 @@ function validateFlowTriggerReadiness(stack) {
677
691
  return findings;
678
692
  }
679
693
 
694
+ // src/flow-walk.ts
695
+ import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
696
+ function isRec(v) {
697
+ return !!v && typeof v === "object" && !Array.isArray(v);
698
+ }
699
+ function strName(v) {
700
+ return typeof v === "string" && v.length > 0 ? v : void 0;
701
+ }
702
+ var REGION_SLOTS = new Map(
703
+ [...FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
704
+ );
705
+ var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
706
+ var MAX_REGION_DEPTH = 16;
707
+ function flowNodeLabel(node, index) {
708
+ return strName(node.label) ?? strName(node.id) ?? `#${index}`;
709
+ }
710
+ function stripRegions(config) {
711
+ if (!isRec(config)) return void 0;
712
+ let out;
713
+ for (const key of Object.keys(config)) {
714
+ if (!REGION_CONFIG_KEYS.has(key)) continue;
715
+ out ?? (out = { ...config });
716
+ delete out[key];
717
+ }
718
+ return out ?? config;
719
+ }
720
+ function walkFlowNodes(flow, flowPath) {
721
+ const out = [];
722
+ if (!isRec(flow)) return out;
723
+ const visitList = (nodes, basePath, trail, depth) => {
724
+ if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
725
+ nodes.forEach((raw, index) => {
726
+ if (!isRec(raw)) return;
727
+ const path = `${basePath}[${index}]`;
728
+ out.push({
729
+ node: raw,
730
+ path,
731
+ localConfig: stripRegions(raw.config),
732
+ regionTrail: trail,
733
+ depth
734
+ });
735
+ const type = strName(raw.type);
736
+ const slots = type ? REGION_SLOTS.get(type) : void 0;
737
+ if (!slots || !isRec(raw.config)) return;
738
+ const config = raw.config;
739
+ const here = `${type} "${flowNodeLabel(raw, index)}"`;
740
+ for (const slot of slots) {
741
+ const value = config[slot];
742
+ if (slot === "branches") {
743
+ if (!Array.isArray(value)) continue;
744
+ value.forEach((branch, b) => {
745
+ if (!isRec(branch)) return;
746
+ const branchName = strName(branch.name) ?? `#${b}`;
747
+ visitList(
748
+ branch.nodes,
749
+ `${path}.config.branches[${b}].nodes`,
750
+ joinTrail(trail, `${here} \u203A branch ${branchName}`),
751
+ depth + 1
752
+ );
753
+ });
754
+ continue;
755
+ }
756
+ if (!isRec(value)) continue;
757
+ visitList(
758
+ value.nodes,
759
+ `${path}.config.${slot}.nodes`,
760
+ joinTrail(trail, `${here} \u203A ${slot}`),
761
+ depth + 1
762
+ );
763
+ }
764
+ });
765
+ };
766
+ visitList(flow.nodes, `${flowPath}.nodes`, "", 0);
767
+ return out;
768
+ }
769
+ function joinTrail(trail, segment) {
770
+ return trail ? `${trail} \u203A ${segment}` : segment;
771
+ }
772
+
680
773
  // src/validate-flow-template-paths.ts
681
774
  var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
682
775
  var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
@@ -690,19 +783,10 @@ function asArray5(v) {
690
783
  }
691
784
  return [];
692
785
  }
693
- var SYSTEM_FIELDS2 = /* @__PURE__ */ new Set([
694
- "id",
786
+ var IMPLICIT_HEADS = /* @__PURE__ */ new Set([
787
+ ...SYSTEM_FIELDS,
695
788
  "name",
696
789
  "owner",
697
- "owner_id",
698
- "created_at",
699
- "created_by",
700
- "updated_at",
701
- "updated_by",
702
- "organization_id",
703
- "tenant_id",
704
- "is_deleted",
705
- "deleted_at",
706
790
  "record_type"
707
791
  ]);
708
792
  var RELATION_TYPES = /* @__PURE__ */ new Set([
@@ -832,12 +916,13 @@ function validateFlowTemplatePaths(stack) {
832
916
  if (!obj) return;
833
917
  const fieldTypes = fieldTypesOf(obj);
834
918
  const expandSet = declaredExpandOf(flow);
835
- nodes.forEach((node, nodeIndex) => {
836
- if (typeof node !== "object" || !node) return;
837
- const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${nodeIndex}`;
919
+ walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
920
+ const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
921
+ const where = regionTrail ? `flow "${flowName}" ${regionTrail} node "${nodeLabel}"` : `flow "${flowName}" node "${nodeLabel}"`;
838
922
  const nodeType = typeof node.type === "string" ? node.type : "";
839
923
  const guarded = FILTER_GUARDED_NODE_TYPES.has(nodeType);
840
- const leaves = collectNodeLeaves(node, guarded);
924
+ const scanNode = localConfig !== void 0 && localConfig !== node.config ? { ...node, config: localConfig } : node;
925
+ const leaves = collectNodeLeaves(scanNode, guarded);
841
926
  if (leaves.length === 0) return;
842
927
  const seenUnknown = /* @__PURE__ */ new Set();
843
928
  const seenTraversal = /* @__PURE__ */ new Set();
@@ -847,15 +932,15 @@ function validateFlowTemplatePaths(stack) {
847
932
  const head = rest[0];
848
933
  const hasSubPath = rest.length > 1;
849
934
  const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
850
- const isKnown = fieldTypes.has(head) || SYSTEM_FIELDS2.has(head);
935
+ const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
851
936
  if (!isKnown) {
852
937
  if (seenUnknown.has(head)) continue;
853
938
  seenUnknown.add(head);
854
939
  findings.push({
855
940
  severity: inFilter ? "error" : "warning",
856
941
  rule: FLOW_TEMPLATE_UNKNOWN_FIELD,
857
- where: `flow "${flowName}" node "${nodeLabel}"`,
858
- path: `flows[${flowIndex}].nodes[${nodeIndex}]`,
942
+ where,
943
+ path: nodePath,
859
944
  message: inFilter ? `${nodeType} filter references '{record.${rest.join(".")}}', but '${head}' is not a field on object '${objectName}' \u2014 the token resolves to nothing, which DROPS the condition from the query instead of narrowing it. The node refuses to run at execution time (#3810).` : `template references '{record.${rest.join(".")}}', but '${head}' is not a field on object '${objectName}' \u2014 it resolves to an empty string at runtime (silently).`,
860
945
  hint: inFilter ? `Check the field name against the object's field definitions (e.g. '{record.full_name}', not '{record.full_naem}'); system columns like id/created_at/owner are also addressable. This gates the build rather than warning: an absent condition WIDENS the query, so the runtime has already decided to refuse this node.` : `Check the field name against the object's field definitions (e.g. '{record.full_name}', not '{record.full_naem}'). System columns like id/created_at/owner are also addressable.`
861
946
  });
@@ -870,8 +955,8 @@ function validateFlowTemplatePaths(stack) {
870
955
  findings.push({
871
956
  severity: inFilter ? "error" : "warning",
872
957
  rule: FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
873
- where: `flow "${flowName}" node "${nodeLabel}"`,
874
- path: `flows[${flowIndex}].nodes[${nodeIndex}]`,
958
+ where,
959
+ path: nodePath,
875
960
  message: inFilter ? `${nodeType} filter references '{record.${key}}', a cross-object hop through the ${headType} field '${head}' \u2014 the flow record carries '${head}' as a scalar id, not an expanded object, so the token resolves to nothing and the condition is DROPPED from the query instead of narrowing it. The node refuses to run at execution time (#3810).` : `template references '{record.${key}}', a cross-object hop through the ${headType} field '${head}' \u2014 the flow record carries '${head}' as a scalar id, not an expanded object, so this resolves to an empty string at runtime (silently).`,
876
961
  hint: inFilter ? `Opt in to resolve it: add '${head}' to the start node's config.expand (#3475) and the engine re-reads it as the run's identity. Otherwise filter on the foreign-key id directly ('{record.${head}}'), or project the value via a formula field on '${objectName}'. This gates the build rather than warning: an absent condition WIDENS the query.` : `Opt in to resolve it: add '${head}' to the start node's config.expand (#3475) and the engine re-reads it as the run's identity. Otherwise reference the foreign-key id directly ('{record.${head}}'), or project the value via a formula field on '${objectName}'.`
877
962
  });
@@ -935,8 +1020,8 @@ function validateReadonlyFlowWrites(stack) {
935
1020
  if (flow.runAs === "system") return;
936
1021
  const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
937
1022
  const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
938
- const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
939
- nodes.forEach((node, nodeIndex) => {
1023
+ const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
1024
+ walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
940
1025
  if (node?.type !== "update_record") return;
941
1026
  const config = node.config ?? {};
942
1027
  const objectName = readLiteralObjectName(config);
@@ -945,7 +1030,8 @@ function validateReadonlyFlowWrites(stack) {
945
1030
  if (!fieldMap) return;
946
1031
  const fields = config.fields;
947
1032
  if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
948
- const nodeName = typeof node.label === "string" && node.label ? node.label : typeof node.id === "string" && node.id ? node.id : `#${nodeIndex}`;
1033
+ const nodeName = flowNodeLabel(node, walkIndex);
1034
+ const where = regionTrail ? `flow "${flowName}" \u203A ${regionTrail} \u203A node "${nodeName}"` : `flow "${flowName}" \u203A node "${nodeName}"`;
949
1035
  for (const fieldName of Object.keys(fields)) {
950
1036
  const meta = fieldMap.get(fieldName);
951
1037
  if (!meta) continue;
@@ -953,8 +1039,8 @@ function validateReadonlyFlowWrites(stack) {
953
1039
  findings.push({
954
1040
  severity: "error",
955
1041
  rule: FLOW_UPDATE_READONLY_FIELD,
956
- where: `flow "${flowName}" \u203A node "${nodeName}"`,
957
- path: `flows[${flowIndex}].nodes[${nodeIndex}].config.fields.${fieldName}`,
1042
+ where,
1043
+ path: `${nodePath}.config.fields.${fieldName}`,
958
1044
  message: `writes field '${fieldName}', which object '${objectName}' declares readonly:true. Under runAs:'${runAs}' the engine silently strips readonly fields from the UPDATE payload (#2948), so this write never lands \u2014 while the step still reports success.`,
959
1045
  hint: `If automation is meant to maintain this field, declare the flow runAs:'system' (the intended channel \u2014 readonly governs the end-user/API surface, not trusted system writers). Otherwise remove '${fieldName}' from this update_record node.`
960
1046
  });
@@ -962,8 +1048,8 @@ function validateReadonlyFlowWrites(stack) {
962
1048
  findings.push({
963
1049
  severity: "warning",
964
1050
  rule: FLOW_UPDATE_READONLY_WHEN_FIELD,
965
- where: `flow "${flowName}" \u203A node "${nodeName}"`,
966
- path: `flows[${flowIndex}].nodes[${nodeIndex}].config.fields.${fieldName}`,
1051
+ where,
1052
+ path: `${nodePath}.config.fields.${fieldName}`,
967
1053
  message: `writes field '${fieldName}', which object '${objectName}' declares readonlyWhen. On records where that predicate is TRUE, a runAs:'${runAs}' UPDATE strips the field (#3042), so this write may silently not land depending on the record's state.`,
968
1054
  hint: `If automation must maintain this field regardless of record state, run the flow runAs:'system'. Otherwise confirm this node only targets records whose readonlyWhen predicate is FALSE.`
969
1055
  });
@@ -1415,206 +1501,584 @@ function validateReactPages(stack) {
1415
1501
  // src/validate-react-page-props.ts
1416
1502
  import { createRequire as createRequire2 } from "module";
1417
1503
  import { REACT_BLOCKS, chartAggregateResultKeys } from "@objectstack/spec/ui";
1418
- var cachedTs = null;
1419
- function loadTypeScript() {
1420
- if (cachedTs) return cachedTs;
1421
- const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
1422
- try {
1423
- cachedTs = createRequire2(anchor)("typescript");
1424
- } catch (err) {
1425
- throw new Error(
1426
- `@objectstack/lint: validating a kind:'react' page requires the "typescript" 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 "typescript" in the image; it is only loaded when a react-source page is validated.`
1427
- );
1504
+ import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
1505
+
1506
+ // src/validate-searchable-fields.ts
1507
+ var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1508
+ function asArray10(v) {
1509
+ if (Array.isArray(v)) return v;
1510
+ if (v && typeof v === "object") {
1511
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1428
1512
  }
1429
- return cachedTs;
1513
+ return [];
1430
1514
  }
1431
- var asArray10 = (v) => Array.isArray(v) ? v : [];
1432
- var BLOCKS = new Map(
1433
- REACT_BLOCKS.map((b) => [
1434
- b.tag,
1435
- {
1436
- requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
1437
- knownProps: new Set(b.interactions.map((i) => i.name))
1438
- }
1439
- ])
1440
- );
1441
- function editDistance(a, b, cap = 2) {
1442
- if (Math.abs(a.length - b.length) > cap) return cap + 1;
1443
- const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
1444
- for (let j = 1; j <= b.length; j++) {
1445
- let prev = dp[0];
1446
- dp[0] = j;
1447
- for (let i = 1; i <= a.length; i++) {
1448
- const tmp = dp[i];
1449
- dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1450
- prev = tmp;
1451
- }
1452
- }
1453
- return dp[a.length];
1515
+ function isRec2(v) {
1516
+ return !!v && typeof v === "object" && !Array.isArray(v);
1454
1517
  }
1455
- function nearestKnown(prop, known) {
1456
- if (known.has(prop)) return null;
1457
- let best = null;
1458
- let bestD = 3;
1459
- for (const k of known) {
1460
- const d = editDistance(prop, k);
1461
- if (d < bestD) {
1462
- bestD = d;
1463
- best = k;
1464
- }
1518
+ function strName2(v) {
1519
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1520
+ }
1521
+ function declaredFieldNames(obj) {
1522
+ const fields = obj.fields;
1523
+ if (!fields || typeof fields !== "object") return null;
1524
+ const names = /* @__PURE__ */ new Set();
1525
+ for (const f of asArray10(fields)) {
1526
+ const n = strName2(f.name);
1527
+ if (n) names.add(n);
1465
1528
  }
1466
- return bestD <= 2 ? best : null;
1529
+ return names.size > 0 ? names : null;
1467
1530
  }
1468
- var NOT_STATIC = /* @__PURE__ */ Symbol("not-static");
1469
- function staticValue(tsc, sf, node) {
1470
- if (!node) return NOT_STATIC;
1471
- if (tsc.isParenthesizedExpression(node)) return staticValue(tsc, sf, node.expression);
1472
- if (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) return node.text;
1473
- if (tsc.isNumericLiteral(node)) return Number(node.text);
1474
- if (node.kind === tsc.SyntaxKind.TrueKeyword) return true;
1475
- if (node.kind === tsc.SyntaxKind.FalseKeyword) return false;
1476
- if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
1477
- if (tsc.isArrayLiteralExpression(node)) {
1478
- const out = [];
1479
- for (const el of node.elements) {
1480
- const v = staticValue(tsc, sf, el);
1481
- if (v === NOT_STATIC) return NOT_STATIC;
1482
- out.push(v);
1531
+ function suggest2(target, known) {
1532
+ let best;
1533
+ let bestScore = Infinity;
1534
+ for (const candidate of known) {
1535
+ const d = distance(target, candidate);
1536
+ if (d < bestScore) {
1537
+ bestScore = d;
1538
+ best = candidate;
1483
1539
  }
1484
- return out;
1485
1540
  }
1486
- if (tsc.isObjectLiteralExpression(node)) {
1487
- const out = {};
1488
- for (const p of node.properties) {
1489
- if (!tsc.isPropertyAssignment(p)) return NOT_STATIC;
1490
- const key = tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name) ? p.name.text : null;
1491
- if (key === null) return NOT_STATIC;
1492
- const v = staticValue(tsc, sf, p.initializer);
1493
- if (v === NOT_STATIC) return NOT_STATIC;
1494
- out[key] = v;
1541
+ const limit = Math.max(2, Math.floor(target.length / 3));
1542
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
1543
+ }
1544
+ function distance(a, b) {
1545
+ const m = a.length;
1546
+ const n = b.length;
1547
+ if (m === 0) return n;
1548
+ if (n === 0) return m;
1549
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
1550
+ for (let i = 1; i <= m; i++) {
1551
+ const curr = [i, ...new Array(n).fill(0)];
1552
+ for (let j = 1; j <= n; j++) {
1553
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1554
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1495
1555
  }
1496
- return out;
1556
+ prev = curr;
1497
1557
  }
1498
- return NOT_STATIC;
1499
- }
1500
- function attrValue(tsc, sf, attr) {
1501
- const init = attr.initializer;
1502
- if (!init) return true;
1503
- if (tsc.isStringLiteral(init)) return init.text;
1504
- if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
1505
- return NOT_STATIC;
1558
+ return prev[n];
1506
1559
  }
1507
- var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
1508
- var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
1509
- var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
1510
- var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
1511
- var SYSTEM_FIELDS3 = /* @__PURE__ */ new Set([
1512
- "id",
1513
- "created_at",
1514
- "created_by",
1515
- "updated_at",
1516
- "updated_by",
1517
- "owner_id",
1518
- "organization_id",
1519
- "tenant_id",
1520
- "user_id",
1521
- "deleted_at"
1522
- ]);
1523
- function namedArray(v) {
1524
- if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
1525
- if (v && typeof v === "object") {
1526
- return Object.entries(v).map(([name, def]) => ({
1527
- name,
1528
- ...def && typeof def === "object" ? def : {}
1529
- }));
1560
+ function indexObjectSearchTargets(stack) {
1561
+ const fieldsByObject = /* @__PURE__ */ new Map();
1562
+ if (!isRec2(stack)) return fieldsByObject;
1563
+ for (const obj of asArray10(stack.objects)) {
1564
+ const name = strName2(obj.name);
1565
+ if (name) fieldsByObject.set(name, declaredFieldNames(obj));
1530
1566
  }
1531
- return [];
1567
+ return fieldsByObject;
1532
1568
  }
1533
- function indexObjectFields(stack) {
1534
- const out = /* @__PURE__ */ new Map();
1535
- for (const obj of namedArray(stack.objects)) {
1536
- const name = typeof obj.name === "string" ? obj.name : void 0;
1569
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
1570
+ const findings = [];
1571
+ if (!Array.isArray(declared) || declared.length === 0) return findings;
1572
+ if (!objectName) return findings;
1573
+ if (!fieldsByObject.has(objectName)) return findings;
1574
+ const known = fieldsByObject.get(objectName);
1575
+ if (!known) return findings;
1576
+ for (let i = 0; i < declared.length; i++) {
1577
+ const entry = declared[i];
1578
+ const name = strName2(entry);
1537
1579
  if (!name) continue;
1538
- const names = /* @__PURE__ */ new Set();
1539
- for (const f of namedArray(obj.fields)) {
1540
- if (typeof f.name === "string" && f.name) names.add(f.name);
1541
- }
1542
- out.set(name, names);
1580
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) continue;
1581
+ const dotted = name.includes(".");
1582
+ findings.push({
1583
+ severity: "error",
1584
+ rule: SEARCHABLE_FIELD_UNKNOWN,
1585
+ where,
1586
+ path: `${path}[${i}]`,
1587
+ message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest2(name, known)),
1588
+ hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1589
+ });
1543
1590
  }
1544
- return out;
1591
+ return findings;
1545
1592
  }
1546
- var isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1547
- var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
1548
- function checkObjectChart(attrs, objectFields, findings) {
1549
- const { values, where, path } = attrs;
1550
- const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
1551
- if (values.has("data")) return;
1552
- const aggregate = values.get("aggregate");
1553
- if (aggregate === void 0 || aggregate === NOT_STATIC) return;
1554
- if (!isRec(aggregate)) return;
1555
- const fn = strOf(aggregate.function);
1556
- const field = strOf(aggregate.field);
1557
- const groupBy = aggregate.groupBy;
1558
- const groupByField = strOf(groupBy) ?? (isRec(groupBy) ? strOf(groupBy.field) : void 0);
1559
- if (fn && !CHART_FUNCTIONS.includes(fn)) {
1560
- push(
1561
- "error",
1562
- REACT_CHART_AGGREGATE_INVALID,
1563
- `aggregate.function "${fn}" is not an aggregation this chart can run.`,
1564
- `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
1593
+ function validateSearchableFields(stack) {
1594
+ const findings = [];
1595
+ if (!isRec2(stack)) return findings;
1596
+ const objects = asArray10(stack.objects);
1597
+ const fieldsByObject = indexObjectSearchTargets(stack);
1598
+ const check = (declared, objectName, where, path, subject) => {
1599
+ findings.push(
1600
+ ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
1565
1601
  );
1566
- } else if (fn && fn !== "count" && !field) {
1567
- push(
1568
- "error",
1569
- REACT_CHART_AGGREGATE_INVALID,
1570
- `aggregate.function "${fn}" has no "field" to aggregate.`,
1571
- 'Add aggregate.field, or use function "count" (the only one that may omit it).'
1602
+ };
1603
+ for (let oi = 0; oi < objects.length; oi++) {
1604
+ const obj = objects[oi];
1605
+ if (!isRec2(obj)) continue;
1606
+ const objName = strName2(obj.name);
1607
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1608
+ check(
1609
+ obj.searchableFields,
1610
+ objName,
1611
+ label2,
1612
+ `objects[${oi}].searchableFields`,
1613
+ "searchableFields"
1572
1614
  );
1615
+ if (isRec2(obj.listViews)) {
1616
+ for (const [key, lv] of Object.entries(obj.listViews)) {
1617
+ if (!isRec2(lv)) continue;
1618
+ check(
1619
+ lv.searchableFields,
1620
+ // A built-in list view belongs to its object; an inline `data.object`
1621
+ // may still retarget it (ADR-0047 allows the explicit binding).
1622
+ listViewObject(lv) ?? objName,
1623
+ `${label2} \u203A listViews.${key}`,
1624
+ `objects[${oi}].listViews.${key}.searchableFields`,
1625
+ "list-view searchableFields"
1626
+ );
1627
+ }
1628
+ }
1573
1629
  }
1574
- const objectName = strOf(values.get("objectName"));
1575
- const known = objectName ? objectFields.get(objectName) : void 0;
1576
- if (objectName && known) {
1577
- const fieldRef = (name, prop) => {
1578
- if (!name) return;
1579
- if (name.includes(".")) return;
1580
- if (known.has(name) || SYSTEM_FIELDS3.has(name)) return;
1581
- push(
1582
- "error",
1583
- REACT_CHART_FIELD_UNKNOWN,
1584
- `aggregate.${prop} "${name}" is not a field on object "${objectName}" \u2014 the aggregate query has nothing to ${prop === "groupBy" ? "group by" : "aggregate"}, so the chart comes back empty.`,
1585
- `Fix the field name, or add "${name}" to ${objectName}.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1630
+ const views = asArray10(stack.views);
1631
+ for (let vi = 0; vi < views.length; vi++) {
1632
+ const view = views[vi];
1633
+ if (!isRec2(view)) continue;
1634
+ const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
1635
+ const viewObject = strName2(view.objectName) ?? strName2(view.object);
1636
+ if (isRec2(view.list)) {
1637
+ check(
1638
+ view.list.searchableFields,
1639
+ listViewObject(view.list) ?? viewObject,
1640
+ `view "${viewLabel}" \u203A list`,
1641
+ `views[${vi}].list.searchableFields`,
1642
+ "list-view searchableFields"
1586
1643
  );
1587
- };
1588
- fieldRef(field, "field");
1589
- fieldRef(groupByField, "groupBy");
1644
+ }
1645
+ if (isRec2(view.listViews)) {
1646
+ for (const [key, lv] of Object.entries(view.listViews)) {
1647
+ if (!isRec2(lv)) continue;
1648
+ check(
1649
+ lv.searchableFields,
1650
+ listViewObject(lv) ?? viewObject,
1651
+ `view "${viewLabel}" \u203A listViews.${key}`,
1652
+ `views[${vi}].listViews.${key}.searchableFields`,
1653
+ "list-view searchableFields"
1654
+ );
1655
+ }
1656
+ }
1590
1657
  }
1591
- const keys = chartAggregateResultKeys({ field, function: fn, groupBy });
1592
- const columns = [keys.category, keys.value].filter((k) => !!k);
1593
- if (columns.length === 0) return;
1594
- const axisRef = (name, prop) => {
1595
- if (!name) return;
1596
- if (columns.includes(name)) return;
1597
- if (keys.comparison && name === keys.comparison) return;
1598
- push(
1599
- "error",
1600
- REACT_CHART_AXIS_UNKNOWN,
1601
- `"${name}" is not a column this aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).`,
1602
- `Result columns: ${columns.join(", ")}` + (keys.comparison ? ` (plus "${keys.comparison}" with a comparison overlay)` : "") + `. Bind ${prop} to one of them.`
1603
- );
1604
- };
1605
- const xAxisRaw = values.get("xAxis");
1606
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
1658
+ return findings;
1659
+ }
1660
+ function listViewObject(listView) {
1661
+ const data = listView.data;
1662
+ return isRec2(data) ? strName2(data.object) : void 0;
1663
+ }
1664
+
1665
+ // src/page-walk.ts
1666
+ function isRec3(v) {
1667
+ return !!v && typeof v === "object" && !Array.isArray(v);
1668
+ }
1669
+ function strName3(v) {
1670
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1671
+ }
1672
+ var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
1673
+ function isSourceAuthoredPage(page) {
1674
+ const kind = strName3(page.kind);
1675
+ return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
1676
+ }
1677
+ function walkPageComponents(page, pagePath) {
1678
+ const out = [];
1679
+ if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
1680
+ const pageObject = strName3(page.object);
1681
+ const visit = (node, path, inheritedObject) => {
1682
+ if (!isRec3(node)) return;
1683
+ const props = isRec3(node.properties) ? node.properties : void 0;
1684
+ const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
1685
+ const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
1686
+ out.push({ component: node, path, objectName });
1687
+ if (!props) return;
1688
+ if (Array.isArray(props.items)) {
1689
+ for (let i = 0; i < props.items.length; i++) {
1690
+ const item = props.items[i];
1691
+ if (!isRec3(item) || !Array.isArray(item.children)) continue;
1692
+ for (let c = 0; c < item.children.length; c++) {
1693
+ visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1694
+ }
1695
+ }
1696
+ }
1697
+ if (Array.isArray(props.children)) {
1698
+ for (let i = 0; i < props.children.length; i++) {
1699
+ visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
1700
+ }
1701
+ }
1702
+ for (const key of ["body", "footer"]) {
1703
+ const slotList = props[key];
1704
+ if (!Array.isArray(slotList)) continue;
1705
+ for (let i = 0; i < slotList.length; i++) {
1706
+ visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
1707
+ }
1708
+ }
1709
+ };
1710
+ const regions = Array.isArray(page.regions) ? page.regions : [];
1711
+ for (let r = 0; r < regions.length; r++) {
1712
+ const region = regions[r];
1713
+ if (!isRec3(region) || !Array.isArray(region.components)) continue;
1714
+ for (let c = 0; c < region.components.length; c++) {
1715
+ visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1716
+ }
1717
+ }
1718
+ const slots = isRec3(page.slots) ? page.slots : void 0;
1719
+ if (slots) {
1720
+ for (const [slot, value] of Object.entries(slots)) {
1721
+ const list3 = Array.isArray(value) ? value : [value];
1722
+ const indexed = Array.isArray(value);
1723
+ for (let i = 0; i < list3.length; i++) {
1724
+ visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
1725
+ }
1726
+ }
1727
+ }
1728
+ return out;
1729
+ }
1730
+
1731
+ // src/validate-page-field-bindings.ts
1732
+ var PAGE_FIELD_UNKNOWN = "page-field-unknown";
1733
+ function asArray11(v) {
1734
+ if (Array.isArray(v)) return v;
1735
+ if (v && typeof v === "object") {
1736
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1737
+ }
1738
+ return [];
1739
+ }
1740
+ function strName4(v) {
1741
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1742
+ }
1743
+ function isRec4(v) {
1744
+ return !!v && typeof v === "object" && !Array.isArray(v);
1745
+ }
1746
+ function fieldRefsFrom(value, basePath) {
1747
+ const out = [];
1748
+ const one = (v, path) => {
1749
+ const bare = strName4(v);
1750
+ if (bare) {
1751
+ out.push({ name: bare, path });
1752
+ return;
1753
+ }
1754
+ if (!isRec4(v)) return;
1755
+ const named = strName4(v.field) ?? strName4(v.name);
1756
+ if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
1757
+ };
1758
+ if (Array.isArray(value)) {
1759
+ for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
1760
+ } else {
1761
+ one(value, basePath);
1762
+ }
1763
+ return out;
1764
+ }
1765
+ function sortFieldRefs(value, basePath) {
1766
+ if (typeof value === "string") {
1767
+ const head = value.trim().split(/\s+/)[0];
1768
+ return head ? [{ name: head, path: basePath }] : [];
1769
+ }
1770
+ return fieldRefsFrom(value, basePath);
1771
+ }
1772
+ var COMPONENT_FIELD_SPECS = {
1773
+ "record:highlights": { props: ["fields"] },
1774
+ // `sections`/`hideFields` are not in RecordDetailsProps, but every real page
1775
+ // authors them (they survive because `properties` is unvalidated).
1776
+ "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
1777
+ "record:path": { props: ["statusField"] },
1778
+ "element:number": { props: ["field"] },
1779
+ "element:filter": { props: ["fields"] },
1780
+ "element:form": { props: ["fields"] },
1781
+ // The schema says `displayField`; real pages author `labelField`. Accept both.
1782
+ "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
1783
+ };
1784
+ var RELATED_LIST_TYPE = "record:related_list";
1785
+ function componentFieldRefs(type, props, basePath, sep = ".") {
1786
+ const spec = COMPONENT_FIELD_SPECS[type];
1787
+ if (!spec) return null;
1788
+ const refs = [];
1789
+ for (const key of spec.props ?? []) {
1790
+ refs.push(...fieldRefsFrom(props[key], `${basePath}${sep}${key}`));
1791
+ }
1792
+ for (const key of spec.nestedSections ?? []) {
1793
+ const sections = Array.isArray(props[key]) ? props[key] : [];
1794
+ for (let si = 0; si < sections.length; si++) {
1795
+ const section = sections[si];
1796
+ if (!isRec4(section)) continue;
1797
+ refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
1798
+ }
1799
+ }
1800
+ return refs;
1801
+ }
1802
+ function relatedListFieldRefs(props, basePath, sep = ".") {
1803
+ const add = isRec4(props.add) ? props.add : void 0;
1804
+ const picker = add && isRec4(add.picker) ? add.picker : void 0;
1805
+ const at = (key) => `${basePath}${sep}${key}`;
1806
+ return {
1807
+ relatedObject: strName4(props.objectName),
1808
+ related: [
1809
+ ...fieldRefsFrom(props.columns, at("columns")),
1810
+ ...sortFieldRefs(props.sort, at("sort")),
1811
+ ...fieldRefsFrom(props.filter, at("filter")),
1812
+ ...fieldRefsFrom(props.relationshipField, at("relationshipField")),
1813
+ ...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
1814
+ ],
1815
+ parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
1816
+ pickerObject: picker ? strName4(picker.object) : void 0,
1817
+ picker: picker ? [
1818
+ ...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
1819
+ ...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
1820
+ ] : []
1821
+ };
1822
+ }
1823
+ function indexObjectFields(stack) {
1824
+ const objectFields = /* @__PURE__ */ new Map();
1825
+ if (!isRec4(stack)) return objectFields;
1826
+ for (const obj of asArray11(stack.objects)) {
1827
+ const name = strName4(obj.name);
1828
+ if (!name) continue;
1829
+ const names = /* @__PURE__ */ new Set();
1830
+ for (const f of asArray11(obj.fields)) {
1831
+ const fn = strName4(f.name);
1832
+ if (fn) names.add(fn);
1833
+ }
1834
+ objectFields.set(name, names);
1835
+ }
1836
+ return objectFields;
1837
+ }
1838
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence = "skipped") {
1839
+ const findings = [];
1840
+ if (!objectName) return findings;
1841
+ const known = objectFields.get(objectName);
1842
+ if (!known) return findings;
1843
+ for (const ref of refs) {
1844
+ if (ref.name.includes(".")) continue;
1845
+ if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
1846
+ findings.push({
1847
+ severity: consequence === "queried" ? "error" : "warning",
1848
+ rule: PAGE_FIELD_UNKNOWN,
1849
+ where,
1850
+ path: ref.path,
1851
+ message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
1852
+ hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1853
+ });
1854
+ }
1855
+ return findings;
1856
+ }
1857
+ function validatePageFieldBindings(stack) {
1858
+ const findings = [];
1859
+ if (!stack || typeof stack !== "object") return findings;
1860
+ const objectFields = indexObjectFields(stack);
1861
+ const pages = asArray11(stack.pages);
1862
+ for (let pi = 0; pi < pages.length; pi++) {
1863
+ const page = pages[pi];
1864
+ if (!page || typeof page !== "object") continue;
1865
+ const pageName = strName4(page.name) ?? `#${pi}`;
1866
+ const checkRefs = (refs, objectName, where) => {
1867
+ findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
1868
+ };
1869
+ for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
1870
+ const type = strName4(component.type);
1871
+ const props = isRec4(component.properties) ? component.properties : void 0;
1872
+ if (!type || !props) continue;
1873
+ const where = `page "${pageName}" \xB7 ${type}`;
1874
+ const base = `${path}.properties`;
1875
+ if (type === RELATED_LIST_TYPE) {
1876
+ const split = relatedListFieldRefs(props, base);
1877
+ checkRefs(split.related, split.relatedObject, where);
1878
+ checkRefs(split.parent, objectName, where);
1879
+ checkRefs(split.picker, split.pickerObject, where);
1880
+ continue;
1881
+ }
1882
+ const refs = componentFieldRefs(type, props, base);
1883
+ if (!refs) continue;
1884
+ checkRefs(refs, objectName, where);
1885
+ }
1886
+ const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
1887
+ if (cfg) {
1888
+ const cfgObject = strName4(cfg.source) ?? strName4(page.object);
1889
+ const base = `pages[${pi}].interfaceConfig`;
1890
+ const refs = [
1891
+ ...fieldRefsFrom(cfg.columns, `${base}.columns`),
1892
+ ...sortFieldRefs(cfg.sort, `${base}.sort`),
1893
+ ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
1894
+ ];
1895
+ const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
1896
+ if (userFilters) {
1897
+ refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
1898
+ }
1899
+ checkRefs(refs, cfgObject, `page "${pageName}" \xB7 interfaceConfig`);
1900
+ }
1901
+ }
1902
+ return findings;
1903
+ }
1904
+
1905
+ // src/validate-react-page-props.ts
1906
+ var cachedTs = null;
1907
+ function loadTypeScript() {
1908
+ if (cachedTs) return cachedTs;
1909
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
1910
+ try {
1911
+ cachedTs = createRequire2(anchor)("typescript");
1912
+ } catch (err) {
1913
+ throw new Error(
1914
+ `@objectstack/lint: validating a kind:'react' page requires the "typescript" 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 "typescript" in the image; it is only loaded when a react-source page is validated.`
1915
+ );
1916
+ }
1917
+ return cachedTs;
1918
+ }
1919
+ var asArray12 = (v) => Array.isArray(v) ? v : [];
1920
+ var BLOCKS = new Map(
1921
+ REACT_BLOCKS.map((b) => [
1922
+ b.tag,
1923
+ {
1924
+ requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
1925
+ knownProps: new Set(b.interactions.map((i) => i.name))
1926
+ }
1927
+ ])
1928
+ );
1929
+ function editDistance(a, b, cap = 2) {
1930
+ if (Math.abs(a.length - b.length) > cap) return cap + 1;
1931
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
1932
+ for (let j = 1; j <= b.length; j++) {
1933
+ let prev = dp[0];
1934
+ dp[0] = j;
1935
+ for (let i = 1; i <= a.length; i++) {
1936
+ const tmp = dp[i];
1937
+ dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1938
+ prev = tmp;
1939
+ }
1940
+ }
1941
+ return dp[a.length];
1942
+ }
1943
+ function nearestKnown(prop, known) {
1944
+ if (known.has(prop)) return null;
1945
+ let best = null;
1946
+ let bestD = 3;
1947
+ for (const k of known) {
1948
+ const d = editDistance(prop, k);
1949
+ if (d < bestD) {
1950
+ bestD = d;
1951
+ best = k;
1952
+ }
1953
+ }
1954
+ return bestD <= 2 ? best : null;
1955
+ }
1956
+ var NOT_STATIC = /* @__PURE__ */ Symbol("not-static");
1957
+ function staticValue(tsc, sf, node) {
1958
+ if (!node) return NOT_STATIC;
1959
+ if (tsc.isParenthesizedExpression(node)) return staticValue(tsc, sf, node.expression);
1960
+ if (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) return node.text;
1961
+ if (tsc.isNumericLiteral(node)) return Number(node.text);
1962
+ if (node.kind === tsc.SyntaxKind.TrueKeyword) return true;
1963
+ if (node.kind === tsc.SyntaxKind.FalseKeyword) return false;
1964
+ if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
1965
+ if (tsc.isArrayLiteralExpression(node)) {
1966
+ const out = [];
1967
+ for (const el of node.elements) {
1968
+ const v = staticValue(tsc, sf, el);
1969
+ if (v === NOT_STATIC) return NOT_STATIC;
1970
+ out.push(v);
1971
+ }
1972
+ return out;
1973
+ }
1974
+ if (tsc.isObjectLiteralExpression(node)) {
1975
+ const out = {};
1976
+ for (const p of node.properties) {
1977
+ if (!tsc.isPropertyAssignment(p)) return NOT_STATIC;
1978
+ const key = tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name) ? p.name.text : null;
1979
+ if (key === null) return NOT_STATIC;
1980
+ const v = staticValue(tsc, sf, p.initializer);
1981
+ if (v === NOT_STATIC) return NOT_STATIC;
1982
+ out[key] = v;
1983
+ }
1984
+ return out;
1985
+ }
1986
+ return NOT_STATIC;
1987
+ }
1988
+ function attrValue(tsc, sf, attr) {
1989
+ const init = attr.initializer;
1990
+ if (!init) return true;
1991
+ if (tsc.isStringLiteral(init)) return init.text;
1992
+ if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
1993
+ return NOT_STATIC;
1994
+ }
1995
+ function filterAttrValue(tsc, sf, attr) {
1996
+ const init = attr.initializer;
1997
+ if (!init || !tsc.isJsxExpression(init)) return NOT_STATIC;
1998
+ const perPosition = (node) => {
1999
+ if (!node) return NOT_STATIC;
2000
+ if (tsc.isParenthesizedExpression(node)) return perPosition(node.expression);
2001
+ if (tsc.isArrayLiteralExpression(node)) return node.elements.map((el) => perPosition(el));
2002
+ return staticValue(tsc, sf, node);
2003
+ };
2004
+ return perPosition(init.expression);
2005
+ }
2006
+ var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
2007
+ var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
2008
+ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
2009
+ var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
2010
+ var isRec5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2011
+ var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2012
+ function checkObjectChart(attrs, objectFields, findings) {
2013
+ const { values, where, path } = attrs;
2014
+ const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2015
+ if (values.has("data")) return;
2016
+ const aggregate = values.get("aggregate");
2017
+ if (aggregate === void 0 || aggregate === NOT_STATIC) return;
2018
+ if (!isRec5(aggregate)) return;
2019
+ const fn = strOf(aggregate.function);
2020
+ const field = strOf(aggregate.field);
2021
+ const groupBy = aggregate.groupBy;
2022
+ const groupByField = strOf(groupBy) ?? (isRec5(groupBy) ? strOf(groupBy.field) : void 0);
2023
+ if (fn && !CHART_FUNCTIONS.includes(fn)) {
2024
+ push(
2025
+ "error",
2026
+ REACT_CHART_AGGREGATE_INVALID,
2027
+ `aggregate.function "${fn}" is not an aggregation this chart can run.`,
2028
+ `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
2029
+ );
2030
+ } else if (fn && fn !== "count" && !field) {
2031
+ push(
2032
+ "error",
2033
+ REACT_CHART_AGGREGATE_INVALID,
2034
+ `aggregate.function "${fn}" has no "field" to aggregate.`,
2035
+ 'Add aggregate.field, or use function "count" (the only one that may omit it).'
2036
+ );
2037
+ }
2038
+ const objectName = strOf(values.get("objectName"));
2039
+ const known = objectName ? objectFields.get(objectName) : void 0;
2040
+ if (objectName && known) {
2041
+ const fieldRef = (name, prop) => {
2042
+ if (!name) return;
2043
+ if (name.includes(".")) return;
2044
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
2045
+ push(
2046
+ "error",
2047
+ REACT_CHART_FIELD_UNKNOWN,
2048
+ `aggregate.${prop} "${name}" is not a field on object "${objectName}" \u2014 the aggregate query has nothing to ${prop === "groupBy" ? "group by" : "aggregate"}, so the chart comes back empty.`,
2049
+ `Fix the field name, or add "${name}" to ${objectName}.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
2050
+ );
2051
+ };
2052
+ fieldRef(field, "field");
2053
+ fieldRef(groupByField, "groupBy");
2054
+ }
2055
+ const keys = chartAggregateResultKeys({ field, function: fn, groupBy });
2056
+ const columns = [keys.category, keys.value].filter((k) => !!k);
2057
+ if (columns.length === 0) return;
2058
+ const axisRef = (name, prop) => {
2059
+ if (!name) return;
2060
+ if (columns.includes(name)) return;
2061
+ if (keys.comparison && name === keys.comparison) return;
2062
+ push(
2063
+ "error",
2064
+ REACT_CHART_AXIS_UNKNOWN,
2065
+ `"${name}" is not a column this aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).`,
2066
+ `Result columns: ${columns.join(", ")}` + (keys.comparison ? ` (plus "${keys.comparison}" with a comparison overlay)` : "") + `. Bind ${prop} to one of them.`
2067
+ );
2068
+ };
2069
+ const xAxisRaw = values.get("xAxis");
2070
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec5(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
1607
2071
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
1608
2072
  axisRef(categoryAxis, categoryProp);
1609
2073
  const yAxisRaw = values.get("yAxis");
1610
2074
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
1611
2075
  for (const a of yAxisList) {
1612
- axisRef(strOf(a) ?? (isRec(a) ? strOf(a.field) : void 0), "yAxis[].field");
2076
+ axisRef(strOf(a) ?? (isRec5(a) ? strOf(a.field) : void 0), "yAxis[].field");
1613
2077
  }
1614
2078
  const series = values.get("series");
1615
2079
  if (Array.isArray(series)) {
1616
2080
  for (const s of series) {
1617
- if (!isRec(s)) continue;
2081
+ if (!isRec5(s)) continue;
1618
2082
  const dataKey = strOf(s.dataKey);
1619
2083
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
1620
2084
  }
@@ -1628,10 +2092,156 @@ function checkObjectChart(attrs, objectFields, findings) {
1628
2092
  );
1629
2093
  }
1630
2094
  }
2095
+ var REACT_FIELD_SPECS = {
2096
+ ListView: {
2097
+ // `fields` is the React overlay's "limit/order the columns"; `columns` the
2098
+ // spec ListView prop. Both name columns on the bound object, and a page may
2099
+ // write either. `hiddenFields`/`fieldOrder`/`filterableFields` are schema
2100
+ // props outside the curated contract — unadvertised but honored by the
2101
+ // renderer, so a stale name there is drift just the same.
2102
+ fields: ["fields", "columns", "hiddenFields", "fieldOrder", "filterableFields"],
2103
+ sorts: ["sort"],
2104
+ nestedFields: ["userFilters", "grouping"],
2105
+ filterArrays: ["filters"]
2106
+ },
2107
+ ObjectForm: {
2108
+ fields: ["fields"],
2109
+ keyedByField: ["initialValues"],
2110
+ // `groups` is FormViewSchema's legacy alias for `sections`.
2111
+ sections: ["sections", "groups"]
2112
+ },
2113
+ ObjectChart: {
2114
+ // The axes are result columns (checkObjectChart owns them); `filter` is an
2115
+ // ordinary ObjectQL predicate over the bound object, like ListView's.
2116
+ filterArrays: ["filter"]
2117
+ }
2118
+ };
2119
+ var PATH_SEP = " \u203A ";
2120
+ var SCHEMA_TYPE_BY_TAG = new Map(
2121
+ REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
2122
+ );
2123
+ var FILTER_PROPS = new Set(
2124
+ Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
2125
+ );
2126
+ function readableProps(values) {
2127
+ const out = {};
2128
+ for (const [k, v] of values) if (v !== NOT_STATIC) out[k] = v;
2129
+ return out;
2130
+ }
2131
+ function subformFieldRefs(value, basePath) {
2132
+ const child = [];
2133
+ const parent = [];
2134
+ if (!Array.isArray(value)) return { child, parent };
2135
+ for (let i = 0; i < value.length; i++) {
2136
+ const sub = value[i];
2137
+ if (!isRec5(sub)) continue;
2138
+ const at = (key) => `${basePath}[${i}].${key}`;
2139
+ child.push({
2140
+ objectName: strOf(sub.childObject),
2141
+ refs: [
2142
+ ...fieldRefsFrom(sub.columns, at("columns")),
2143
+ ...fieldRefsFrom(sub.relationshipField, at("relationshipField")),
2144
+ ...fieldRefsFrom(sub.amountField, at("amountField"))
2145
+ ]
2146
+ });
2147
+ parent.push(...fieldRefsFrom(sub.totalField, at("totalField")));
2148
+ }
2149
+ return { child, parent };
2150
+ }
2151
+ function filterFieldRefs(node, basePath, out) {
2152
+ if (!Array.isArray(node) || node.length === 0) return;
2153
+ const head = node[0];
2154
+ if (typeof head === "string" && (head.toLowerCase() === "and" || head.toLowerCase() === "or")) {
2155
+ for (let i = 1; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
2156
+ return;
2157
+ }
2158
+ if (Array.isArray(head)) {
2159
+ for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
2160
+ return;
2161
+ }
2162
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
2163
+ out.push({ name: head, path: `${basePath}[0]` });
2164
+ }
2165
+ }
2166
+ function reactFieldRefs(spec, values, basePath) {
2167
+ const own = [];
2168
+ const queried = [];
2169
+ const readable = (key) => {
2170
+ const v = values.get(key);
2171
+ return v === NOT_STATIC ? void 0 : v;
2172
+ };
2173
+ const at = (key) => `${basePath}${PATH_SEP}${key}`;
2174
+ for (const key of spec.fields ?? []) {
2175
+ own.push(...fieldRefsFrom(readable(key), at(key)));
2176
+ }
2177
+ for (const key of spec.sorts ?? []) {
2178
+ own.push(...sortFieldRefs(readable(key), at(key)));
2179
+ }
2180
+ for (const key of spec.nestedFields ?? []) {
2181
+ const v = readable(key);
2182
+ if (isRec5(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2183
+ }
2184
+ for (const key of spec.sections ?? []) {
2185
+ const v = readable(key);
2186
+ if (!Array.isArray(v)) continue;
2187
+ for (let i = 0; i < v.length; i++) {
2188
+ const section = v[i];
2189
+ if (!isRec5(section)) continue;
2190
+ own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
2191
+ }
2192
+ }
2193
+ for (const key of spec.keyedByField ?? []) {
2194
+ const v = readable(key);
2195
+ if (!isRec5(v)) continue;
2196
+ for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
2197
+ }
2198
+ for (const key of spec.filterArrays ?? []) {
2199
+ filterFieldRefs(values.get(key), at(key), queried);
2200
+ }
2201
+ return { own, queried };
2202
+ }
2203
+ function checkBlockFieldProps(tag, values, objectFields, where, path) {
2204
+ const objectName = strOf(values.get("objectName"));
2205
+ const out = [];
2206
+ const spec = REACT_FIELD_SPECS[tag];
2207
+ if (spec) {
2208
+ const { own, queried } = reactFieldRefs(spec, values, path);
2209
+ out.push(...checkFieldRefs(own, objectName, objectFields, where));
2210
+ out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
2211
+ }
2212
+ if (tag === "ObjectForm") {
2213
+ const raw = values.get("subforms");
2214
+ const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
2215
+ for (const sub of subs.child) {
2216
+ out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
2217
+ }
2218
+ out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
2219
+ }
2220
+ const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
2221
+ if (schemaType) {
2222
+ const props = readableProps(values);
2223
+ if (schemaType === RELATED_LIST_TYPE) {
2224
+ const split = relatedListFieldRefs(props, path, PATH_SEP);
2225
+ out.push(...checkFieldRefs(split.related, split.relatedObject, objectFields, where));
2226
+ out.push(...checkFieldRefs(split.picker, split.pickerObject, objectFields, where));
2227
+ } else if (COMPONENT_FIELD_SPECS[schemaType]) {
2228
+ out.push(
2229
+ ...checkFieldRefs(
2230
+ componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
2231
+ objectName,
2232
+ objectFields,
2233
+ where
2234
+ )
2235
+ );
2236
+ }
2237
+ }
2238
+ return out;
2239
+ }
1631
2240
  function validateReactPageProps(stack) {
1632
2241
  const findings = [];
1633
2242
  const objectFields = indexObjectFields(stack);
1634
- const pages = asArray10(stack.pages);
2243
+ const searchTargets = indexObjectSearchTargets(stack);
2244
+ const pages = asArray12(stack.pages);
1635
2245
  for (let p = 0; p < pages.length; p++) {
1636
2246
  const page = pages[p];
1637
2247
  if (!page || page.kind !== "react") continue;
@@ -1661,7 +2271,10 @@ function validateReactPageProps(stack) {
1661
2271
  if (tsc.isJsxAttribute(a)) {
1662
2272
  const propName = a.name.getText(sf);
1663
2273
  used.add(propName);
1664
- values.set(propName, attrValue(tsc, sf, a));
2274
+ values.set(
2275
+ propName,
2276
+ FILTER_PROPS.has(propName) ? filterAttrValue(tsc, sf, a) : attrValue(tsc, sf, a)
2277
+ );
1665
2278
  }
1666
2279
  }
1667
2280
  const where = `page "${name}" \u203A <${tag}>`;
@@ -1696,6 +2309,23 @@ function validateReactPageProps(stack) {
1696
2309
  if (tag === "ObjectChart" && !hasSpread) {
1697
2310
  checkObjectChart({ values, where, path }, objectFields, findings);
1698
2311
  }
2312
+ if (tag === "ListView" && !hasSpread) {
2313
+ findings.push(
2314
+ ...checkSearchableFieldList(
2315
+ values.get("searchableFields"),
2316
+ strOf(values.get("objectName")),
2317
+ searchTargets,
2318
+ where,
2319
+ `${path} \u203A searchableFields`,
2320
+ "searchableFields"
2321
+ )
2322
+ );
2323
+ }
2324
+ if (!hasSpread) {
2325
+ findings.push(
2326
+ ...checkBlockFieldProps(tag, values, objectFields, where, path)
2327
+ );
2328
+ }
1699
2329
  }
1700
2330
  }
1701
2331
  tsc.forEachChild(node, visit);
@@ -1707,11 +2337,11 @@ function validateReactPageProps(stack) {
1707
2337
 
1708
2338
  // src/validate-page-source-styling.ts
1709
2339
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
1710
- var asArray11 = (v) => Array.isArray(v) ? v : [];
2340
+ var asArray13 = (v) => Array.isArray(v) ? v : [];
1711
2341
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
1712
2342
  function validatePageSourceStyling(stack) {
1713
2343
  const findings = [];
1714
- const pages = asArray11(stack.pages);
2344
+ const pages = asArray13(stack.pages);
1715
2345
  for (let p = 0; p < pages.length; p++) {
1716
2346
  const page = pages[p];
1717
2347
  if (!page) continue;
@@ -1740,7 +2370,7 @@ function validatePageSourceStyling(stack) {
1740
2370
  import { objectTitleCompleteness } from "@objectstack/spec/data";
1741
2371
  var TITLE_FORMAT_RETIRED = "title-format-retired";
1742
2372
  var TITLE_UNRESOLVABLE = "title-unresolvable";
1743
- function asArray12(v) {
2373
+ function asArray14(v) {
1744
2374
  if (Array.isArray(v)) return v;
1745
2375
  if (v && typeof v === "object") {
1746
2376
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1749,7 +2379,7 @@ function asArray12(v) {
1749
2379
  }
1750
2380
  function validateRecordTitle(stack) {
1751
2381
  const findings = [];
1752
- const objects = asArray12(stack.objects);
2382
+ const objects = asArray14(stack.objects);
1753
2383
  for (let i = 0; i < objects.length; i++) {
1754
2384
  const obj = objects[i];
1755
2385
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -1785,7 +2415,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
1785
2415
  var FIELD_GROUP_EMPTY = "field-group-empty";
1786
2416
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
1787
2417
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
1788
- function asArray13(v) {
2418
+ function asArray15(v) {
1789
2419
  if (Array.isArray(v)) return v;
1790
2420
  if (v && typeof v === "object") {
1791
2421
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1794,7 +2424,7 @@ function asArray13(v) {
1794
2424
  }
1795
2425
  function validateSemanticRoles(stack) {
1796
2426
  const findings = [];
1797
- const objects = asArray13(stack.objects);
2427
+ const objects = asArray15(stack.objects);
1798
2428
  for (let i = 0; i < objects.length; i++) {
1799
2429
  const obj = objects[i];
1800
2430
  if (!obj || typeof obj !== "object") continue;
@@ -1889,7 +2519,7 @@ function validateSemanticRoles(stack) {
1889
2519
  // src/validate-form-layout.ts
1890
2520
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
1891
2521
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
1892
- function asArray14(v) {
2522
+ function asArray16(v) {
1893
2523
  if (Array.isArray(v)) return v;
1894
2524
  if (v && typeof v === "object") {
1895
2525
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1914,13 +2544,13 @@ function boundObject(view) {
1914
2544
  function validateFormLayout(stack) {
1915
2545
  const findings = [];
1916
2546
  const objectFields = /* @__PURE__ */ new Map();
1917
- for (const obj of asArray14(stack.objects)) {
2547
+ for (const obj of asArray16(stack.objects)) {
1918
2548
  const name = typeof obj.name === "string" ? obj.name : void 0;
1919
2549
  if (!name) continue;
1920
2550
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
1921
2551
  objectFields.set(name, new Set(fields));
1922
2552
  }
1923
- const views = asArray14(stack.views);
2553
+ const views = asArray16(stack.views);
1924
2554
  for (let i = 0; i < views.length; i++) {
1925
2555
  const view = views[i];
1926
2556
  if (!view || typeof view !== "object") continue;
@@ -1970,7 +2600,7 @@ var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
1970
2600
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
1971
2601
  var CANONICAL = "visibleWhen";
1972
2602
  var ALIASES = ["visibleOn", "visibility"];
1973
- function asArray15(v) {
2603
+ function asArray17(v) {
1974
2604
  if (Array.isArray(v)) return v;
1975
2605
  if (v && typeof v === "object") {
1976
2606
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2032,7 +2662,7 @@ function isFieldObject(entry) {
2032
2662
  function validateVisibilityPredicates(stack, opts = {}) {
2033
2663
  const layer = opts.layer ?? "runtime";
2034
2664
  const findings = [];
2035
- const views = asArray15(stack.views);
2665
+ const views = asArray17(stack.views);
2036
2666
  for (let i = 0; i < views.length; i++) {
2037
2667
  const view = views[i];
2038
2668
  if (!view || typeof view !== "object") continue;
@@ -2055,7 +2685,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
2055
2685
  }
2056
2686
  }
2057
2687
  }
2058
- const pages = asArray15(stack.pages);
2688
+ const pages = asArray17(stack.pages);
2059
2689
  for (let i = 0; i < pages.length; i++) {
2060
2690
  const page = pages[i];
2061
2691
  if (!page || typeof page !== "object") continue;
@@ -2079,7 +2709,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
2079
2709
  // src/validate-capability-references.ts
2080
2710
  import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
2081
2711
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
2082
- function asArray16(v) {
2712
+ function asArray18(v) {
2083
2713
  if (Array.isArray(v)) return v;
2084
2714
  if (v && typeof v === "object") {
2085
2715
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2104,13 +2734,13 @@ function validateCapabilityReferences(stack) {
2104
2734
  const findings = [];
2105
2735
  if (!stack || typeof stack !== "object") return findings;
2106
2736
  const known = new Set(PLATFORM_CAPABILITY_NAMES);
2107
- for (const cap of asArray16(stack.capabilities)) {
2737
+ for (const cap of asArray18(stack.capabilities)) {
2108
2738
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
2109
2739
  }
2110
- for (const ps of asArray16(stack.permissions)) {
2740
+ for (const ps of asArray18(stack.permissions)) {
2111
2741
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
2112
2742
  }
2113
- for (const seed of asArray16(stack.data)) {
2743
+ for (const seed of asArray18(stack.data)) {
2114
2744
  if (seed.object !== "sys_capability") continue;
2115
2745
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
2116
2746
  const name = rec?.name;
@@ -2129,7 +2759,7 @@ function validateCapabilityReferences(stack) {
2129
2759
  hint
2130
2760
  });
2131
2761
  };
2132
- const objects = asArray16(stack.objects);
2762
+ const objects = asArray18(stack.objects);
2133
2763
  for (let i = 0; i < objects.length; i++) {
2134
2764
  const obj = objects[i];
2135
2765
  if (!obj || typeof obj !== "object") continue;
@@ -2138,27 +2768,27 @@ function validateCapabilityReferences(stack) {
2138
2768
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
2139
2769
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
2140
2770
  }
2141
- const fields = asArray16(obj.fields);
2771
+ const fields = asArray18(obj.fields);
2142
2772
  for (const f of fields) {
2143
2773
  const fname = typeof f.name === "string" ? f.name : "(field)";
2144
2774
  for (const cap of asCapArray(f.requiredPermissions)) {
2145
2775
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
2146
2776
  }
2147
2777
  }
2148
- for (const [ai, action] of asArray16(obj.actions).entries()) {
2778
+ for (const [ai, action] of asArray18(obj.actions).entries()) {
2149
2779
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
2150
2780
  for (const cap of asCapArray(action.requiredPermissions)) {
2151
2781
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
2152
2782
  }
2153
2783
  }
2154
2784
  }
2155
- for (const [i, action] of asArray16(stack.actions).entries()) {
2785
+ for (const [i, action] of asArray18(stack.actions).entries()) {
2156
2786
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
2157
2787
  for (const cap of asCapArray(action.requiredPermissions)) {
2158
2788
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
2159
2789
  }
2160
2790
  }
2161
- const apps = asArray16(stack.apps);
2791
+ const apps = asArray18(stack.apps);
2162
2792
  for (let i = 0; i < apps.length; i++) {
2163
2793
  const app = apps[i];
2164
2794
  if (!app || typeof app !== "object") continue;
@@ -2215,7 +2845,7 @@ var TYPE_FIX = {
2215
2845
  business_unit: "department",
2216
2846
  bu: "department"
2217
2847
  };
2218
- function asArray17(v) {
2848
+ function asArray19(v) {
2219
2849
  if (Array.isArray(v)) return v;
2220
2850
  if (v && typeof v === "object") {
2221
2851
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2225,15 +2855,15 @@ function asArray17(v) {
2225
2855
  function validateApprovalApprovers(stack) {
2226
2856
  const findings = [];
2227
2857
  if (!stack || typeof stack !== "object") return findings;
2228
- const flows = asArray17(stack.flows);
2858
+ const flows = asArray19(stack.flows);
2229
2859
  const validTypes = new Set(ApproverType.options);
2230
2860
  for (let fi = 0; fi < flows.length; fi++) {
2231
2861
  const flow = flows[fi];
2232
2862
  if (!flow || typeof flow !== "object") continue;
2233
2863
  const flowName = typeof flow.name === "string" ? flow.name : `(flow ${fi})`;
2234
- const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
2235
- for (let ni = 0; ni < nodes.length; ni++) {
2236
- const node = nodes[ni];
2864
+ const walked = walkFlowNodes(flow, `flows[${fi}]`);
2865
+ for (let ni = 0; ni < walked.length; ni++) {
2866
+ const { node, path: nodePath } = walked[ni];
2237
2867
  if (!node || node.type !== APPROVAL_NODE_TYPE) continue;
2238
2868
  const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
2239
2869
  const cfg = node.config ?? {};
@@ -2244,7 +2874,7 @@ function validateApprovalApprovers(stack) {
2244
2874
  if (!a || typeof a !== "object") continue;
2245
2875
  const type = typeof a.type === "string" ? a.type : "";
2246
2876
  const value = typeof a.value === "string" ? a.value : "";
2247
- const path = `flows[${fi}].nodes[${ni}].config.approvers[${ai}]`;
2877
+ const path = `${nodePath}.config.approvers[${ai}]`;
2248
2878
  if (type && !validTypes.has(type)) {
2249
2879
  const fix = TYPE_FIX[type];
2250
2880
  findings.push({
@@ -2355,7 +2985,7 @@ function validateApprovalApprovers(stack) {
2355
2985
  severity: "info",
2356
2986
  rule: APPROVAL_APPROVERS_MAY_RESOLVE_EMPTY,
2357
2987
  where,
2358
- path: `flows[${fi}].nodes[${ni}].config.approvers`,
2988
+ path: `${nodePath}.config.approvers`,
2359
2989
  message: `every approver on this node routes to a group (position/team/department) whose members are runtime data \u2014 if none is staffed, the request resolves to an empty slate and waits forever` + (locks ? `, and (lockRecord) the record stays locked with no in-product recovery.` : `.`),
2360
2990
  hint: `Make sure at least one target is always staffed, or add a guaranteed-staffed fallback approver, e.g. { type: 'org_membership_level', value: 'owner' }. A request that still lands empty is recoverable only by a platform/tenant admin override (#3424).`
2361
2991
  });
@@ -2368,7 +2998,7 @@ function validateApprovalApprovers(stack) {
2368
2998
  severity: "info",
2369
2999
  rule: APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
2370
3000
  where,
2371
- path: `flows[${fi}].nodes[${ni}].config`,
3001
+ path: `${nodePath}.config`,
2372
3002
  message: `this node resolves approvers from an expression but declares no onEmptyApprovers \u2014 an empty result falls back to the default ('admin_rescue': request opens, only a privileged admin can act).`,
2373
3003
  hint: `Declare the empty-slate policy explicitly: onEmptyApprovers: 'admin_rescue' (hold for admin takeover), 'fail' (fail the node \u2014 config bug), or 'auto_approve' (wave through, output.autoApproved = true).`
2374
3004
  });
@@ -2380,7 +3010,7 @@ function validateApprovalApprovers(stack) {
2380
3010
  severity: "error",
2381
3011
  rule: APPROVAL_DECISION_OUTPUTS_RESERVED,
2382
3012
  where,
2383
- path: `flows[${fi}].nodes[${ni}].config.decisionOutputs`,
3013
+ path: `${nodePath}.config.decisionOutputs`,
2384
3014
  message: `decisionOutputs declares reserved key(s) \`${reserved.join("`, `")}\` \u2014 the resume envelope owns them, so every decide carrying them is rejected.`,
2385
3015
  hint: `Rename the output key(s); any name other than 'decision'/'requestId' works.`
2386
3016
  });
@@ -2393,7 +3023,7 @@ function validateApprovalApprovers(stack) {
2393
3023
  severity: "warning",
2394
3024
  rule: APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
2395
3025
  where,
2396
- path: `flows[${fi}].nodes[${ni}].config.escalation.escalateTo`,
3026
+ path: `${nodePath}.config.escalation.escalateTo`,
2397
3027
  message: `escalation.action is 'reassign' but escalateTo is empty \u2014 at runtime the escalation degrades to a notify and the request stays with the original approvers.`,
2398
3028
  hint: `Set escalateTo to a position machine name (expanded via sys_user_position, ADR-0090 D3) or a specific user id, or change action to 'notify'.`
2399
3029
  });
@@ -2520,7 +3150,7 @@ var OWD_WIDTH = {
2520
3150
  public_read: 1,
2521
3151
  public_read_write: 2
2522
3152
  };
2523
- function asArray18(v) {
3153
+ function asArray20(v) {
2524
3154
  if (Array.isArray(v)) return v;
2525
3155
  if (v && typeof v === "object") {
2526
3156
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2546,7 +3176,7 @@ function refOf(def) {
2546
3176
  return typeof r === "string" && r ? r : void 0;
2547
3177
  }
2548
3178
  function firstMasterDetailField(obj) {
2549
- for (const f of asArray18(obj.fields)) {
3179
+ for (const f of asArray20(obj.fields)) {
2550
3180
  if (f.type === "master_detail") {
2551
3181
  return { name: String(f.name ?? "?"), parent: refOf(f) };
2552
3182
  }
@@ -2559,8 +3189,8 @@ function grantsObjectAccess(p) {
2559
3189
  function validateSecurityPosture(stack, opts) {
2560
3190
  const findings = [];
2561
3191
  if (!stack || typeof stack !== "object") return findings;
2562
- const objects = asArray18(stack.objects);
2563
- const permissionSets = asArray18(stack.permissions);
3192
+ const objects = asArray20(stack.objects);
3193
+ const permissionSets = asArray20(stack.permissions);
2564
3194
  for (let i = 0; i < objects.length; i++) {
2565
3195
  const obj = objects[i];
2566
3196
  if (!obj || typeof obj !== "object") continue;
@@ -2689,10 +3319,10 @@ function validateSecurityPosture(stack, opts) {
2689
3319
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
2690
3320
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
2691
3321
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
2692
- for (const f of asArray18(obj.fields)) {
3322
+ for (const f of asArray20(obj.fields)) {
2693
3323
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
2694
3324
  }
2695
- for (const [ai, action] of asArray18(obj.actions).entries()) {
3325
+ for (const [ai, action] of asArray20(obj.actions).entries()) {
2696
3326
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
2697
3327
  }
2698
3328
  }
@@ -2701,19 +3331,19 @@ function validateSecurityPosture(stack, opts) {
2701
3331
  if (!ps || typeof ps !== "object") continue;
2702
3332
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
2703
3333
  }
2704
- for (const [i, pos] of asArray18(stack.positions).entries()) {
3334
+ for (const [i, pos] of asArray20(stack.positions).entries()) {
2705
3335
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
2706
3336
  }
2707
- for (const [i, app] of asArray18(stack.apps).entries()) {
3337
+ for (const [i, app] of asArray20(stack.apps).entries()) {
2708
3338
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
2709
3339
  }
2710
- for (const [i, book] of asArray18(stack.books).entries()) {
3340
+ for (const [i, book] of asArray20(stack.books).entries()) {
2711
3341
  flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
2712
3342
  }
2713
3343
  const stackSetNames = new Set(
2714
3344
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
2715
3345
  );
2716
- for (const [i, book] of asArray18(stack.books).entries()) {
3346
+ for (const [i, book] of asArray20(stack.books).entries()) {
2717
3347
  const audience = book.audience;
2718
3348
  if (!audience || typeof audience !== "object") continue;
2719
3349
  const setName = audience.permissionSet;
@@ -2791,7 +3421,7 @@ function validateSecurityPosture(stack, opts) {
2791
3421
  }
2792
3422
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
2793
3423
  const nowMs = opts?.nowMs ?? Date.now();
2794
- for (const [i, seed] of asArray18(stack.data).entries()) {
3424
+ for (const [i, seed] of asArray20(stack.data).entries()) {
2795
3425
  const seedObject = typeof seed.object === "string" ? seed.object : "";
2796
3426
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
2797
3427
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -2835,7 +3465,7 @@ function validateSecurityPosture(stack, opts) {
2835
3465
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
2836
3466
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
2837
3467
  var ORG_PARENT_FIELD = "parent_organization_id";
2838
- function asArray19(v) {
3468
+ function asArray21(v) {
2839
3469
  if (Array.isArray(v)) return v;
2840
3470
  if (v && typeof v === "object") {
2841
3471
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2856,9 +3486,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
2856
3486
  function validateOrgAxisRedLines(stack) {
2857
3487
  const findings = [];
2858
3488
  const cfg = stack ?? {};
2859
- const permissionSets = asArray19(cfg.permissions ?? cfg.permissionSets);
3489
+ const permissionSets = asArray21(cfg.permissions ?? cfg.permissionSets);
2860
3490
  permissionSets.forEach((ps, psIndex) => {
2861
- asArray19(ps.rowLevelSecurity).forEach((policy, pIndex) => {
3491
+ asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
2862
3492
  for (const clause of ["using", "check"]) {
2863
3493
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
2864
3494
  findings.push({
@@ -2872,10 +3502,10 @@ function validateOrgAxisRedLines(stack) {
2872
3502
  }
2873
3503
  });
2874
3504
  });
2875
- const objects = asArray19(cfg.objects);
3505
+ const objects = asArray21(cfg.objects);
2876
3506
  objects.forEach((object, oIndex) => {
2877
3507
  const objectName = str(object.name) || String(oIndex);
2878
- asArray19(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
3508
+ asArray21(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
2879
3509
  for (const clause of ["using", "check"]) {
2880
3510
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
2881
3511
  findings.push({
@@ -2889,7 +3519,7 @@ function validateOrgAxisRedLines(stack) {
2889
3519
  }
2890
3520
  });
2891
3521
  });
2892
- asArray19(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
3522
+ asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
2893
3523
  const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
2894
3524
  const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
2895
3525
  if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
@@ -2906,7 +3536,7 @@ function validateOrgAxisRedLines(stack) {
2906
3536
  const tenancyDisabledObjects = new Set(
2907
3537
  objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
2908
3538
  );
2909
- asArray19(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
3539
+ asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
2910
3540
  const target = str(rule.object ?? rule.objectName);
2911
3541
  if (!target || !tenancyDisabledObjects.has(target)) return;
2912
3542
  const sharedTo = rule.sharedTo ?? rule.recipient;
@@ -2927,14 +3557,14 @@ function validateOrgAxisRedLines(stack) {
2927
3557
  // src/validate-dashboard-action-refs.ts
2928
3558
  var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
2929
3559
  var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
2930
- function asArray20(v) {
3560
+ function asArray22(v) {
2931
3561
  if (Array.isArray(v)) return v;
2932
3562
  if (v && typeof v === "object") {
2933
3563
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
2934
3564
  }
2935
3565
  return [];
2936
3566
  }
2937
- function strName(v) {
3567
+ function strName5(v) {
2938
3568
  return typeof v === "string" && v.length > 0 ? v : void 0;
2939
3569
  }
2940
3570
  var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
@@ -2951,7 +3581,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
2951
3581
  views: "views"
2952
3582
  };
2953
3583
  function viewContainerName(item) {
2954
- return strName(item.name) ?? strName(item.id) ?? strName(item.object) ?? strName(item.list?.data && item.list.data.object) ?? strName(item.form?.data && item.form.data.object);
3584
+ return strName5(item.name) ?? strName5(item.id) ?? strName5(item.object) ?? strName5(item.list?.data && item.list.data.object) ?? strName5(item.form?.data && item.form.data.object);
2955
3585
  }
2956
3586
  function collectKnownTargets(stack) {
2957
3587
  const actions = /* @__PURE__ */ new Set();
@@ -2961,22 +3591,22 @@ function collectKnownTargets(stack) {
2961
3591
  const pages = /* @__PURE__ */ new Set();
2962
3592
  const views = /* @__PURE__ */ new Set();
2963
3593
  const collectNames = (v, into, name) => {
2964
- for (const item of asArray20(v)) {
3594
+ for (const item of asArray22(v)) {
2965
3595
  if (!item || typeof item !== "object") continue;
2966
3596
  const n = name(item);
2967
3597
  if (n) into.add(n);
2968
3598
  }
2969
3599
  };
2970
- collectNames(stack.actions, actions, (a) => strName(a.name));
2971
- for (const obj of asArray20(stack.objects)) {
3600
+ collectNames(stack.actions, actions, (a) => strName5(a.name));
3601
+ for (const obj of asArray22(stack.objects)) {
2972
3602
  if (!obj || typeof obj !== "object") continue;
2973
- const n = strName(obj.name);
3603
+ const n = strName5(obj.name);
2974
3604
  if (n) objects.add(n);
2975
- collectNames(obj.actions, actions, (a) => strName(a.name));
3605
+ collectNames(obj.actions, actions, (a) => strName5(a.name));
2976
3606
  }
2977
- collectNames(stack.reports, reports, (r) => strName(r.name));
2978
- collectNames(stack.dashboards, dashboards, (d) => strName(d.name));
2979
- collectNames(stack.pages, pages, (p) => strName(p.name));
3607
+ collectNames(stack.reports, reports, (r) => strName5(r.name));
3608
+ collectNames(stack.dashboards, dashboards, (d) => strName5(d.name));
3609
+ collectNames(stack.pages, pages, (p) => strName5(p.name));
2980
3610
  collectNames(stack.views, views, viewContainerName);
2981
3611
  for (const o of objects) views.add(o);
2982
3612
  return { actions, objects, reports, dashboards, pages, views };
@@ -3008,14 +3638,14 @@ function resolveUrlRoute(target, known) {
3008
3638
  function validateDashboardActionRefs(stack) {
3009
3639
  const findings = [];
3010
3640
  if (!stack || typeof stack !== "object") return findings;
3011
- const dashboards = asArray20(stack.dashboards);
3641
+ const dashboards = asArray22(stack.dashboards);
3012
3642
  if (dashboards.length === 0) return findings;
3013
3643
  const known = collectKnownTargets(stack);
3014
3644
  const checkOne = (action, where, path) => {
3015
- const target = strName(action.actionUrl);
3645
+ const target = strName5(action.actionUrl);
3016
3646
  if (!target) return;
3017
3647
  if (target.includes("${")) return;
3018
- const actionType = strName(action.actionType) ?? "url";
3648
+ const actionType = strName5(action.actionType) ?? "url";
3019
3649
  if (actionType === "script" || actionType === "modal") {
3020
3650
  if (resolveActionTarget(actionType, target, known)) return;
3021
3651
  const kindWord = actionType === "script" ? "script" : "modal";
@@ -3046,25 +3676,25 @@ function validateDashboardActionRefs(stack) {
3046
3676
  for (let di = 0; di < dashboards.length; di++) {
3047
3677
  const dash = dashboards[di];
3048
3678
  if (!dash || typeof dash !== "object") continue;
3049
- const dashName = strName(dash.name) ?? `(dashboard ${di})`;
3679
+ const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
3050
3680
  const dashPath = `dashboards[${di}]`;
3051
- const headerActions = asArray20(dash.header?.actions);
3681
+ const headerActions = asArray22(dash.header?.actions);
3052
3682
  for (let ai = 0; ai < headerActions.length; ai++) {
3053
3683
  const action = headerActions[ai];
3054
3684
  if (!action || typeof action !== "object") continue;
3055
- const label2 = strName(action.label) ?? strName(action.actionUrl) ?? `#${ai}`;
3685
+ const label2 = strName5(action.label) ?? strName5(action.actionUrl) ?? `#${ai}`;
3056
3686
  checkOne(
3057
3687
  action,
3058
3688
  `dashboard "${dashName}" \xB7 header action "${label2}"`,
3059
3689
  `${dashPath}.header.actions[${ai}].actionUrl`
3060
3690
  );
3061
3691
  }
3062
- const widgets = asArray20(dash.widgets);
3692
+ const widgets = asArray22(dash.widgets);
3063
3693
  for (let wi = 0; wi < widgets.length; wi++) {
3064
3694
  const widget = widgets[wi];
3065
3695
  if (!widget || typeof widget !== "object") continue;
3066
- if (!strName(widget.actionUrl)) continue;
3067
- const widgetId = strName(widget.id) ?? `#${wi}`;
3696
+ if (!strName5(widget.actionUrl)) continue;
3697
+ const widgetId = strName5(widget.id) ?? `#${wi}`;
3068
3698
  checkOne(
3069
3699
  { actionType: widget.actionType, actionUrl: widget.actionUrl },
3070
3700
  `dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
@@ -3079,7 +3709,7 @@ function validateDashboardActionRefs(stack) {
3079
3709
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
3080
3710
  var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
3081
3711
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
3082
- function asArray21(v) {
3712
+ function asArray23(v) {
3083
3713
  if (Array.isArray(v)) return v;
3084
3714
  if (v && typeof v === "object") {
3085
3715
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -3148,7 +3778,7 @@ function validateFilterTokens(stack) {
3148
3778
  ["apps", "app"]
3149
3779
  ];
3150
3780
  for (const [key, kind] of surfaces) {
3151
- const items = asArray21(stack[key]);
3781
+ const items = asArray23(stack[key]);
3152
3782
  items.forEach((item, i) => {
3153
3783
  const name = label(item.name ?? item.id, `#${i}`);
3154
3784
  if (kind === "dashboard") {
@@ -3182,14 +3812,14 @@ import {
3182
3812
  var PLATFORM_NAMES = [...PLATFORM_PROVIDED_OBJECT_NAMES];
3183
3813
  var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
3184
3814
  var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
3185
- function asArray22(v) {
3815
+ function asArray24(v) {
3186
3816
  if (Array.isArray(v)) return v;
3187
3817
  if (v && typeof v === "object") {
3188
3818
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3189
3819
  }
3190
3820
  return [];
3191
3821
  }
3192
- function strName2(v) {
3822
+ function strName6(v) {
3193
3823
  return typeof v === "string" && v.length > 0 ? v : void 0;
3194
3824
  }
3195
3825
  function isInterpolated(target) {
@@ -3197,11 +3827,11 @@ function isInterpolated(target) {
3197
3827
  const open = target.indexOf("{");
3198
3828
  return open !== -1 && target.indexOf("}", open + 2) !== -1;
3199
3829
  }
3200
- function suggest2(target, known) {
3830
+ function suggest3(target, known) {
3201
3831
  let best;
3202
3832
  let bestScore = Infinity;
3203
3833
  for (const candidate of known) {
3204
- const d = distance(target, candidate);
3834
+ const d = distance2(target, candidate);
3205
3835
  if (d < bestScore) {
3206
3836
  bestScore = d;
3207
3837
  best = candidate;
@@ -3210,7 +3840,7 @@ function suggest2(target, known) {
3210
3840
  const limit = Math.max(2, Math.floor(target.length / 3));
3211
3841
  return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
3212
3842
  }
3213
- function distance(a, b) {
3843
+ function distance2(a, b) {
3214
3844
  const m = a.length;
3215
3845
  const n = b.length;
3216
3846
  if (m === 0) return n;
@@ -3229,14 +3859,14 @@ function distance(a, b) {
3229
3859
  function validateObjectReferences(stack) {
3230
3860
  const findings = [];
3231
3861
  if (!stack || typeof stack !== "object") return findings;
3232
- const objects = asArray22(stack.objects);
3862
+ const objects = asArray24(stack.objects);
3233
3863
  const ownObjects = /* @__PURE__ */ new Set();
3234
3864
  for (const obj of objects) {
3235
- const n = strName2(obj.name);
3865
+ const n = strName6(obj.name);
3236
3866
  if (n) ownObjects.add(n);
3237
3867
  }
3238
3868
  const check = (target, where, path, subject, fix) => {
3239
- const name = strName2(target);
3869
+ const name = strName6(target);
3240
3870
  if (!name) return;
3241
3871
  if (isInterpolated(name)) return;
3242
3872
  if (ownObjects.has(name)) return;
@@ -3247,7 +3877,7 @@ function validateObjectReferences(stack) {
3247
3877
  rule: OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
3248
3878
  where,
3249
3879
  path,
3250
- message: `${subject} "${name}" carries a platform namespace prefix, but no platform package, official plugin, or cloud runtime object registers that name \u2014 and this stack does not define it either. If nothing provides it at runtime the reference resolves to nothing and fails silently.` + suggest2(name, PLATFORM_NAMES),
3880
+ message: `${subject} "${name}" carries a platform namespace prefix, but no platform package, official plugin, or cloud runtime object registers that name \u2014 and this stack does not define it either. If nothing provides it at runtime the reference resolves to nothing and fails silently.` + suggest3(name, PLATFORM_NAMES),
3251
3881
  hint: `Check the spelling against the object the providing package actually registers (e.g. "sys_approval_request", not "sys_approval_process" \u2014 the process object was removed when approval became a flow node, ADR-0019). If a third-party package genuinely provides it, this warning is expected. ${fix}`
3252
3882
  });
3253
3883
  return;
@@ -3257,26 +3887,26 @@ function validateObjectReferences(stack) {
3257
3887
  rule: OBJECT_REFERENCE_UNKNOWN,
3258
3888
  where,
3259
3889
  path,
3260
- message: `${subject} "${name}" resolves to no object defined in this stack. The reference is inert at runtime \u2014 nothing reports the miss.` + suggest2(name, ownObjects),
3890
+ message: `${subject} "${name}" resolves to no object defined in this stack. The reference is inert at runtime \u2014 nothing reports the miss.` + suggest3(name, ownObjects),
3261
3891
  hint: `Point it at one of this stack's objects, or at a platform object by its full name (the platform user object is "sys_user", not "user"). ${fix}` + (ownObjects.size > 0 ? ` Defined objects: ${[...ownObjects].sort().join(", ")}.` : "")
3262
3892
  });
3263
3893
  };
3264
3894
  const checkActionParams2 = (action, actionPath, actionLabel) => {
3265
- const params = asArray22(action.params);
3895
+ const params = asArray24(action.params);
3266
3896
  for (let pi = 0; pi < params.length; pi++) {
3267
3897
  const param = params[pi];
3268
3898
  if (!param || typeof param !== "object") continue;
3269
- const paramLabel = strName2(param.name) ?? strName2(param.field) ?? `#${pi}`;
3899
+ const paramLabel = strName6(param.name) ?? strName6(param.field) ?? `#${pi}`;
3270
3900
  const where = `${actionLabel} \xB7 param "${paramLabel}"`;
3271
3901
  check(
3272
- strName2(param.reference),
3902
+ strName6(param.reference),
3273
3903
  where,
3274
3904
  `${actionPath}.params[${pi}].reference`,
3275
3905
  "record-picker target",
3276
3906
  "Without a resolvable target the picker degrades to a raw record-id text input."
3277
3907
  );
3278
3908
  check(
3279
- strName2(param.objectOverride),
3909
+ strName6(param.objectOverride),
3280
3910
  where,
3281
3911
  `${actionPath}.params[${pi}].objectOverride`,
3282
3912
  "field-backed param object",
@@ -3284,70 +3914,70 @@ function validateObjectReferences(stack) {
3284
3914
  );
3285
3915
  }
3286
3916
  };
3287
- const globalActions = asArray22(stack.actions);
3917
+ const globalActions = asArray24(stack.actions);
3288
3918
  for (let ai = 0; ai < globalActions.length; ai++) {
3289
3919
  const action = globalActions[ai];
3290
3920
  if (!action || typeof action !== "object") continue;
3291
- checkActionParams2(action, `actions[${ai}]`, `action "${strName2(action.name) ?? `#${ai}`}"`);
3921
+ checkActionParams2(action, `actions[${ai}]`, `action "${strName6(action.name) ?? `#${ai}`}"`);
3292
3922
  }
3293
3923
  for (let oi = 0; oi < objects.length; oi++) {
3294
3924
  const obj = objects[oi];
3295
3925
  if (!obj || typeof obj !== "object") continue;
3296
- const objName = strName2(obj.name) ?? `#${oi}`;
3297
- const objActions = asArray22(obj.actions);
3926
+ const objName = strName6(obj.name) ?? `#${oi}`;
3927
+ const objActions = asArray24(obj.actions);
3298
3928
  for (let ai = 0; ai < objActions.length; ai++) {
3299
3929
  const action = objActions[ai];
3300
3930
  if (!action || typeof action !== "object") continue;
3301
3931
  checkActionParams2(
3302
3932
  action,
3303
3933
  `objects[${oi}].actions[${ai}]`,
3304
- `object "${objName}" \xB7 action "${strName2(action.name) ?? `#${ai}`}"`
3934
+ `object "${objName}" \xB7 action "${strName6(action.name) ?? `#${ai}`}"`
3305
3935
  );
3306
3936
  }
3307
3937
  }
3308
- const dashboards = asArray22(stack.dashboards);
3938
+ const dashboards = asArray24(stack.dashboards);
3309
3939
  for (let di = 0; di < dashboards.length; di++) {
3310
3940
  const dash = dashboards[di];
3311
3941
  if (!dash || typeof dash !== "object") continue;
3312
- const dashName = strName2(dash.name) ?? `#${di}`;
3313
- const filters = asArray22(dash.globalFilters);
3942
+ const dashName = strName6(dash.name) ?? `#${di}`;
3943
+ const filters = asArray24(dash.globalFilters);
3314
3944
  for (let fi = 0; fi < filters.length; fi++) {
3315
3945
  const filter = filters[fi];
3316
3946
  if (!filter || typeof filter !== "object") continue;
3317
3947
  const optionsFrom = filter.optionsFrom;
3318
3948
  if (!optionsFrom || typeof optionsFrom !== "object") continue;
3319
3949
  check(
3320
- strName2(optionsFrom.object),
3321
- `dashboard "${dashName}" \xB7 filter "${strName2(filter.name) ?? `#${fi}`}"`,
3950
+ strName6(optionsFrom.object),
3951
+ `dashboard "${dashName}" \xB7 filter "${strName6(filter.name) ?? `#${fi}`}"`,
3322
3952
  `dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
3323
3953
  "filter options source",
3324
3954
  "The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
3325
3955
  );
3326
3956
  }
3327
3957
  }
3328
- const apps = asArray22(stack.apps);
3958
+ const apps = asArray24(stack.apps);
3329
3959
  for (let ai = 0; ai < apps.length; ai++) {
3330
3960
  const app = apps[ai];
3331
3961
  if (!app || typeof app !== "object") continue;
3332
- const appName = strName2(app.name) ?? `#${ai}`;
3962
+ const appName = strName6(app.name) ?? `#${ai}`;
3333
3963
  const walkNav = (items, basePath) => {
3334
- const navItems = asArray22(items);
3964
+ const navItems = asArray24(items);
3335
3965
  for (let ni = 0; ni < navItems.length; ni++) {
3336
3966
  const nav = navItems[ni];
3337
3967
  if (!nav || typeof nav !== "object") continue;
3338
- const navId = strName2(nav.id) ?? `#${ni}`;
3968
+ const navId = strName6(nav.id) ?? `#${ni}`;
3339
3969
  const where = `app "${appName}" \xB7 nav "${navId}"`;
3340
3970
  const navPath = `${basePath}[${ni}]`;
3341
3971
  check(
3342
- strName2(nav.requiresObject),
3972
+ strName6(nav.requiresObject),
3343
3973
  where,
3344
3974
  `${navPath}.requiresObject`,
3345
3975
  "capability gate object",
3346
3976
  "The entry is hidden unless this object is registered, so a typo hides it permanently \u2014 and it suppresses the nav cross-reference check that would have caught the target."
3347
3977
  );
3348
- if (nav.requiresObject && strName2(nav.objectName)) {
3978
+ if (nav.requiresObject && strName6(nav.objectName)) {
3349
3979
  check(
3350
- strName2(nav.objectName),
3980
+ strName6(nav.objectName),
3351
3981
  where,
3352
3982
  `${navPath}.objectName`,
3353
3983
  "navigation target",
@@ -3358,7 +3988,7 @@ function validateObjectReferences(stack) {
3358
3988
  }
3359
3989
  };
3360
3990
  walkNav(app.navigation, `apps[${ai}].navigation`);
3361
- const areas = asArray22(app.areas);
3991
+ const areas = asArray24(app.areas);
3362
3992
  for (let ri = 0; ri < areas.length; ri++) {
3363
3993
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
3364
3994
  }
@@ -3366,88 +3996,22 @@ function validateObjectReferences(stack) {
3366
3996
  return findings;
3367
3997
  }
3368
3998
 
3369
- // src/page-walk.ts
3370
- function isRec2(v) {
3371
- return !!v && typeof v === "object" && !Array.isArray(v);
3372
- }
3373
- function strName3(v) {
3374
- return typeof v === "string" && v.length > 0 ? v : void 0;
3375
- }
3376
- var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
3377
- function isSourceAuthoredPage(page) {
3378
- const kind = strName3(page.kind);
3379
- return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
3380
- }
3381
- function walkPageComponents(page, pagePath) {
3382
- const out = [];
3383
- if (!isRec2(page) || isSourceAuthoredPage(page)) return out;
3384
- const pageObject = strName3(page.object);
3385
- const visit = (node, path, inheritedObject) => {
3386
- if (!isRec2(node)) return;
3387
- const props = isRec2(node.properties) ? node.properties : void 0;
3388
- const dataSource = isRec2(node.dataSource) ? node.dataSource : void 0;
3389
- const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
3390
- out.push({ component: node, path, objectName });
3391
- if (!props) return;
3392
- if (Array.isArray(props.items)) {
3393
- for (let i = 0; i < props.items.length; i++) {
3394
- const item = props.items[i];
3395
- if (!isRec2(item) || !Array.isArray(item.children)) continue;
3396
- for (let c = 0; c < item.children.length; c++) {
3397
- visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
3398
- }
3399
- }
3400
- }
3401
- if (Array.isArray(props.children)) {
3402
- for (let i = 0; i < props.children.length; i++) {
3403
- visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
3404
- }
3405
- }
3406
- for (const key of ["body", "footer"]) {
3407
- const slotList = props[key];
3408
- if (!Array.isArray(slotList)) continue;
3409
- for (let i = 0; i < slotList.length; i++) {
3410
- visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
3411
- }
3412
- }
3413
- };
3414
- const regions = Array.isArray(page.regions) ? page.regions : [];
3415
- for (let r = 0; r < regions.length; r++) {
3416
- const region = regions[r];
3417
- if (!isRec2(region) || !Array.isArray(region.components)) continue;
3418
- for (let c = 0; c < region.components.length; c++) {
3419
- visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
3420
- }
3421
- }
3422
- const slots = isRec2(page.slots) ? page.slots : void 0;
3423
- if (slots) {
3424
- for (const [slot, value] of Object.entries(slots)) {
3425
- const list3 = Array.isArray(value) ? value : [value];
3426
- const indexed = Array.isArray(value);
3427
- for (let i = 0; i < list3.length; i++) {
3428
- visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
3429
- }
3430
- }
3431
- }
3432
- return out;
3433
- }
3434
-
3435
3999
  // src/validate-action-name-refs.ts
3436
4000
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
3437
- function asArray23(v) {
4001
+ function asArray25(v) {
3438
4002
  if (Array.isArray(v)) return v;
3439
4003
  if (v && typeof v === "object") {
3440
4004
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3441
4005
  }
3442
4006
  return [];
3443
4007
  }
3444
- function strName4(v) {
4008
+ function strName7(v) {
3445
4009
  return typeof v === "string" && v.length > 0 ? v : void 0;
3446
4010
  }
3447
4011
  function strList(v) {
3448
4012
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
3449
4013
  }
3450
- function distance2(a, b) {
4014
+ function distance3(a, b) {
3451
4015
  const m = a.length;
3452
4016
  const n = b.length;
3453
4017
  if (m === 0) return n;
@@ -3463,11 +4027,11 @@ function distance2(a, b) {
3463
4027
  }
3464
4028
  return prev[n];
3465
4029
  }
3466
- function suggest3(target, known) {
4030
+ function suggest4(target, known) {
3467
4031
  let best;
3468
4032
  let bestScore = Infinity;
3469
4033
  for (const candidate of known) {
3470
- const d = distance2(target, candidate);
4034
+ const d = distance3(target, candidate);
3471
4035
  if (d < bestScore) {
3472
4036
  bestScore = d;
3473
4037
  best = candidate;
@@ -3478,14 +4042,14 @@ function suggest3(target, known) {
3478
4042
  }
3479
4043
  function collectActionNames(stack) {
3480
4044
  const names = /* @__PURE__ */ new Set();
3481
- for (const action of asArray23(stack.actions)) {
3482
- const n = strName4(action?.name);
4045
+ for (const action of asArray25(stack.actions)) {
4046
+ const n = strName7(action?.name);
3483
4047
  if (n) names.add(n);
3484
4048
  }
3485
- for (const obj of asArray23(stack.objects)) {
4049
+ for (const obj of asArray25(stack.objects)) {
3486
4050
  if (!obj || typeof obj !== "object") continue;
3487
- for (const action of asArray23(obj.actions)) {
3488
- const n = strName4(action?.name);
4051
+ for (const action of asArray25(obj.actions)) {
4052
+ const n = strName7(action?.name);
3489
4053
  if (n) names.add(n);
3490
4054
  }
3491
4055
  }
@@ -3502,257 +4066,85 @@ function validateActionNameRefs(stack) {
3502
4066
  rule: ACTION_NAME_UNDEFINED,
3503
4067
  where,
3504
4068
  path,
3505
- message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest3(name, known),
4069
+ message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
3506
4070
  hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
3507
4071
  });
3508
4072
  };
3509
- const views = asArray23(stack.views);
4073
+ const views = asArray25(stack.views);
3510
4074
  for (let vi = 0; vi < views.length; vi++) {
3511
4075
  const view = views[vi];
3512
4076
  if (!view || typeof view !== "object") continue;
3513
- const viewName = strName4(view.name) ?? strName4(view.object) ?? `#${vi}`;
4077
+ const viewName = strName7(view.name) ?? strName7(view.object) ?? `#${vi}`;
3514
4078
  const checkListContainer = (container, label2, path) => {
3515
4079
  if (!container || typeof container !== "object") return;
3516
4080
  const list3 = container;
3517
4081
  for (const key of ["rowActions", "bulkActions"]) {
3518
4082
  const names = strList(list3[key]);
3519
4083
  for (let ai = 0; ai < names.length; ai++) {
3520
- check(
3521
- names[ai],
3522
- `view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
3523
- `${path}.${key}[${ai}]`,
3524
- key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
3525
- );
3526
- }
3527
- }
3528
- };
3529
- checkListContainer(view.list, "list", `views[${vi}].list`);
3530
- const listViews = view.listViews;
3531
- if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
3532
- for (const [key, lv] of Object.entries(listViews)) {
3533
- checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
3534
- }
3535
- }
3536
- }
3537
- const pages = asArray23(stack.pages);
3538
- for (let pi = 0; pi < pages.length; pi++) {
3539
- const page = pages[pi];
3540
- if (!page || typeof page !== "object") continue;
3541
- const pageName = strName4(page.name) ?? `#${pi}`;
3542
- for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
3543
- const props = component.properties;
3544
- if (!props || typeof props !== "object") continue;
3545
- const names = strList(props.actionNames);
3546
- for (let ai = 0; ai < names.length; ai++) {
3547
- check(
3548
- names[ai],
3549
- `page "${pageName}" \xB7 component "${strName4(component.type) ?? "?"}"`,
3550
- `${path}.properties.actionNames[${ai}]`,
3551
- "Quick-actions bar"
3552
- );
3553
- }
3554
- }
3555
- }
3556
- const apps = asArray23(stack.apps);
3557
- for (let ai = 0; ai < apps.length; ai++) {
3558
- const app = apps[ai];
3559
- if (!app || typeof app !== "object") continue;
3560
- const appName = strName4(app.name) ?? `#${ai}`;
3561
- const walkNav = (items, basePath) => {
3562
- const navItems = asArray23(items);
3563
- for (let ni = 0; ni < navItems.length; ni++) {
3564
- const nav = navItems[ni];
3565
- if (!nav || typeof nav !== "object") continue;
3566
- const navPath = `${basePath}[${ni}]`;
3567
- const actionDef = nav.actionDef;
3568
- const actionName = strName4(actionDef?.actionName);
3569
- if (nav.type === "action" && actionName) {
3570
- check(
3571
- actionName,
3572
- `app "${appName}" \xB7 nav "${strName4(nav.id) ?? `#${ni}`}"`,
3573
- `${navPath}.actionDef.actionName`,
3574
- "Navigation action item"
3575
- );
3576
- }
3577
- if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
3578
- }
3579
- };
3580
- walkNav(app.navigation, `apps[${ai}].navigation`);
3581
- const areas = asArray23(app.areas);
3582
- for (let ri = 0; ri < areas.length; ri++) {
3583
- walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
3584
- }
3585
- }
3586
- return findings;
3587
- }
3588
-
3589
- // src/validate-page-field-bindings.ts
3590
- var PAGE_FIELD_UNKNOWN = "page-field-unknown";
3591
- var SYSTEM_FIELDS4 = /* @__PURE__ */ new Set([
3592
- "id",
3593
- "created_at",
3594
- "created_by",
3595
- "updated_at",
3596
- "updated_by",
3597
- "owner_id",
3598
- "organization_id",
3599
- "tenant_id",
3600
- "user_id",
3601
- "deleted_at"
3602
- ]);
3603
- function asArray24(v) {
3604
- if (Array.isArray(v)) return v;
3605
- if (v && typeof v === "object") {
3606
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3607
- }
3608
- return [];
3609
- }
3610
- function strName5(v) {
3611
- return typeof v === "string" && v.length > 0 ? v : void 0;
3612
- }
3613
- function isRec3(v) {
3614
- return !!v && typeof v === "object" && !Array.isArray(v);
3615
- }
3616
- function fieldRefsFrom(value, basePath) {
3617
- const out = [];
3618
- const one = (v, path) => {
3619
- const bare = strName5(v);
3620
- if (bare) {
3621
- out.push({ name: bare, path });
3622
- return;
3623
- }
3624
- if (!isRec3(v)) return;
3625
- const named = strName5(v.field) ?? strName5(v.name);
3626
- if (named) out.push({ name: named, path: `${path}.${strName5(v.field) ? "field" : "name"}` });
3627
- };
3628
- if (Array.isArray(value)) {
3629
- for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
3630
- } else {
3631
- one(value, basePath);
3632
- }
3633
- return out;
3634
- }
3635
- var COMPONENT_FIELD_SPECS = {
3636
- "record:highlights": { props: ["fields"] },
3637
- // `sections`/`hideFields` are not in RecordDetailsProps, but every real page
3638
- // authors them (they survive because `properties` is unvalidated).
3639
- "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
3640
- "record:path": { props: ["statusField"] },
3641
- "element:number": { props: ["field"] },
3642
- "element:filter": { props: ["fields"] },
3643
- "element:form": { props: ["fields"] },
3644
- // The schema says `displayField`; real pages author `labelField`. Accept both.
3645
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
3646
- };
3647
- var RELATED_LIST_TYPE = "record:related_list";
3648
- function validatePageFieldBindings(stack) {
3649
- const findings = [];
3650
- if (!stack || typeof stack !== "object") return findings;
3651
- const objectFields = /* @__PURE__ */ new Map();
3652
- for (const obj of asArray24(stack.objects)) {
3653
- const name = strName5(obj.name);
3654
- if (!name) continue;
3655
- const names = /* @__PURE__ */ new Set();
3656
- for (const f of asArray24(obj.fields)) {
3657
- const fn = strName5(f.name);
3658
- if (fn) names.add(fn);
3659
- }
3660
- objectFields.set(name, names);
3661
- }
3662
- const pages = asArray24(stack.pages);
3663
- for (let pi = 0; pi < pages.length; pi++) {
3664
- const page = pages[pi];
3665
- if (!page || typeof page !== "object") continue;
3666
- const pageName = strName5(page.name) ?? `#${pi}`;
3667
- const checkRefs = (refs, objectName, where) => {
3668
- if (!objectName) return;
3669
- const known = objectFields.get(objectName);
3670
- if (!known) return;
3671
- for (const ref of refs) {
3672
- if (ref.name.includes(".")) continue;
3673
- if (known.has(ref.name) || SYSTEM_FIELDS4.has(ref.name)) continue;
3674
- findings.push({
3675
- severity: "warning",
3676
- rule: PAGE_FIELD_UNKNOWN,
3677
- where,
3678
- path: ref.path,
3679
- message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 the component silently skips it, so it never renders.`,
3680
- hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
3681
- });
3682
- }
3683
- };
3684
- for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
3685
- const type = strName5(component.type);
3686
- const props = isRec3(component.properties) ? component.properties : void 0;
3687
- if (!type || !props) continue;
3688
- const where = `page "${pageName}" \xB7 ${type}`;
3689
- if (type === RELATED_LIST_TYPE) {
3690
- const relatedObject = strName5(props.objectName);
3691
- const relatedRefs = [
3692
- ...fieldRefsFrom(props.columns, `${path}.properties.columns`),
3693
- ...fieldRefsFrom(props.sort, `${path}.properties.sort`),
3694
- ...fieldRefsFrom(props.filter, `${path}.properties.filter`),
3695
- ...fieldRefsFrom(props.relationshipField, `${path}.properties.relationshipField`)
3696
- ];
3697
- checkRefs(relatedRefs, relatedObject, where);
3698
- checkRefs(
3699
- fieldRefsFrom(props.relationshipValueField, `${path}.properties.relationshipValueField`),
3700
- objectName,
3701
- where
3702
- );
3703
- const add = isRec3(props.add) ? props.add : void 0;
3704
- const picker = add && isRec3(add.picker) ? add.picker : void 0;
3705
- if (picker) {
3706
- checkRefs(
3707
- [
3708
- ...fieldRefsFrom(picker.valueField, `${path}.properties.add.picker.valueField`),
3709
- ...fieldRefsFrom(picker.labelField, `${path}.properties.add.picker.labelField`)
3710
- ],
3711
- strName5(picker.object),
3712
- where
3713
- );
3714
- }
3715
- if (add) {
3716
- checkRefs(
3717
- fieldRefsFrom(add.linkField, `${path}.properties.add.linkField`),
3718
- relatedObject,
3719
- where
4084
+ check(
4085
+ names[ai],
4086
+ `view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
4087
+ `${path}.${key}[${ai}]`,
4088
+ key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
3720
4089
  );
3721
4090
  }
3722
- continue;
3723
4091
  }
3724
- const spec = COMPONENT_FIELD_SPECS[type];
3725
- if (!spec) continue;
3726
- const refs = [];
3727
- for (const key of spec.props ?? []) {
3728
- refs.push(...fieldRefsFrom(props[key], `${path}.properties.${key}`));
4092
+ };
4093
+ checkListContainer(view.list, "list", `views[${vi}].list`);
4094
+ const listViews = view.listViews;
4095
+ if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
4096
+ for (const [key, lv] of Object.entries(listViews)) {
4097
+ checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
3729
4098
  }
3730
- for (const key of spec.nestedSections ?? []) {
3731
- const sections = Array.isArray(props[key]) ? props[key] : [];
3732
- for (let si = 0; si < sections.length; si++) {
3733
- const section = sections[si];
3734
- if (!isRec3(section)) continue;
3735
- refs.push(
3736
- ...fieldRefsFrom(section.fields, `${path}.properties.${key}[${si}].fields`)
3737
- );
3738
- }
4099
+ }
4100
+ }
4101
+ const pages = asArray25(stack.pages);
4102
+ for (let pi = 0; pi < pages.length; pi++) {
4103
+ const page = pages[pi];
4104
+ if (!page || typeof page !== "object") continue;
4105
+ const pageName = strName7(page.name) ?? `#${pi}`;
4106
+ for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4107
+ const props = component.properties;
4108
+ if (!props || typeof props !== "object") continue;
4109
+ const names = strList(props.actionNames);
4110
+ for (let ai = 0; ai < names.length; ai++) {
4111
+ check(
4112
+ names[ai],
4113
+ `page "${pageName}" \xB7 component "${strName7(component.type) ?? "?"}"`,
4114
+ `${path}.properties.actionNames[${ai}]`,
4115
+ "Quick-actions bar"
4116
+ );
3739
4117
  }
3740
- checkRefs(refs, objectName, where);
3741
4118
  }
3742
- const cfg = isRec3(page.interfaceConfig) ? page.interfaceConfig : void 0;
3743
- if (cfg) {
3744
- const cfgObject = strName5(cfg.source) ?? strName5(page.object);
3745
- const base = `pages[${pi}].interfaceConfig`;
3746
- const refs = [
3747
- ...fieldRefsFrom(cfg.columns, `${base}.columns`),
3748
- ...fieldRefsFrom(cfg.sort, `${base}.sort`),
3749
- ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
3750
- ];
3751
- const userFilters = isRec3(cfg.userFilters) ? cfg.userFilters : void 0;
3752
- if (userFilters) {
3753
- refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
4119
+ }
4120
+ const apps = asArray25(stack.apps);
4121
+ for (let ai = 0; ai < apps.length; ai++) {
4122
+ const app = apps[ai];
4123
+ if (!app || typeof app !== "object") continue;
4124
+ const appName = strName7(app.name) ?? `#${ai}`;
4125
+ const walkNav = (items, basePath) => {
4126
+ const navItems = asArray25(items);
4127
+ for (let ni = 0; ni < navItems.length; ni++) {
4128
+ const nav = navItems[ni];
4129
+ if (!nav || typeof nav !== "object") continue;
4130
+ const navPath = `${basePath}[${ni}]`;
4131
+ const actionDef = nav.actionDef;
4132
+ const actionName = strName7(actionDef?.actionName);
4133
+ if (nav.type === "action" && actionName) {
4134
+ check(
4135
+ actionName,
4136
+ `app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
4137
+ `${navPath}.actionDef.actionName`,
4138
+ "Navigation action item"
4139
+ );
4140
+ }
4141
+ if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
3754
4142
  }
3755
- checkRefs(refs, cfgObject, `page "${pageName}" \xB7 interfaceConfig`);
4143
+ };
4144
+ walkNav(app.navigation, `apps[${ai}].navigation`);
4145
+ const areas = asArray25(app.areas);
4146
+ for (let ri = 0; ri < areas.length; ri++) {
4147
+ walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
3756
4148
  }
3757
4149
  }
3758
4150
  return findings;
@@ -3763,23 +4155,23 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
3763
4155
  var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
3764
4156
  var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
3765
4157
  var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
3766
- function asArray25(v) {
4158
+ function asArray26(v) {
3767
4159
  if (Array.isArray(v)) return v;
3768
4160
  if (v && typeof v === "object") {
3769
4161
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3770
4162
  }
3771
4163
  return [];
3772
4164
  }
3773
- function strName6(v) {
4165
+ function strName8(v) {
3774
4166
  return typeof v === "string" && v.length > 0 ? v : void 0;
3775
4167
  }
3776
4168
  function strList2(v) {
3777
4169
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
3778
4170
  }
3779
- function isRec4(v) {
4171
+ function isRec6(v) {
3780
4172
  return !!v && typeof v === "object" && !Array.isArray(v);
3781
4173
  }
3782
- function distance3(a, b) {
4174
+ function distance4(a, b) {
3783
4175
  const m = a.length;
3784
4176
  const n = b.length;
3785
4177
  if (m === 0) return n;
@@ -3795,11 +4187,11 @@ function distance3(a, b) {
3795
4187
  }
3796
4188
  return prev[n];
3797
4189
  }
3798
- function suggest4(target, known) {
4190
+ function suggest5(target, known) {
3799
4191
  let best;
3800
4192
  let bestScore = Infinity;
3801
4193
  for (const c of known) {
3802
- const d = distance3(target, c);
4194
+ const d = distance4(target, c);
3803
4195
  if (d < bestScore) {
3804
4196
  bestScore = d;
3805
4197
  best = c;
@@ -3814,17 +4206,17 @@ function list2(names) {
3814
4206
  }
3815
4207
  function indexDatasets(stack) {
3816
4208
  const out = /* @__PURE__ */ new Map();
3817
- for (const ds of asArray25(stack.datasets)) {
3818
- const name = strName6(ds.name);
4209
+ for (const ds of asArray26(stack.datasets)) {
4210
+ const name = strName8(ds.name);
3819
4211
  if (!name) continue;
3820
4212
  const dimensions = /* @__PURE__ */ new Set();
3821
- for (const d of asArray25(ds.dimensions)) {
3822
- const n = strName6(d.name);
4213
+ for (const d of asArray26(ds.dimensions)) {
4214
+ const n = strName8(d.name);
3823
4215
  if (n) dimensions.add(n);
3824
4216
  }
3825
4217
  const measures = /* @__PURE__ */ new Set();
3826
- for (const m of asArray25(ds.measures)) {
3827
- const n = strName6(m.name);
4218
+ for (const m of asArray26(ds.measures)) {
4219
+ const n = strName8(m.name);
3828
4220
  if (n) measures.add(n);
3829
4221
  }
3830
4222
  out.set(name, { dimensions, measures });
@@ -3847,7 +4239,7 @@ function validateChartBindings(stack) {
3847
4239
  where: binding.where,
3848
4240
  path: `${binding.path}.dataset`,
3849
4241
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
3850
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest4(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
4242
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
3851
4243
  });
3852
4244
  return;
3853
4245
  }
@@ -3859,7 +4251,7 @@ function validateChartBindings(stack) {
3859
4251
  where: binding.where,
3860
4252
  path,
3861
4253
  message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
3862
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest4(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
4254
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
3863
4255
  });
3864
4256
  };
3865
4257
  const measureRef = (name, path, selected2) => {
@@ -3870,7 +4262,7 @@ function validateChartBindings(stack) {
3870
4262
  where: binding.where,
3871
4263
  path,
3872
4264
  message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
3873
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest4(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
4265
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
3874
4266
  });
3875
4267
  return;
3876
4268
  }
@@ -3902,29 +4294,29 @@ function validateChartBindings(stack) {
3902
4294
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
3903
4295
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
3904
4296
  };
3905
- const reports = asArray25(stack.reports);
4297
+ const reports = asArray26(stack.reports);
3906
4298
  for (let ri = 0; ri < reports.length; ri++) {
3907
4299
  const report = reports[ri];
3908
- if (!isRec4(report)) continue;
3909
- const reportName = strName6(report.name) ?? `#${ri}`;
4300
+ if (!isRec6(report)) continue;
4301
+ const reportName = strName8(report.name) ?? `#${ri}`;
3910
4302
  const checkReportChart = (chart, dataset, values, where, path) => {
3911
- if (!isRec4(chart)) return;
4303
+ if (!isRec6(chart)) return;
3912
4304
  check({
3913
4305
  dataset,
3914
4306
  // `values` is the report's measure SELECTION, not a chart ref; feeding
3915
4307
  // it in lets the yAxis "declared but not selected" check work without
3916
4308
  // reporting the selection itself twice.
3917
4309
  values: { names: values, path: `${path}.values` },
3918
- xAxis: strName6(chart.xAxis) ? { name: strName6(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
3919
- yAxis: strName6(chart.yAxis) ? { name: strName6(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
3920
- series: asArray25(chart.series).map((s, si) => ({ name: strName6(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4310
+ xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
4311
+ yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
4312
+ series: asArray26(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
3921
4313
  where,
3922
4314
  path: `${path}.chart`
3923
4315
  });
3924
4316
  };
3925
4317
  checkReportChart(
3926
4318
  report.chart,
3927
- strName6(report.dataset),
4319
+ strName8(report.dataset),
3928
4320
  strList2(report.values),
3929
4321
  `report "${reportName}" \xB7 chart`,
3930
4322
  `reports[${ri}]`
@@ -3932,45 +4324,45 @@ function validateChartBindings(stack) {
3932
4324
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
3933
4325
  for (let bi = 0; bi < blocks.length; bi++) {
3934
4326
  const block = blocks[bi];
3935
- if (!isRec4(block)) continue;
4327
+ if (!isRec6(block)) continue;
3936
4328
  checkReportChart(
3937
4329
  block.chart,
3938
- strName6(block.dataset),
4330
+ strName8(block.dataset),
3939
4331
  strList2(block.values),
3940
- `report "${reportName}" \xB7 block "${strName6(block.name) ?? `#${bi}`}" chart`,
4332
+ `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
3941
4333
  `reports[${ri}].blocks[${bi}]`
3942
4334
  );
3943
4335
  }
3944
4336
  }
3945
4337
  const checkListChart = (container, where, path) => {
3946
- if (!isRec4(container)) return;
4338
+ if (!isRec6(container)) return;
3947
4339
  const chart = container.chart;
3948
- if (!isRec4(chart)) return;
4340
+ if (!isRec6(chart)) return;
3949
4341
  check({
3950
- dataset: strName6(chart.dataset),
4342
+ dataset: strName8(chart.dataset),
3951
4343
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
3952
4344
  values: { names: strList2(chart.values), path: `${path}.chart.values` },
3953
4345
  where,
3954
4346
  path: `${path}.chart`
3955
4347
  });
3956
4348
  };
3957
- const views = asArray25(stack.views);
4349
+ const views = asArray26(stack.views);
3958
4350
  for (let vi = 0; vi < views.length; vi++) {
3959
4351
  const view = views[vi];
3960
- if (!isRec4(view)) continue;
3961
- const viewName = strName6(view.name) ?? strName6(view.objectName) ?? `#${vi}`;
4352
+ if (!isRec6(view)) continue;
4353
+ const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
3962
4354
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
3963
- if (isRec4(view.listViews)) {
4355
+ if (isRec6(view.listViews)) {
3964
4356
  for (const [key, lv] of Object.entries(view.listViews)) {
3965
4357
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
3966
4358
  }
3967
4359
  }
3968
4360
  }
3969
- const objects = asArray25(stack.objects);
4361
+ const objects = asArray26(stack.objects);
3970
4362
  for (let oi = 0; oi < objects.length; oi++) {
3971
4363
  const obj = objects[oi];
3972
- if (!isRec4(obj) || !isRec4(obj.listViews)) continue;
3973
- const objName = strName6(obj.name) ?? `#${oi}`;
4364
+ if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
4365
+ const objName = strName8(obj.name) ?? `#${oi}`;
3974
4366
  for (const [key, lv] of Object.entries(obj.listViews)) {
3975
4367
  checkListChart(
3976
4368
  lv,
@@ -3979,22 +4371,22 @@ function validateChartBindings(stack) {
3979
4371
  );
3980
4372
  }
3981
4373
  }
3982
- const pages = asArray25(stack.pages);
4374
+ const pages = asArray26(stack.pages);
3983
4375
  for (let pi = 0; pi < pages.length; pi++) {
3984
4376
  const page = pages[pi];
3985
- if (!isRec4(page)) continue;
3986
- const pageName = strName6(page.name) ?? `#${pi}`;
4377
+ if (!isRec6(page)) continue;
4378
+ const pageName = strName8(page.name) ?? `#${pi}`;
3987
4379
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
3988
- const props = isRec4(component.properties) ? component.properties : void 0;
3989
- if (!props || !strName6(props.dataset)) continue;
3990
- const axisRefs = asArray25(props.yAxis).map((a, ai) => ({ name: strName6(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
3991
- const seriesRefs = asArray25(props.series).map((s, si) => ({ name: strName6(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
4380
+ const props = isRec6(component.properties) ? component.properties : void 0;
4381
+ if (!props || !strName8(props.dataset)) continue;
4382
+ const axisRefs = asArray26(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
4383
+ const seriesRefs = asArray26(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
3992
4384
  check({
3993
- dataset: strName6(props.dataset),
4385
+ dataset: strName8(props.dataset),
3994
4386
  dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
3995
4387
  values: { names: strList2(props.values), path: `${path}.properties.values` },
3996
4388
  series: [...axisRefs, ...seriesRefs],
3997
- where: `page "${pageName}" \xB7 ${strName6(component.type) ?? "chart"}`,
4389
+ where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
3998
4390
  path: `${path}.properties`
3999
4391
  });
4000
4392
  }
@@ -4006,7 +4398,7 @@ function validateChartBindings(stack) {
4006
4398
  import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
4007
4399
 
4008
4400
  // src/build-access-matrix.ts
4009
- function asArray26(v) {
4401
+ function asArray27(v) {
4010
4402
  if (Array.isArray(v)) return v;
4011
4403
  if (v && typeof v === "object") {
4012
4404
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4017,13 +4409,13 @@ function buildAccessMatrix(stack) {
4017
4409
  const entries = [];
4018
4410
  if (!stack || typeof stack !== "object") return { version: 1, entries };
4019
4411
  const owdByObject = /* @__PURE__ */ new Map();
4020
- for (const obj of asArray26(stack.objects)) {
4412
+ for (const obj of asArray27(stack.objects)) {
4021
4413
  const name = typeof obj.name === "string" ? obj.name : "";
4022
4414
  if (!name) continue;
4023
4415
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
4024
4416
  if (typeof owd === "string") owdByObject.set(name, owd);
4025
4417
  }
4026
- for (const ps of asArray26(stack.permissions)) {
4418
+ for (const ps of asArray27(stack.permissions)) {
4027
4419
  const psName = typeof ps.name === "string" ? ps.name : "";
4028
4420
  if (!psName) continue;
4029
4421
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -4096,34 +4488,34 @@ function diffAccessMatrix(before, after) {
4096
4488
 
4097
4489
  // src/validate-nav-access.ts
4098
4490
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
4099
- function asArray27(v) {
4491
+ function asArray28(v) {
4100
4492
  if (Array.isArray(v)) return v;
4101
4493
  if (v && typeof v === "object") {
4102
4494
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4103
4495
  }
4104
4496
  return [];
4105
4497
  }
4106
- function strName7(v) {
4498
+ function strName9(v) {
4107
4499
  return typeof v === "string" && v.length > 0 ? v : void 0;
4108
4500
  }
4109
4501
  function collectNavExposures(stack) {
4110
4502
  const out = [];
4111
- const apps = asArray27(stack.apps);
4503
+ const apps = asArray28(stack.apps);
4112
4504
  for (let ai = 0; ai < apps.length; ai++) {
4113
4505
  const app = apps[ai];
4114
4506
  if (!app || typeof app !== "object") continue;
4115
- const appName = strName7(app.name) ?? `#${ai}`;
4507
+ const appName = strName9(app.name) ?? `#${ai}`;
4116
4508
  const walk = (items, basePath) => {
4117
- const navItems = asArray27(items);
4509
+ const navItems = asArray28(items);
4118
4510
  for (let ni = 0; ni < navItems.length; ni++) {
4119
4511
  const nav = navItems[ni];
4120
4512
  if (!nav || typeof nav !== "object") continue;
4121
4513
  const navPath = `${basePath}[${ni}]`;
4122
- const objectName = strName7(nav.objectName);
4514
+ const objectName = strName9(nav.objectName);
4123
4515
  if (nav.type === "object" && objectName) {
4124
4516
  out.push({
4125
4517
  objectName,
4126
- where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
4518
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
4127
4519
  path: `${navPath}.objectName`
4128
4520
  });
4129
4521
  }
@@ -4131,7 +4523,7 @@ function collectNavExposures(stack) {
4131
4523
  }
4132
4524
  };
4133
4525
  walk(app.navigation, `apps[${ai}].navigation`);
4134
- const areas = asArray27(app.areas);
4526
+ const areas = asArray28(app.areas);
4135
4527
  for (let ri = 0; ri < areas.length; ri++) {
4136
4528
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4137
4529
  }
@@ -4141,13 +4533,13 @@ function collectNavExposures(stack) {
4141
4533
  function validateNavAccess(stack) {
4142
4534
  const findings = [];
4143
4535
  if (!stack || typeof stack !== "object") return findings;
4144
- const permissionSets = asArray27(stack.permissions);
4536
+ const permissionSets = asArray28(stack.permissions);
4145
4537
  if (permissionSets.length === 0) return findings;
4146
4538
  const exposures = collectNavExposures(stack);
4147
4539
  if (exposures.length === 0) return findings;
4148
4540
  const ownObjects = /* @__PURE__ */ new Set();
4149
- for (const obj of asArray27(stack.objects)) {
4150
- const n = strName7(obj.name);
4541
+ for (const obj of asArray28(stack.objects)) {
4542
+ const n = strName9(obj.name);
4151
4543
  if (n) ownObjects.add(n);
4152
4544
  }
4153
4545
  const readable = /* @__PURE__ */ new Set();
@@ -4179,18 +4571,18 @@ function validateNavAccess(stack) {
4179
4571
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
4180
4572
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
4181
4573
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
4182
- function isRec5(v) {
4574
+ function isRec7(v) {
4183
4575
  return !!v && typeof v === "object" && !Array.isArray(v);
4184
4576
  }
4185
- function asArray28(v) {
4577
+ function asArray29(v) {
4186
4578
  if (Array.isArray(v)) return v;
4187
- if (isRec5(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec5(def) ? def : {} }));
4579
+ if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
4188
4580
  return [];
4189
4581
  }
4190
- function strName8(v) {
4582
+ function strName10(v) {
4191
4583
  return typeof v === "string" && v.length > 0 ? v : void 0;
4192
4584
  }
4193
- function distance4(a, b) {
4585
+ function distance5(a, b) {
4194
4586
  const m = a.length;
4195
4587
  const n = b.length;
4196
4588
  if (m === 0) return n;
@@ -4206,7 +4598,7 @@ function distance4(a, b) {
4206
4598
  }
4207
4599
  return prev[n];
4208
4600
  }
4209
- function suggest5(target, known) {
4601
+ function suggest6(target, known) {
4210
4602
  const names = [...known];
4211
4603
  const segmentMatch = names.find(
4212
4604
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -4215,7 +4607,7 @@ function suggest5(target, known) {
4215
4607
  let best;
4216
4608
  let bestScore = Infinity;
4217
4609
  for (const candidate of names) {
4218
- const d = distance4(target, candidate);
4610
+ const d = distance5(target, candidate);
4219
4611
  if (d < bestScore) {
4220
4612
  bestScore = d;
4221
4613
  best = candidate;
@@ -4230,20 +4622,10 @@ function listNames(names, max = 12) {
4230
4622
  const shown = all.slice(0, max).join(", ");
4231
4623
  return all.length > max ? `${shown}, \u2026 (${all.length} total)` : shown;
4232
4624
  }
4233
- var SYSTEM_FIELDS5 = /* @__PURE__ */ new Set([
4234
- "id",
4625
+ var IMPLICIT_FIELDS = /* @__PURE__ */ new Set([
4626
+ ...SYSTEM_FIELDS,
4235
4627
  "_id",
4236
4628
  "name",
4237
- "created_at",
4238
- "created_by",
4239
- "updated_at",
4240
- "updated_by",
4241
- "owner_id",
4242
- "organization_id",
4243
- "tenant_id",
4244
- "user_id",
4245
- "is_deleted",
4246
- "deleted_at",
4247
4629
  "space"
4248
4630
  ]);
4249
4631
  function emptyFacts() {
@@ -4255,20 +4637,20 @@ function collectViewRecord(view, factsFor) {
4255
4637
  const addView = (objectName, name) => {
4256
4638
  if (objectName && name) factsFor(objectName).views.add(name);
4257
4639
  };
4258
- const listBinding = isRec5(view.list) ? bindingOf(view.list) : void 0;
4259
- if (isRec5(view.list)) addView(listBinding, strName8(view.list.name));
4260
- addView(recordObject ?? listBinding, strName8(view.name));
4640
+ const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
4641
+ if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
4642
+ addView(recordObject ?? listBinding, strName10(view.name));
4261
4643
  for (const key of ["listViews", "formViews"]) {
4262
4644
  const container = view[key];
4263
- if (!isRec5(container)) continue;
4645
+ if (!isRec7(container)) continue;
4264
4646
  for (const [subKey, sub] of Object.entries(container)) {
4265
- if (!isRec5(sub)) continue;
4647
+ if (!isRec7(sub)) continue;
4266
4648
  const binding = bindingOf(sub) ?? listBinding;
4267
4649
  addView(binding, subKey);
4268
- addView(binding, strName8(sub.name));
4650
+ addView(binding, strName10(sub.name));
4269
4651
  if (binding) {
4270
- for (const section of asArray28(sub.sections)) {
4271
- const sectionName = strName8(section.name);
4652
+ for (const section of asArray29(sub.sections)) {
4653
+ const sectionName = strName10(section.name);
4272
4654
  if (sectionName) factsFor(binding).sections.add(sectionName);
4273
4655
  }
4274
4656
  }
@@ -4276,14 +4658,14 @@ function collectViewRecord(view, factsFor) {
4276
4658
  }
4277
4659
  const sectionBinding = recordObject ?? listBinding;
4278
4660
  if (sectionBinding) {
4279
- for (const section of asArray28(view.sections)) {
4280
- const sectionName = strName8(section.name);
4661
+ for (const section of asArray29(view.sections)) {
4662
+ const sectionName = strName10(section.name);
4281
4663
  if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
4282
4664
  }
4283
4665
  }
4284
4666
  }
4285
4667
  function viewObjectName(view) {
4286
- return strName8(view.objectName) ?? strName8(view.object) ?? (isRec5(view.data) ? strName8(view.data.object) : void 0);
4668
+ return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
4287
4669
  }
4288
4670
  function readOptions(field) {
4289
4671
  const raw = field.options;
@@ -4295,14 +4677,14 @@ function readOptions(field) {
4295
4677
  values.add(opt);
4296
4678
  continue;
4297
4679
  }
4298
- if (!isRec5(opt)) continue;
4299
- const value = strName8(opt.value);
4680
+ if (!isRec7(opt)) continue;
4681
+ const value = strName10(opt.value);
4300
4682
  if (!value) continue;
4301
4683
  values.add(value);
4302
- const label2 = strName8(opt.label);
4684
+ const label2 = strName10(opt.label);
4303
4685
  if (label2) byLabel.set(label2.toLowerCase(), value);
4304
4686
  }
4305
- } else if (isRec5(raw)) {
4687
+ } else if (isRec7(raw)) {
4306
4688
  for (const [value, label2] of Object.entries(raw)) {
4307
4689
  values.add(value);
4308
4690
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -4322,48 +4704,48 @@ function buildUniverse(stack) {
4322
4704
  }
4323
4705
  return facts;
4324
4706
  };
4325
- for (const obj of asArray28(stack.objects)) {
4326
- const objectName = strName8(obj.name);
4707
+ for (const obj of asArray29(stack.objects)) {
4708
+ const objectName = strName10(obj.name);
4327
4709
  if (!objectName) continue;
4328
4710
  const facts = factsFor(objectName);
4329
- for (const field of asArray28(obj.fields)) {
4330
- const fieldName = strName8(field.name);
4711
+ for (const field of asArray29(obj.fields)) {
4712
+ const fieldName = strName10(field.name);
4331
4713
  if (fieldName) facts.fields.set(fieldName, field);
4332
4714
  }
4333
- for (const action of asArray28(obj.actions)) {
4334
- const actionName = strName8(action.name);
4715
+ for (const action of asArray29(obj.actions)) {
4716
+ const actionName = strName10(action.name);
4335
4717
  if (actionName) facts.actions.set(actionName, action);
4336
4718
  }
4337
- for (const view of asArray28(obj.views)) {
4338
- collectViewRecord({ ...view, object: strName8(view.object) ?? objectName }, factsFor);
4719
+ for (const view of asArray29(obj.views)) {
4720
+ collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
4339
4721
  }
4340
4722
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
4341
- for (const group of asArray28(obj.fieldGroups)) {
4342
- const key = strName8(group.key) ?? strName8(group.name);
4723
+ for (const group of asArray29(obj.fieldGroups)) {
4724
+ const key = strName10(group.key) ?? strName10(group.name);
4343
4725
  if (key) facts.sections.add(key);
4344
4726
  }
4345
4727
  }
4346
- for (const view of asArray28(stack.views)) {
4728
+ for (const view of asArray29(stack.views)) {
4347
4729
  collectViewRecord(view, factsFor);
4348
4730
  }
4349
- const pages = asArray28(stack.pages);
4731
+ const pages = asArray29(stack.pages);
4350
4732
  for (let pi = 0; pi < pages.length; pi++) {
4351
4733
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
4352
4734
  if (!walked.objectName) continue;
4353
- const props = isRec5(walked.component.properties) ? walked.component.properties : void 0;
4735
+ const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
4354
4736
  if (!props) continue;
4355
- for (const section of asArray28(props.sections)) {
4356
- const sectionName = strName8(section.name);
4737
+ for (const section of asArray29(props.sections)) {
4738
+ const sectionName = strName10(section.name);
4357
4739
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
4358
4740
  }
4359
4741
  }
4360
4742
  }
4361
4743
  const globalActions = /* @__PURE__ */ new Map();
4362
4744
  const actionOwners = /* @__PURE__ */ new Map();
4363
- for (const action of asArray28(stack.actions)) {
4364
- const actionName = strName8(action.name);
4745
+ for (const action of asArray29(stack.actions)) {
4746
+ const actionName = strName10(action.name);
4365
4747
  if (!actionName) continue;
4366
- const owner = strName8(action.objectName) ?? strName8(action.object);
4748
+ const owner = strName10(action.objectName) ?? strName10(action.object);
4367
4749
  if (owner) {
4368
4750
  factsFor(owner).actions.set(actionName, action);
4369
4751
  actionOwners.set(actionName, owner);
@@ -4377,41 +4759,41 @@ function buildUniverse(stack) {
4377
4759
  }
4378
4760
  }
4379
4761
  const apps = /* @__PURE__ */ new Map();
4380
- for (const app of asArray28(stack.apps)) {
4381
- const appName = strName8(app.name);
4762
+ for (const app of asArray29(stack.apps)) {
4763
+ const appName = strName10(app.name);
4382
4764
  if (!appName) continue;
4383
4765
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
4384
4766
  const walkNav = (items) => {
4385
- for (const item of asArray28(items)) {
4386
- const id = strName8(item.id);
4767
+ for (const item of asArray29(items)) {
4768
+ const id = strName10(item.id);
4387
4769
  if (id) navIds.add(id);
4388
4770
  if (item.children) walkNav(item.children);
4389
4771
  }
4390
4772
  };
4391
4773
  walkNav(app.navigation);
4392
- for (const area of asArray28(app.areas)) {
4393
- const areaId = strName8(area.id);
4774
+ for (const area of asArray29(app.areas)) {
4775
+ const areaId = strName10(area.id);
4394
4776
  if (areaId) navIds.add(areaId);
4395
4777
  walkNav(area.navigation);
4396
4778
  }
4397
4779
  apps.set(appName, navIds);
4398
4780
  }
4399
4781
  const dashboards = /* @__PURE__ */ new Map();
4400
- for (const dash of asArray28(stack.dashboards)) {
4401
- const dashName = strName8(dash.name);
4782
+ for (const dash of asArray29(stack.dashboards)) {
4783
+ const dashName = strName10(dash.name);
4402
4784
  if (!dashName) continue;
4403
4785
  const widgets = /* @__PURE__ */ new Set();
4404
- for (const widget of asArray28(dash.widgets)) {
4405
- const id = strName8(widget.id) ?? strName8(widget.name);
4786
+ for (const widget of asArray29(dash.widgets)) {
4787
+ const id = strName10(widget.id) ?? strName10(widget.name);
4406
4788
  if (id) widgets.add(id);
4407
4789
  }
4408
4790
  const actions = /* @__PURE__ */ new Set();
4409
4791
  const headerActions = [
4410
- ...asArray28(isRec5(dash.header) ? dash.header.actions : void 0),
4411
- ...asArray28(dash.actions)
4792
+ ...asArray29(isRec7(dash.header) ? dash.header.actions : void 0),
4793
+ ...asArray29(dash.actions)
4412
4794
  ];
4413
4795
  for (const action of headerActions) {
4414
- const key = strName8(action.actionUrl) ?? strName8(action.url) ?? strName8(action.name);
4796
+ const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
4415
4797
  if (key) actions.add(key);
4416
4798
  }
4417
4799
  dashboards.set(dashName, { widgets, actions });
@@ -4423,7 +4805,7 @@ function localePath(bundleIndex, locale) {
4423
4805
  }
4424
4806
  function validateTranslationReferences(stack) {
4425
4807
  const findings = [];
4426
- if (!isRec5(stack)) return findings;
4808
+ if (!isRec7(stack)) return findings;
4427
4809
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
4428
4810
  if (bundles.length === 0) return findings;
4429
4811
  const universe = buildUniverse(stack);
@@ -4432,13 +4814,13 @@ function validateTranslationReferences(stack) {
4432
4814
  };
4433
4815
  for (let bi = 0; bi < bundles.length; bi++) {
4434
4816
  const bundle = bundles[bi];
4435
- if (!isRec5(bundle)) continue;
4817
+ if (!isRec7(bundle)) continue;
4436
4818
  for (const [locale, rawData] of Object.entries(bundle)) {
4437
- if (!isRec5(rawData)) continue;
4819
+ if (!isRec7(rawData)) continue;
4438
4820
  const base = localePath(bi, locale);
4439
4821
  const inLocale = `locale "${locale}"`;
4440
4822
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
4441
- if (!isRec5(rawNode)) continue;
4823
+ if (!isRec7(rawNode)) continue;
4442
4824
  const objPath = `${base}.objects.${objectName}`;
4443
4825
  const facts = universe.objects.get(objectName);
4444
4826
  if (!facts) {
@@ -4446,7 +4828,7 @@ function validateTranslationReferences(stack) {
4446
4828
  orphan(
4447
4829
  `${inLocale} \xB7 object "${objectName}"`,
4448
4830
  objPath,
4449
- hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest5(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest5(objectName, universe.objects.keys()),
4831
+ hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
4450
4832
  `Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
4451
4833
  );
4452
4834
  continue;
@@ -4455,16 +4837,16 @@ function validateTranslationReferences(stack) {
4455
4837
  const fieldPath = `${objPath}.fields.${fieldName}`;
4456
4838
  const field = facts.fields.get(fieldName);
4457
4839
  if (!field) {
4458
- if (SYSTEM_FIELDS5.has(fieldName)) continue;
4840
+ if (IMPLICIT_FIELDS.has(fieldName)) continue;
4459
4841
  orphan(
4460
4842
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
4461
4843
  fieldPath,
4462
- `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest5(fieldName, facts.fields.keys()),
4844
+ `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest6(fieldName, facts.fields.keys()),
4463
4845
  `Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
4464
4846
  );
4465
4847
  continue;
4466
4848
  }
4467
- if (!isRec5(rawField)) continue;
4849
+ if (!isRec7(rawField)) continue;
4468
4850
  checkOptionKeys(findings, {
4469
4851
  optionMap: rawField.options,
4470
4852
  field,
@@ -4479,7 +4861,7 @@ function validateTranslationReferences(stack) {
4479
4861
  orphan(
4480
4862
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
4481
4863
  `${objPath}._views.${viewName}`,
4482
- `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest5(viewName, facts.views),
4864
+ `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest6(viewName, facts.views),
4483
4865
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
4484
4866
  );
4485
4867
  }
@@ -4488,7 +4870,7 @@ function validateTranslationReferences(stack) {
4488
4870
  orphan(
4489
4871
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
4490
4872
  `${objPath}._sections.${sectionName}`,
4491
- `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest5(sectionName, facts.sections),
4873
+ `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest6(sectionName, facts.sections),
4492
4874
  `Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
4493
4875
  );
4494
4876
  }
@@ -4499,7 +4881,7 @@ function validateTranslationReferences(stack) {
4499
4881
  orphan(
4500
4882
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
4501
4883
  actionPath,
4502
- `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest5(actionName, facts.actions.keys()),
4884
+ `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest6(actionName, facts.actions.keys()),
4503
4885
  `Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
4504
4886
  );
4505
4887
  continue;
@@ -4521,7 +4903,7 @@ function validateTranslationReferences(stack) {
4521
4903
  orphan(
4522
4904
  `${inLocale} \xB7 global action "${actionName}"`,
4523
4905
  actionPath,
4524
- owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest5(actionName, universe.globalActions.keys()),
4906
+ owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest6(actionName, universe.globalActions.keys()),
4525
4907
  owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
4526
4908
  );
4527
4909
  continue;
@@ -4541,18 +4923,18 @@ function validateTranslationReferences(stack) {
4541
4923
  orphan(
4542
4924
  `${inLocale} \xB7 app "${appName}"`,
4543
4925
  appPath,
4544
- `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest5(appName, universe.apps.keys()),
4926
+ `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest6(appName, universe.apps.keys()),
4545
4927
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
4546
4928
  );
4547
4929
  continue;
4548
4930
  }
4549
- if (!isRec5(rawApp)) continue;
4931
+ if (!isRec7(rawApp)) continue;
4550
4932
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
4551
4933
  if (navIds.has(navId)) continue;
4552
4934
  orphan(
4553
4935
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
4554
4936
  `${appPath}.navigation.${navId}`,
4555
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest5(navId, navIds),
4937
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
4556
4938
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
4557
4939
  );
4558
4940
  }
@@ -4564,18 +4946,18 @@ function validateTranslationReferences(stack) {
4564
4946
  orphan(
4565
4947
  `${inLocale} \xB7 dashboard "${dashName}"`,
4566
4948
  dashPath,
4567
- `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest5(dashName, universe.dashboards.keys()),
4949
+ `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest6(dashName, universe.dashboards.keys()),
4568
4950
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
4569
4951
  );
4570
4952
  continue;
4571
4953
  }
4572
- if (!isRec5(rawDash)) continue;
4954
+ if (!isRec7(rawDash)) continue;
4573
4955
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
4574
4956
  if (dash.widgets.has(widgetId)) continue;
4575
4957
  orphan(
4576
4958
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
4577
4959
  `${dashPath}.widgets.${widgetId}`,
4578
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest5(widgetId, dash.widgets),
4960
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
4579
4961
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
4580
4962
  );
4581
4963
  }
@@ -4584,7 +4966,7 @@ function validateTranslationReferences(stack) {
4584
4966
  orphan(
4585
4967
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
4586
4968
  `${dashPath}.actions.${actionKey}`,
4587
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest5(actionKey, dash.actions),
4969
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
4588
4970
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
4589
4971
  );
4590
4972
  }
@@ -4594,7 +4976,7 @@ function validateTranslationReferences(stack) {
4594
4976
  return findings;
4595
4977
  }
4596
4978
  function asRecord(v) {
4597
- return isRec5(v) ? v : {};
4979
+ return isRec7(v) ? v : {};
4598
4980
  }
4599
4981
  function checkOptionKeys(findings, ctx) {
4600
4982
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -4606,7 +4988,7 @@ function checkOptionKeys(findings, ctx) {
4606
4988
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
4607
4989
  where: ctx.where,
4608
4990
  path: ctx.path,
4609
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName8(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
4991
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName10(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
4610
4992
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
4611
4993
  });
4612
4994
  return;
@@ -4619,17 +5001,17 @@ function checkOptionKeys(findings, ctx) {
4619
5001
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
4620
5002
  where: ctx.where,
4621
5003
  path: `${ctx.path}.${key}`,
4622
- message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest5(key, declared.values),
5004
+ message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest6(key, declared.values),
4623
5005
  hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
4624
5006
  });
4625
5007
  }
4626
5008
  }
4627
5009
  function checkActionParams(findings, ctx) {
4628
- const rawParams = Object.keys(asRecord(isRec5(ctx.rawAction) ? ctx.rawAction.params : void 0));
5010
+ const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
4629
5011
  if (rawParams.length === 0) return;
4630
5012
  const declared = /* @__PURE__ */ new Set();
4631
- for (const param of asArray28(ctx.action.params)) {
4632
- const name = strName8(param.name) ?? strName8(param.field);
5013
+ for (const param of asArray29(ctx.action.params)) {
5014
+ const name = strName10(param.name) ?? strName10(param.field);
4633
5015
  if (name) declared.add(name);
4634
5016
  }
4635
5017
  for (const paramName of rawParams) {
@@ -4639,7 +5021,7 @@ function checkActionParams(findings, ctx) {
4639
5021
  rule: TRANSLATION_TARGET_UNKNOWN,
4640
5022
  where: `${ctx.where} \xB7 param "${paramName}"`,
4641
5023
  path: `${ctx.path}.params.${paramName}`,
4642
- message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest5(paramName, declared),
5024
+ message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest6(paramName, declared),
4643
5025
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
4644
5026
  });
4645
5027
  }
@@ -4647,14 +5029,14 @@ function checkActionParams(findings, ctx) {
4647
5029
 
4648
5030
  // src/validate-ai-surface-affinity.ts
4649
5031
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
4650
- function asArray29(v) {
5032
+ function asArray30(v) {
4651
5033
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
4652
5034
  if (v && typeof v === "object") {
4653
5035
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4654
5036
  }
4655
5037
  return [];
4656
5038
  }
4657
- function strName9(v) {
5039
+ function strName11(v) {
4658
5040
  return typeof v === "string" && v.length > 0 ? v : void 0;
4659
5041
  }
4660
5042
  function surfaceOf(v) {
@@ -4664,18 +5046,18 @@ function validateAiSurfaceAffinity(stack) {
4664
5046
  const findings = [];
4665
5047
  if (!stack || typeof stack !== "object") return findings;
4666
5048
  const skillsByName = /* @__PURE__ */ new Map();
4667
- for (const skill of asArray29(stack.skills)) {
4668
- const n = strName9(skill.name);
5049
+ for (const skill of asArray30(stack.skills)) {
5050
+ const n = strName11(skill.name);
4669
5051
  if (n) skillsByName.set(n, skill);
4670
5052
  }
4671
- const agents = asArray29(stack.agents);
5053
+ const agents = asArray30(stack.agents);
4672
5054
  for (let ai = 0; ai < agents.length; ai++) {
4673
5055
  const agent = agents[ai];
4674
- const agentName = strName9(agent.name) ?? `#${ai}`;
5056
+ const agentName = strName11(agent.name) ?? `#${ai}`;
4675
5057
  const agentSurface = surfaceOf(agent.surface);
4676
5058
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
4677
5059
  for (let si = 0; si < skillRefs.length; si++) {
4678
- const ref = strName9(skillRefs[si]);
5060
+ const ref = strName11(skillRefs[si]);
4679
5061
  if (!ref) continue;
4680
5062
  const skill = skillsByName.get(ref);
4681
5063
  if (!skill) continue;
@@ -4697,17 +5079,17 @@ function validateAiSurfaceAffinity(stack) {
4697
5079
  // src/validate-ai-tool-references.ts
4698
5080
  import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
4699
5081
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
4700
- function asArray30(v) {
5082
+ function asArray31(v) {
4701
5083
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
4702
5084
  if (v && typeof v === "object") {
4703
5085
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4704
5086
  }
4705
5087
  return [];
4706
5088
  }
4707
- function strName10(v) {
5089
+ function strName12(v) {
4708
5090
  return typeof v === "string" && v.length > 0 ? v : void 0;
4709
5091
  }
4710
- function distance5(a, b) {
5092
+ function distance6(a, b) {
4711
5093
  const m = a.length;
4712
5094
  const n = b.length;
4713
5095
  if (m === 0) return n;
@@ -4723,14 +5105,14 @@ function distance5(a, b) {
4723
5105
  }
4724
5106
  return prev[n];
4725
5107
  }
4726
- function suggest6(target, known) {
5108
+ function suggest7(target, known) {
4727
5109
  for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
4728
5110
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
4729
5111
  }
4730
5112
  let best;
4731
5113
  let bestScore = Infinity;
4732
5114
  for (const candidate of known) {
4733
- const d = distance5(target, candidate);
5115
+ const d = distance6(target, candidate);
4734
5116
  if (d < bestScore) {
4735
5117
  bestScore = d;
4736
5118
  best = candidate;
@@ -4745,26 +5127,26 @@ function materialisesAsTool(action) {
4745
5127
  if (!ai || typeof ai !== "object") return false;
4746
5128
  const aiRec = ai;
4747
5129
  if (aiRec.exposed !== true) return false;
4748
- if (!strName10(aiRec.description)) return false;
4749
- const type = strName10(action.type);
5130
+ if (!strName12(aiRec.description)) return false;
5131
+ const type = strName12(action.type);
4750
5132
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
4751
5133
  if (type === "script") return Boolean(action.target || action.body);
4752
5134
  return Boolean(action.target);
4753
5135
  }
4754
5136
  function collectToolUniverse(stack) {
4755
5137
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
4756
- for (const tool of asArray30(stack.tools)) {
4757
- const n = strName10(tool.name);
5138
+ for (const tool of asArray31(stack.tools)) {
5139
+ const n = strName12(tool.name);
4758
5140
  if (n) universe.add(n);
4759
5141
  }
4760
5142
  const addActionFamily = (actions) => {
4761
- for (const action of asArray30(actions)) {
4762
- const n = strName10(action.name);
5143
+ for (const action of asArray31(actions)) {
5144
+ const n = strName12(action.name);
4763
5145
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
4764
5146
  }
4765
5147
  };
4766
5148
  addActionFamily(stack.actions);
4767
- for (const obj of asArray30(stack.objects)) {
5149
+ for (const obj of asArray31(stack.objects)) {
4768
5150
  addActionFamily(obj.actions);
4769
5151
  }
4770
5152
  return universe;
@@ -4772,13 +5154,13 @@ function collectToolUniverse(stack) {
4772
5154
  function collectUnexposedActionNames(stack) {
4773
5155
  const names = /* @__PURE__ */ new Set();
4774
5156
  const scan = (actions) => {
4775
- for (const action of asArray30(actions)) {
4776
- const n = strName10(action.name);
5157
+ for (const action of asArray31(actions)) {
5158
+ const n = strName12(action.name);
4777
5159
  if (n && !materialisesAsTool(action)) names.add(n);
4778
5160
  }
4779
5161
  };
4780
5162
  scan(stack.actions);
4781
- for (const obj of asArray30(stack.objects)) scan(obj.actions);
5163
+ for (const obj of asArray31(stack.objects)) scan(obj.actions);
4782
5164
  return names;
4783
5165
  }
4784
5166
  function validateAiToolReferences(stack) {
@@ -4796,13 +5178,13 @@ function validateAiToolReferences(stack) {
4796
5178
  }
4797
5179
  return universe.has(ref);
4798
5180
  };
4799
- const skills = asArray30(stack.skills);
5181
+ const skills = asArray31(stack.skills);
4800
5182
  for (let si = 0; si < skills.length; si++) {
4801
5183
  const skill = skills[si];
4802
- const skillName = strName10(skill.name) ?? `#${si}`;
5184
+ const skillName = strName12(skill.name) ?? `#${si}`;
4803
5185
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
4804
5186
  for (let ti = 0; ti < refs.length; ti++) {
4805
- const ref = strName10(refs[ti]);
5187
+ const ref = strName12(refs[ti]);
4806
5188
  if (!ref || resolves(ref)) continue;
4807
5189
  const isPattern = ref.endsWith("*");
4808
5190
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -4811,7 +5193,7 @@ function validateAiToolReferences(stack) {
4811
5193
  rule: AI_SKILL_TOOL_UNRESOLVED,
4812
5194
  where: `skill "${skillName}" \xB7 tools`,
4813
5195
  path: `skills[${si}].tools[${ti}]`,
4814
- message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest6(ref, universe),
5196
+ message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest7(ref, universe),
4815
5197
  hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
4816
5198
  });
4817
5199
  }
@@ -4821,24 +5203,24 @@ function validateAiToolReferences(stack) {
4821
5203
 
4822
5204
  // src/validate-ai-agent-authoring.ts
4823
5205
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
4824
- function asArray31(v) {
5206
+ function asArray32(v) {
4825
5207
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
4826
5208
  if (v && typeof v === "object") {
4827
5209
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4828
5210
  }
4829
5211
  return [];
4830
5212
  }
4831
- function strName11(v) {
5213
+ function strName13(v) {
4832
5214
  return typeof v === "string" && v.length > 0 ? v : void 0;
4833
5215
  }
4834
5216
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
4835
5217
  function validateAiAgentAuthoring(stack) {
4836
5218
  const findings = [];
4837
5219
  if (!stack || typeof stack !== "object") return findings;
4838
- const agents = asArray31(stack.agents);
5220
+ const agents = asArray32(stack.agents);
4839
5221
  for (let ai = 0; ai < agents.length; ai++) {
4840
5222
  const agent = agents[ai];
4841
- const name = strName11(agent.name) ?? `#${ai}`;
5223
+ const name = strName13(agent.name) ?? `#${ai}`;
4842
5224
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
4843
5225
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
4844
5226
  findings.push({
@@ -4853,9 +5235,494 @@ function validateAiAgentAuthoring(stack) {
4853
5235
  return findings;
4854
5236
  }
4855
5237
 
5238
+ // src/validate-hook-body-writes.ts
5239
+ import { createRequire as createRequire3 } from "module";
5240
+ import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
5241
+ var cachedTs2 = null;
5242
+ function loadTypeScript2() {
5243
+ if (cachedTs2) return cachedTs2;
5244
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5245
+ try {
5246
+ cachedTs2 = createRequire3(anchor)("typescript");
5247
+ } catch (err) {
5248
+ throw new Error(
5249
+ `@objectstack/lint: checking an L2 (language:'js') hook body requires the "typescript" 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 "typescript" in the image; it is only loaded when a hook with a JS body is validated.`
5250
+ );
5251
+ }
5252
+ return cachedTs2;
5253
+ }
5254
+ var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
5255
+ var HOOK_BODY_WRITE_PATTERNS = [
5256
+ {
5257
+ id: "input-property-assign",
5258
+ syntax: "ctx.input.<field> = \u2026 | ctx.input['<field>'] \u27E8op\u27E9= \u2026",
5259
+ example: {
5260
+ // Compound (`+=`) and logical (`??=`) assignment operators write their
5261
+ // LHS exactly like `=` does — the example pins the whole operator range.
5262
+ source: "ctx.input.total = 0; ctx.input['status'] ??= 'open'; ctx.input.retries += 1;",
5263
+ writes: [{ field: "total" }, { field: "status" }, { field: "retries" }]
5264
+ }
5265
+ },
5266
+ {
5267
+ id: "input-object-assign",
5268
+ syntax: "Object.assign(ctx.input, { <field>: \u2026 })",
5269
+ example: {
5270
+ source: "Object.assign(ctx.input, { total: 5, 'status': 'open', discount });",
5271
+ writes: [{ field: "total" }, { field: "status" }, { field: "discount" }]
5272
+ }
5273
+ },
5274
+ {
5275
+ // ACTION-only shape (the hook sandbox context has no `ctx.record` at all).
5276
+ // Declared here because this ledger is the extractor's shape inventory, not
5277
+ // any one rule's; every consumer declares which shapes it consumes.
5278
+ id: "record-property-assign",
5279
+ syntax: "ctx.record.<field> = \u2026 | ctx.record['<field>'] \u27E8op\u27E9= \u2026",
5280
+ example: {
5281
+ source: "ctx.record.stage = 'won'; ctx.record['amount'] += 1;",
5282
+ writes: [{ field: "stage" }, { field: "amount" }]
5283
+ }
5284
+ },
5285
+ {
5286
+ id: "api-crud-literal",
5287
+ syntax: "ctx.api.object('<object>').insert({\u2026}) | .create({\u2026}) | .update({\u2026}) | .updateById(id, {\u2026})",
5288
+ example: {
5289
+ // Real ObjectRepository signatures: the record payload is argument 0 for
5290
+ // insert/create/update and argument 1 for updateById. (`update(data)` —
5291
+ // NOT `update(id, data)`; the id travels inside the payload/options.)
5292
+ source: "await ctx.api.object('audit_log').insert({ event: 'won' }); await ctx.api.object('crm_deal').updateById(id, { stage: 'won' });",
5293
+ writes: [
5294
+ { field: "event", object: "audit_log" },
5295
+ { field: "stage", object: "crm_deal" }
5296
+ ]
5297
+ }
5298
+ }
5299
+ ];
5300
+ var HOOK_BODY_WRITE_PATTERN_IDS = [
5301
+ "input-property-assign",
5302
+ "input-object-assign",
5303
+ "api-crud-literal"
5304
+ ];
5305
+ var HOOK_BODY_WRITE_EXCLUSIONS = [
5306
+ {
5307
+ id: "record-property-assign",
5308
+ reason: "a hook sandbox context has no `ctx.record` at all \u2014 `buildSandboxContext` never sets it (a hook\u2019s record IS `ctx.input`), so the expression throws at run time rather than silently no-op\u2019ing. A loud failure the author sees on the first run is not this advisory rule\u2019s business"
5309
+ }
5310
+ ];
5311
+ var HOOK_APPLICABLE_IDS = new Set(HOOK_BODY_WRITE_PATTERN_IDS);
5312
+ var API_WRITE_METHODS = /* @__PURE__ */ new Map([
5313
+ ["insert", 0],
5314
+ ["create", 0],
5315
+ ["update", 0],
5316
+ ["updateById", 1]
5317
+ ]);
5318
+ var INPUT_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["id", "options", "ast", "data"]);
5319
+ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
5320
+ ...SYSTEM_FIELDS,
5321
+ "_id",
5322
+ "name",
5323
+ "space",
5324
+ "owner",
5325
+ "record_type"
5326
+ ]);
5327
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5328
+ function asArray33(v) {
5329
+ if (Array.isArray(v)) return v.filter((x) => isRec8(x));
5330
+ if (isRec8(v)) {
5331
+ return Object.entries(v).map(([name, def]) => ({
5332
+ name,
5333
+ ...isRec8(def) ? def : {}
5334
+ }));
5335
+ }
5336
+ return [];
5337
+ }
5338
+ function indexObjectFields2(stack) {
5339
+ const out = /* @__PURE__ */ new Map();
5340
+ for (const obj of asArray33(stack.objects)) {
5341
+ const name = typeof obj.name === "string" ? obj.name : void 0;
5342
+ if (!name) continue;
5343
+ const names = /* @__PURE__ */ new Set();
5344
+ for (const f of asArray33(obj.fields)) {
5345
+ if (typeof f.name === "string" && f.name) names.add(f.name);
5346
+ }
5347
+ out.set(name, names);
5348
+ }
5349
+ return out;
5350
+ }
5351
+ function judgeableFieldsOf(index, objectName) {
5352
+ const declared = index.get(objectName);
5353
+ if (!declared || declared.size === 0) return void 0;
5354
+ return declared;
5355
+ }
5356
+ function extractHookBodyWrites(source) {
5357
+ return extractHookBodyWriteSet(source).writes;
5358
+ }
5359
+ function extractHookBodyWriteSet(source) {
5360
+ if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
5361
+ return { writes: [], ctxRecordEscapes: false };
5362
+ }
5363
+ const tsc = loadTypeScript2();
5364
+ const sf = tsc.createSourceFile(
5365
+ "hook-body.ts",
5366
+ `async function __body(ctx) {
5367
+ ${source}
5368
+ }`,
5369
+ tsc.ScriptTarget.Latest,
5370
+ /* setParentNodes */
5371
+ false,
5372
+ tsc.ScriptKind.TS
5373
+ );
5374
+ const writes = [];
5375
+ const recordRefs = [];
5376
+ const consumedRecordRefs = /* @__PURE__ */ new Set();
5377
+ const isCtxDot = (node, prop) => tsc.isPropertyAccessExpression(node) && tsc.isIdentifier(node.expression) && node.expression.text === "ctx" && node.name.text === prop;
5378
+ const fieldOfCtxLhs = (lhs, prop) => {
5379
+ if (tsc.isPropertyAccessExpression(lhs) && tsc.isIdentifier(lhs.name) && isCtxDot(lhs.expression, prop)) {
5380
+ return lhs.name.text;
5381
+ }
5382
+ if (tsc.isElementAccessExpression(lhs) && isCtxDot(lhs.expression, prop)) {
5383
+ const arg = lhs.argumentExpression;
5384
+ if (tsc.isStringLiteral(arg) || tsc.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
5385
+ }
5386
+ return void 0;
5387
+ };
5388
+ const literalObjectKeys = (node) => {
5389
+ if (!tsc.isObjectLiteralExpression(node)) return [];
5390
+ const keys = [];
5391
+ for (const p of node.properties) {
5392
+ if (tsc.isPropertyAssignment(p)) {
5393
+ if (tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name)) keys.push(p.name.text);
5394
+ } else if (tsc.isShorthandPropertyAssignment(p)) {
5395
+ keys.push(p.name.text);
5396
+ }
5397
+ }
5398
+ return keys;
5399
+ };
5400
+ const visit = (node) => {
5401
+ if (tsc.isBinaryExpression(node) && node.operatorToken.kind >= tsc.SyntaxKind.FirstAssignment && node.operatorToken.kind <= tsc.SyntaxKind.LastAssignment) {
5402
+ const inputField = fieldOfCtxLhs(node.left, "input");
5403
+ if (inputField !== void 0 && !INPUT_ENVELOPE_KEYS.has(inputField)) {
5404
+ writes.push({ patternId: "input-property-assign", field: inputField });
5405
+ }
5406
+ const recordField = fieldOfCtxLhs(node.left, "record");
5407
+ if (recordField !== void 0) {
5408
+ writes.push({ patternId: "record-property-assign", field: recordField });
5409
+ }
5410
+ }
5411
+ if (tsc.isPropertyAccessExpression(node) || tsc.isElementAccessExpression(node)) {
5412
+ if (isCtxDot(node.expression, "record")) consumedRecordRefs.add(node.expression);
5413
+ }
5414
+ if (tsc.isBinaryExpression(node)) {
5415
+ const op = node.operatorToken.kind;
5416
+ if ((op === tsc.SyntaxKind.AmpersandAmpersandToken || op === tsc.SyntaxKind.BarBarToken || op === tsc.SyntaxKind.QuestionQuestionToken) && isCtxDot(node.left, "record")) {
5417
+ consumedRecordRefs.add(node.left);
5418
+ }
5419
+ }
5420
+ if (tsc.isPrefixUnaryExpression(node) && node.operator === tsc.SyntaxKind.ExclamationToken) {
5421
+ if (isCtxDot(node.operand, "record")) consumedRecordRefs.add(node.operand);
5422
+ }
5423
+ if (tsc.isTypeOfExpression(node) && isCtxDot(node.expression, "record")) {
5424
+ consumedRecordRefs.add(node.expression);
5425
+ }
5426
+ if ((tsc.isIfStatement(node) || tsc.isWhileStatement(node) || tsc.isDoStatement(node)) && isCtxDot(node.expression, "record")) {
5427
+ consumedRecordRefs.add(node.expression);
5428
+ }
5429
+ if (tsc.isConditionalExpression(node) && isCtxDot(node.condition, "record")) {
5430
+ consumedRecordRefs.add(node.condition);
5431
+ }
5432
+ if (isCtxDot(node, "record")) recordRefs.push(node);
5433
+ if (tsc.isCallExpression(node)) {
5434
+ const callee = node.expression;
5435
+ if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.expression) && callee.expression.text === "Object" && callee.name.text === "assign" && node.arguments.length >= 2 && isCtxDot(node.arguments[0], "input")) {
5436
+ for (const arg of node.arguments.slice(1)) {
5437
+ for (const field of literalObjectKeys(arg)) {
5438
+ if (!INPUT_ENVELOPE_KEYS.has(field)) {
5439
+ writes.push({ patternId: "input-object-assign", field });
5440
+ }
5441
+ }
5442
+ }
5443
+ }
5444
+ if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.name)) {
5445
+ const payloadIndex = API_WRITE_METHODS.get(callee.name.text);
5446
+ const recv = callee.expression;
5447
+ if (payloadIndex !== void 0 && tsc.isCallExpression(recv) && tsc.isPropertyAccessExpression(recv.expression) && recv.expression.name.text === "object" && isCtxDot(recv.expression.expression, "api") && recv.arguments.length === 1) {
5448
+ const objArg = recv.arguments[0];
5449
+ const objectName = tsc.isStringLiteral(objArg) || tsc.isNoSubstitutionTemplateLiteral(objArg) ? objArg.text : void 0;
5450
+ const payload = node.arguments[payloadIndex];
5451
+ if (objectName && payload !== void 0) {
5452
+ for (const field of literalObjectKeys(payload)) {
5453
+ writes.push({
5454
+ patternId: "api-crud-literal",
5455
+ object: objectName,
5456
+ method: callee.name.text,
5457
+ field
5458
+ });
5459
+ }
5460
+ }
5461
+ }
5462
+ }
5463
+ }
5464
+ tsc.forEachChild(node, visit);
5465
+ };
5466
+ visit(sf);
5467
+ return {
5468
+ writes,
5469
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
5470
+ };
5471
+ }
5472
+ function validateHookBodyWrites(stack) {
5473
+ const findings = [];
5474
+ const hooks = asArray33(stack.hooks);
5475
+ if (hooks.length === 0) return findings;
5476
+ let objectFields = null;
5477
+ hooks.forEach((hook, hookIndex) => {
5478
+ const body = hook.body;
5479
+ if (!isRec8(body) || body.language !== "js") return;
5480
+ const source = body.source;
5481
+ if (typeof source !== "string" || source.trim() === "") return;
5482
+ const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
5483
+ if (writes.length === 0) return;
5484
+ objectFields ?? (objectFields = indexObjectFields2(stack));
5485
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
5486
+ const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
5487
+ (o) => typeof o === "string" && o.trim() !== ""
5488
+ );
5489
+ const targetSets = targets.map((t) => judgeableFieldsOf(objectFields, t));
5490
+ const inputJudgeable = targets.length > 0 && !targets.includes("*") && targetSets.every((s) => s !== void 0);
5491
+ const where = `hook "${hookName}" \u203A body`;
5492
+ const path = `hooks[${hookIndex}].body.source`;
5493
+ const reported = /* @__PURE__ */ new Set();
5494
+ for (const w of writes) {
5495
+ const dedupeKey = `${w.object ?? ""}\0${w.field}`;
5496
+ if (reported.has(dedupeKey)) continue;
5497
+ if (w.object === void 0) {
5498
+ if (!inputJudgeable) continue;
5499
+ if (IMPLICIT_FIELDS2.has(w.field)) continue;
5500
+ if (targetSets.some((s) => s.has(w.field))) continue;
5501
+ reported.add(dedupeKey);
5502
+ const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
5503
+ const declares = targets.length === 1 ? "declares no such field" : "declare that field";
5504
+ findings.push({
5505
+ severity: "warning",
5506
+ rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
5507
+ where,
5508
+ path,
5509
+ message: `body writes '${w.field}' to its input, but ${objDesc} ${declares}. The sandboxed script runs clean and the value is copied back onto the record payload unfiltered \u2014 on a SQL driver the stray column then fails the WHOLE write with a driver-level error far from here; on a schemaless driver (memory, MongoDB) it is persisted as an undeclared key (#4271).`,
5510
+ hint: fixHint(w.field, unionCandidates(targetSets))
5511
+ });
5512
+ } else {
5513
+ const known = judgeableFieldsOf(objectFields, w.object);
5514
+ if (!known) continue;
5515
+ if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
5516
+ reported.add(dedupeKey);
5517
+ findings.push({
5518
+ severity: "warning",
5519
+ rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
5520
+ where,
5521
+ path,
5522
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', but object '${w.object}' declares no such field. The write-path validator skips the unknown key \u2014 on a SQL driver the whole call then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted (#4271).`,
5523
+ hint: fixHint(w.field, [...known])
5524
+ });
5525
+ }
5526
+ }
5527
+ });
5528
+ return findings;
5529
+ }
5530
+ function unionCandidates(targetSets) {
5531
+ const out = /* @__PURE__ */ new Set();
5532
+ for (const s of targetSets) for (const f of s ?? []) out.add(f);
5533
+ return [...out];
5534
+ }
5535
+ function fixHint(field, declared) {
5536
+ const suggestion = formatSuggestion(findClosestMatches(field, [...declared, ...IMPLICIT_FIELDS2]));
5537
+ return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in HOOK_BODY_WRITE_PATTERNS are checked \u2014 computed keys, spreads and aliased input are not \u2014 and this warning never blocks a build.`;
5538
+ }
5539
+
5540
+ // src/validate-action-body-writes.ts
5541
+ import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
5542
+ var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
5543
+ var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
5544
+ var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
5545
+ var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
5546
+ var ACTION_BODY_WRITE_EXCLUSIONS = [
5547
+ {
5548
+ id: "input-property-assign",
5549
+ reason: "an action's ctx.input is its params bag (`input: unwrapProxyToPlain(actionCtx?.params)`), not a record \u2014 `ctx.input.<name>` writes a declared PARAMETER, which object fields cannot judge"
5550
+ },
5551
+ {
5552
+ id: "input-object-assign",
5553
+ reason: "same surface as input-property-assign \u2014 Object.assign(ctx.input, \u2026) targets the params bag"
5554
+ }
5555
+ ];
5556
+ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
5557
+ var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
5558
+ var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
5559
+ var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
5560
+ var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5561
+ function asArray34(v) {
5562
+ if (Array.isArray(v)) return v.filter((x) => isRec9(x));
5563
+ if (isRec9(v)) {
5564
+ return Object.entries(v).map(([name, def]) => ({
5565
+ name,
5566
+ ...isRec9(def) ? def : {}
5567
+ }));
5568
+ }
5569
+ return [];
5570
+ }
5571
+ function actionObjectBinding(action, parentObject) {
5572
+ if (typeof action.object === "string" && action.object) return action.object;
5573
+ if (typeof action.objectName === "string" && action.objectName) return action.objectName;
5574
+ return parentObject;
5575
+ }
5576
+ function collectActionBodies(stack) {
5577
+ const sites = [];
5578
+ const seen = /* @__PURE__ */ new Set();
5579
+ const collect = (actions, pathPrefix, parentObject) => {
5580
+ asArray34(actions).forEach((action, index) => {
5581
+ const body = action.body;
5582
+ if (!isRec9(body) || body.language !== "js") return;
5583
+ const source = body.source;
5584
+ if (typeof source !== "string" || source.trim() === "") return;
5585
+ const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
5586
+ const key = `${actionObjectBinding(action, parentObject) ?? ""}\0${name}\0${source}`;
5587
+ if (seen.has(key)) return;
5588
+ seen.add(key);
5589
+ sites.push({ name, source, path: `${pathPrefix}[${index}].body.source` });
5590
+ });
5591
+ };
5592
+ collect(stack.actions, "actions");
5593
+ asArray34(stack.objects).forEach((obj, objIndex) => {
5594
+ const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
5595
+ collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
5596
+ });
5597
+ return sites;
5598
+ }
5599
+ function validateActionBodyWrites(stack) {
5600
+ const findings = [];
5601
+ if (!isRec9(stack)) return findings;
5602
+ const sites = collectActionBodies(stack);
5603
+ if (sites.length === 0) return findings;
5604
+ let objectFields = null;
5605
+ for (const site of sites) {
5606
+ if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
5607
+ const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
5608
+ const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
5609
+ const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
5610
+ if (writes.length === 0 && recordWrites.length === 0) continue;
5611
+ const where = `action "${site.name}" \u203A body`;
5612
+ if (recordWrites.length > 0 && !ctxRecordEscapes) {
5613
+ const reportedFields = /* @__PURE__ */ new Set();
5614
+ for (const w of recordWrites) {
5615
+ if (reportedFields.has(w.field)) continue;
5616
+ reportedFields.add(w.field);
5617
+ findings.push({
5618
+ severity: "warning",
5619
+ rule: ACTION_RECORD_WRITE_DISCARDED,
5620
+ where,
5621
+ path: site.path,
5622
+ message: `body assigns ctx.record.${w.field}, but an action's ctx.record is a plain snapshot the runtime never writes back \u2014 the action returns success and the assignment is discarded, whether or not '${w.field}' is a declared field (#4345).`,
5623
+ hint: `To persist it, write through the API: ctx.api.object('<object>').updateById(ctx.recordId, { ${w.field}: \u2026 }). Reported only because ctx.record is never passed anywhere in this body \u2014 mutating the snapshot and then handing it to an API write is a live payload and is not flagged. This warning never blocks a build.`
5624
+ });
5625
+ }
5626
+ }
5627
+ if (writes.length === 0) continue;
5628
+ objectFields ?? (objectFields = indexObjectFields2(stack));
5629
+ const reported = /* @__PURE__ */ new Set();
5630
+ for (const w of writes) {
5631
+ if (w.object === void 0) continue;
5632
+ const dedupeKey = `${w.object}\0${w.field}`;
5633
+ if (reported.has(dedupeKey)) continue;
5634
+ const known = judgeableFieldsOf(objectFields, w.object);
5635
+ if (!known) continue;
5636
+ if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
5637
+ reported.add(dedupeKey);
5638
+ findings.push({
5639
+ severity: "warning",
5640
+ rule: ACTION_BODY_WRITE_UNKNOWN_FIELD,
5641
+ where,
5642
+ path: site.path,
5643
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', but object '${w.object}' declares no such field. The write-path validator skips the unknown key \u2014 on a SQL driver the whole action then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted (#4271).`,
5644
+ hint: fixHint2(w.field, [...known])
5645
+ });
5646
+ }
5647
+ }
5648
+ return findings;
5649
+ }
5650
+ function fixHint2(field, declared) {
5651
+ const suggestion = formatSuggestion2(findClosestMatches2(field, [...declared, ...IMPLICIT_FIELDS2]));
5652
+ return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in ACTION_BODY_WRITE_PATTERNS are checked \u2014 an action's ctx.input is its params bag, so it is not a record-write surface and is never resolved against fields \u2014 and this warning never blocks a build.`;
5653
+ }
5654
+
5655
+ // src/validate-flow-node-writes.ts
5656
+ import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
5657
+ var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
5658
+ var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
5659
+ var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
5660
+ var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5661
+ function asArray35(v) {
5662
+ if (Array.isArray(v)) return v.filter((x) => isRec10(x));
5663
+ if (isRec10(v)) {
5664
+ return Object.entries(v).map(([name, def]) => ({
5665
+ name,
5666
+ ...isRec10(def) ? def : {}
5667
+ }));
5668
+ }
5669
+ return [];
5670
+ }
5671
+ function readLiteralObjectName2(config) {
5672
+ const raw = config.objectName ?? config.object;
5673
+ if (typeof raw !== "string" || raw.includes("{")) return void 0;
5674
+ return raw || void 0;
5675
+ }
5676
+ var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
5677
+ function validateFlowNodeWrites(stack) {
5678
+ const findings = [];
5679
+ if (!isRec10(stack)) return findings;
5680
+ const flows = asArray35(stack.flows);
5681
+ if (flows.length === 0) return findings;
5682
+ let objectFields = null;
5683
+ flows.forEach((flow, flowIndex) => {
5684
+ const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
5685
+ const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
5686
+ walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
5687
+ if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
5688
+ const config = isRec10(node.config) ? node.config : void 0;
5689
+ if (!config) return;
5690
+ const fields = config.fields;
5691
+ if (!isRec10(fields)) return;
5692
+ const written = Object.keys(fields);
5693
+ if (written.length === 0) return;
5694
+ const objectName = readLiteralObjectName2(config);
5695
+ if (!objectName) return;
5696
+ objectFields ?? (objectFields = indexObjectFields2(stack));
5697
+ const known = judgeableFieldsOf(objectFields, objectName);
5698
+ if (!known) return;
5699
+ const nodeName = flowNodeLabel(node, walkIndex);
5700
+ const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
5701
+ for (const fieldName of written) {
5702
+ if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
5703
+ if (fieldName.includes(".")) continue;
5704
+ findings.push({
5705
+ severity: "error",
5706
+ rule: FLOW_NODE_WRITE_UNKNOWN_FIELD,
5707
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
5708
+ path: `${nodePath}.config.fields.${fieldName}`,
5709
+ message: `${node.type} writes '${fieldName}', but object '${objectName}' declares no such field. Nothing between the node and storage removes the key: on a SQL datasource the driver rejects the whole statement ('no such column'), so the correctly named fields in this same payload never land either${node.type === "create_record" ? " and the record is never created at all" : ""}; on a schemaless one the stray key is persisted into a column no read surface returns.`,
5710
+ hint: fixHint3(fieldName, [...known])
5711
+ });
5712
+ }
5713
+ });
5714
+ });
5715
+ return findings;
5716
+ }
5717
+ function fixHint3(field, declared) {
5718
+ const suggestion = formatSuggestion3(findClosestMatches3(field, [...declared, ...IMPLICIT_FIELDS2]));
5719
+ return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. This gates the build rather than warning: the key is literal and so is the object, so unlike the hook/action body rules there is nothing here that could have been mis-extracted.`;
5720
+ }
5721
+
4856
5722
  // src/reference-integrity-suite.ts
4857
5723
  var REFERENCE_INTEGRITY_RULES = [
4858
5724
  { name: "validateObjectReferences", run: validateObjectReferences },
5725
+ { name: "validateSearchableFields", run: validateSearchableFields },
4859
5726
  { name: "validateActionNameRefs", run: validateActionNameRefs },
4860
5727
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
4861
5728
  { name: "validateChartBindings", run: validateChartBindings },
@@ -4864,7 +5731,70 @@ var REFERENCE_INTEGRITY_RULES = [
4864
5731
  { name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
4865
5732
  { name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
4866
5733
  { name: "validateAiToolReferences", run: validateAiToolReferences },
4867
- { name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring }
5734
+ { name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring },
5735
+ // Field names WRITTEN by an L2 hook body (`ctx.input.x = …`,
5736
+ // `ctx.api.object('y').update({ x })`), resolved against the target object's
5737
+ // declared fields — the write-side counterpart of validateFlowTemplatePaths'
5738
+ // read-side membership (#4271). Lazy: only a hook that actually carries a
5739
+ // `language:'js'` body loads the TypeScript parser.
5740
+ { name: "validateHookBodyWrites", run: validateHookBodyWrites },
5741
+ // The same check on the other surface that carries a `HookBodySchema` body:
5742
+ // action bodies, run by the same sandbox. Only the `ctx.api` write family
5743
+ // carries over — an action's `ctx.input` is its params bag, not a record
5744
+ // (see that module's ledger). Lazy on the same terms.
5745
+ //
5746
+ // The first member here to emit more than one rule id (`validateReactPageProps`
5747
+ // below is the other, and carries the most). Besides resolving `ctx.api`
5748
+ // writes against declared fields (`action-body-write-unknown-field`), it
5749
+ // reports a `ctx.record` write that can reach nothing
5750
+ // (`action-record-write-discarded`, #4345) — not a resolution question, so
5751
+ // by the charter above it does not belong in the suite. It rides along
5752
+ // anyway because it falls out of the SAME parse of the SAME source: a
5753
+ // separate member would parse every action body twice to say two things
5754
+ // about one walk, and hand-wiring it into the CLI instead is exactly the
5755
+ // drift this suite exists to end — which `validateReadonlyFlowWrites` was
5756
+ // the standing proof of, until it joined the suite below.
5757
+ { name: "validateActionBodyWrites", run: validateActionBodyWrites },
5758
+ // The third surface that writes a record field set: a flow `update_record`
5759
+ // node's `config.fields`. Same question as the two rules above, but the map
5760
+ // is structural metadata rather than parsed JS, so a finding is a certainty
5761
+ // and gates (`error`) — see that module for why, and why the docs' long-
5762
+ // standing "prefer a flow node, it's checked" advice was the least true of
5763
+ // the three until it landed.
5764
+ { name: "validateFlowNodeWrites", run: validateFlowNodeWrites },
5765
+ // The OTHER question about that same `config.fields` map: not "does this
5766
+ // field exist?" but "is it writable?" — a `runAs:'user'` update_record
5767
+ // writing a static-`readonly` field is stripped by the engine and the step
5768
+ // still reports success (#2948/#3425). It walks the identical map the rule
5769
+ // above walks, so the two splitting call sites was never defensible: hand-
5770
+ // wired into `validate` and `compile` only, it left `os lint` PASSING a flow
5771
+ // `os validate` refuses — and this one gates, so the divergence shipped a
5772
+ // build the other command would have stopped. Joining the suite is the whole
5773
+ // fix; the two hand-wired call sites are deleted with it (#4345 follow-up).
5774
+ { name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
5775
+ // The `kind:'react'` page surface. Every prop a react block binds BY FIELD
5776
+ // NAME is resolved against the object it names (#4340) — `<ListView columns>`,
5777
+ // `<ObjectForm fields>`, the `record:*` family through the SAME
5778
+ // `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
5779
+ // over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
5780
+ // `searchableFields` (#4329). Squarely the charter's question, on the surface
5781
+ // where it had no answer at all.
5782
+ //
5783
+ // It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
5784
+ // accepted a react page whose every field binding was stale — including the
5785
+ // gating ones (a missing required binding, a filter position naming no field:
5786
+ // the predicate can never match and the list comes back empty). That is
5787
+ // `validateReadonlyFlowWrites`' divergence again, one surface over, and it is
5788
+ // the reason this entry exists rather than a fourth hand-wiring.
5789
+ //
5790
+ // Like `validateActionBodyWrites` above, it emits ids that are not resolution
5791
+ // questions — `react-prop-missing-required` and `react-prop-typo` are shape,
5792
+ // and by the charter belong outside. They ride along for the same reason: they
5793
+ // fall out of the SAME TypeScript parse of the SAME page source, and splitting
5794
+ // them into a second member would parse every react page twice to say two
5795
+ // things about one walk. Lazy on the same terms as the hook/action body rules
5796
+ // — only a page that is actually `kind:'react'` loads the compiler.
5797
+ { name: "validateReactPageProps", run: validateReactPageProps }
4868
5798
  ];
4869
5799
  function validateReferenceIntegrity(stack) {
4870
5800
  const findings = [];
@@ -4874,7 +5804,14 @@ function validateReferenceIntegrity(stack) {
4874
5804
  return findings;
4875
5805
  }
4876
5806
  export {
5807
+ ACTION_BODY_WRITE_EXCLUSIONS,
5808
+ ACTION_BODY_WRITE_PATTERNS,
5809
+ ACTION_BODY_WRITE_PATTERN_IDS,
5810
+ ACTION_BODY_WRITE_UNKNOWN_FIELD,
4877
5811
  ACTION_NAME_UNDEFINED,
5812
+ ACTION_RECORD_WRITE_DISCARDED,
5813
+ ACTION_RECORD_WRITE_PATTERNS,
5814
+ ACTION_RECORD_WRITE_PATTERN_IDS,
4878
5815
  AGENT_AUTHORING_WITHDRAWN,
4879
5816
  AI_SKILL_SURFACE_MISMATCH,
4880
5817
  AI_SKILL_TOOL_UNRESOLVED,
@@ -4901,13 +5838,20 @@ export {
4901
5838
  FIELD_GROUP_UNDECLARED,
4902
5839
  FILTER_TOKEN_UNKNOWN,
4903
5840
  FLOW_DRAFT_STATUS_AMBIGUOUS,
5841
+ FLOW_NODE_WRITE_UNKNOWN_FIELD,
4904
5842
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
4905
5843
  FLOW_TEMPLATE_UNKNOWN_FIELD,
4906
5844
  FLOW_TRIGGER_UNKNOWN_OBJECT,
4907
5845
  FLOW_UPDATE_READONLY_FIELD,
4908
5846
  FLOW_UPDATE_READONLY_WHEN_FIELD,
5847
+ FLOW_WRITE_NODE_TYPES,
5848
+ FLOW_WRITE_NODE_TYPES_DEFERRED,
4909
5849
  FORM_COLSPAN_ABSOLUTE,
4910
5850
  FORM_FIELD_UNKNOWN,
5851
+ HOOK_BODY_WRITE_EXCLUSIONS,
5852
+ HOOK_BODY_WRITE_PATTERNS,
5853
+ HOOK_BODY_WRITE_PATTERN_IDS,
5854
+ HOOK_BODY_WRITE_UNKNOWN_FIELD,
4911
5855
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
4912
5856
  MEASURE_AGGREGATE_INCOHERENT,
4913
5857
  NAV_OBJECT_UNGRANTED,
@@ -4921,6 +5865,7 @@ export {
4921
5865
  REACT_CHART_AXIS_UNKNOWN,
4922
5866
  REACT_CHART_FIELD_UNKNOWN,
4923
5867
  REFERENCE_INTEGRITY_RULES,
5868
+ SEARCHABLE_FIELD_UNKNOWN,
4924
5869
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
4925
5870
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
4926
5871
  SECURITY_DELEGATION_MISSING_REASON,
@@ -4953,6 +5898,9 @@ export {
4953
5898
  WIDGET_MEASURE_UNKNOWN,
4954
5899
  buildAccessMatrix,
4955
5900
  diffAccessMatrix,
5901
+ extractHookBodyWriteSet,
5902
+ extractHookBodyWrites,
5903
+ validateActionBodyWrites,
4956
5904
  validateActionNameRefs,
4957
5905
  validateAiAgentAuthoring,
4958
5906
  validateAiSurfaceAffinity,
@@ -4962,9 +5910,11 @@ export {
4962
5910
  validateChartBindings,
4963
5911
  validateDashboardActionRefs,
4964
5912
  validateFilterTokens,
5913
+ validateFlowNodeWrites,
4965
5914
  validateFlowTemplatePaths,
4966
5915
  validateFlowTriggerReadiness,
4967
5916
  validateFormLayout,
5917
+ validateHookBodyWrites,
4968
5918
  validateJsxPages,
4969
5919
  validateListViewMode,
4970
5920
  validateNavAccess,
@@ -4978,6 +5928,7 @@ export {
4978
5928
  validateRecordTitle,
4979
5929
  validateReferenceIntegrity,
4980
5930
  validateResponsiveStyles,
5931
+ validateSearchableFields,
4981
5932
  validateSecurityPosture,
4982
5933
  validateSeedReplaySafety,
4983
5934
  validateSeedStateMachine,